go-authkit
Reusable, framework- and database-agnostic authentication for Go services:
local password login, opaque server-side sessions, OIDC identity linking, CSRF,
and role-based access control. Extracted so security fixes land in one place and
propagate to every consumer via a version bump.
Design
The database is fully decoupled behind the auth.Store
interface. Business logic in auth.Service contains no SQL
and no HTTP framework dependency. HTTP wiring lives in optional sibling
packages that target the standard net/http (so they compose with chi, gorilla,
echo, gin adapters, etc.).
auth/ Service, Store interface, User/Session types, password (Argon2id), cookies
store/sqlite/ SQLite implementation of auth.Store (bring your own *sql.DB driver)
oidc/ OIDC Authorization Code flow (discovery, state/nonce, callback)
httpmw/ net/http middleware: RequireAuth, RequireCSRF, RequireAdmin, RequireRole
To support Postgres in another project, implement auth.Store against Postgres;
nothing else changes.
Install
go get github.com/blurrycontour/go-authkit@latest
Quick start (SQLite + chi)
import (
"database/sql"
_ "modernc.org/sqlite"
"github.com/blurrycontour/go-authkit/auth"
"github.com/blurrycontour/go-authkit/httpmw"
sqlitestore "github.com/blurrycontour/go-authkit/store/sqlite"
)
db, _ := sql.Open("sqlite", "file:app.db?_pragma=foreign_keys(ON)")
store := sqlitestore.New(db)
if err := store.Migrate(ctx); err != nil { /* ... */ } // idempotent schema
authSvc := auth.NewService(store, auth.Config{
SessionCookieName: "app_session",
SessionTTL: 30 * 24 * time.Hour,
})
_ = authSvc.EnsureBootstrapAdmin(ctx, auth.BootstrapAdmin{
Username: os.Getenv("ADMIN_USER"),
Email: os.Getenv("ADMIN_EMAIL"),
Password: os.Getenv("ADMIN_PASS"),
})
mw := &httpmw.Middleware{Auth: authSvc, Secure: requestIsSecure}
r := chi.NewRouter()
r.Post("/api/login", loginHandler(authSvc, mw))
r.With(mw.RequireAuth).Get("/api/me", meHandler)
r.With(mw.RequireAuth, mw.RequireCSRF).Post("/api/logout", logoutHandler(authSvc, mw))
r.With(mw.RequireAuth, mw.RequireAdmin).Get("/api/admin/users", listUsers(authSvc))
Login handler (sets session + CSRF cookies)
func loginHandler(a *auth.Service, mw *httpmw.Middleware) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct{ Identifier, Password string }
_ = json.NewDecoder(r.Body).Decode(&req)
user, sid, exp, err := a.Login(r.Context(), req.Identifier, req.Password, r.UserAgent(), r.RemoteAddr)
if err != nil {
http.Error(w, "invalid credentials", http.StatusUnauthorized)
return
}
secure := requestIsSecure(r)
http.SetCookie(w, a.SessionCookie(sid, exp, secure))
csrf, _ := mw.IssueCSRFCookie(r)
http.SetCookie(w, csrf)
json.NewEncoder(w).Encode(map[string]any{"user": user, "csrfToken": csrf.Value})
}
}
OIDC
The oidc.Handler runs the Authorization Code flow and delegates user
resolution to the auth.Service. Effective config is supplied per request via
ConfigFunc, so you can merge env vars with admin-UI settings without a restart.
oh := &oidc.Handler{
Auth: authSvc,
ConfigFunc: func(ctx context.Context) (oidc.Config, error) {
return loadEffectiveOIDCConfig(ctx) // your merge of env + DB
},
Secure: requestIsSecure,
ClientIP: clientIP,
OnSuccess: func(w http.ResponseWriter, r *http.Request, u *auth.User, sid string, exp time.Time) {
http.SetCookie(w, authSvc.SessionCookie(sid, exp, requestIsSecure(r)))
csrf, _ := mw.IssueCSRFCookie(r)
http.SetCookie(w, csrf)
http.Redirect(w, r, "/", http.StatusFound)
},
}
r.Get("/api/auth/oidc/login", oh.Login)
r.Get("/api/auth/oidc/callback", oh.Callback)
Security note: email-based linking
An OIDC identity is linked to a pre-existing local account by email address
only when the provider asserts email_verified: true. Unverified or absent
emails get a synthesized @oidc.local placeholder, preventing account takeover
via an attacker-controlled email that matches a victim's address.
CSRF
Stateless double-submit cookie. The CSRF cookie is readable by JS; the client
echoes it in the X-CSRF-Token header on unsafe methods. RequireCSRF compares
them in constant time. Combined with SameSite=Lax session cookies this blocks
cross-site request forgery.
Sentinel errors
auth.ErrNotFound, ErrInvalidCredentials, ErrInactive,
ErrPasswordLoginDisabled, ErrConflict, ErrRegistrationDisabled. Compare
with errors.Is.
Versioning
Semantic versioning. Security patches ship as patch releases so consumers can
go get -u (or automate with Dependabot/Renovate) and re-run store.Migrate.
Breaking API/schema changes go in a new major (/v2).