- Go 100%
| pkg | ||
| .gitignore | ||
| .golangci.yml | ||
| .goreleaser.yaml | ||
| auth.go | ||
| callback.go | ||
| errs.go | ||
| go.mod | ||
| go.sum | ||
| handlers.go | ||
| info.go | ||
| LICENSE | ||
| login.go | ||
| logout.go | ||
| middleware.go | ||
| oicd.go | ||
| options.go | ||
| README.md | ||
| session.go | ||
simple-auth
A lightweight Go library providing HTTP handlers for OIDC, session-based, and API key authentication.
Features
- OIDC/OAuth 2.0 authentication with automatic token refresh
- PKCE support for enhanced security
- API key authentication with BLAKE2b hashing
- Session-based authentication with password validation and token rotation
- Cookie-based session management with encryption
- JWT profile authentication support
- Pluggable user storage via interface
Installation
go get git.crawford.zone/jd/simple-auth
Quick Start
OIDC Authentication
// Define a User type
type MyUser struct{}
func (u *MyUser) Authorized(path string) bool { return true }
func (u *MyUser) NeedsUpdate(claims *oidc.IDTokenClaims) bool {
return false
}
func (u *MyUser) GetInfo() (simpleauth.UserInfo, error) {
return simpleauth.UserInfo{Name: "display name"}, nil
}
// Define a UserHandler
type MyUserHandler struct{}
func (h *MyUserHandler) CreateUser(ctx context.Context, claims *oidc.IDTokenClaims) (string, error) {
return "", nil
}
func (h *MyUserHandler) GetUserID(ctx context.Context, subject, provider string) (string, error) {
return "", simpleauth.ErrNoUser
}
func (h *MyUserHandler) GetUser(ctx context.Context, id string) (*MyUser, error) {
return &MyUser{}, nil
}
func (h *MyUserHandler) UpdateUser(ctx context.Context, id string, claims *oidc.IDTokenClaims) (*MyUser, error) {
return &MyUser{}, nil
}
// Create Auth instance with one or more OIDC providers
auth, err := simpleauth.New[string, *MyUser](
context.Background(),
simpleauth.Handlers[string, *MyUser]{User: &MyUserHandler{}},
slog.Default(),
simpleauth.WithProvider("google",
simpleauth.WithOAuthClient("google-client-id", "google-client-secret", "https://app.com/auth/callback/google"),
simpleauth.WithIssuer("https://accounts.google.com"),
),
simpleauth.WithProvider("entra",
simpleauth.WithOAuthClient("entra-client-id", "entra-client-secret", "https://app.com/auth/callback/entra"),
simpleauth.WithIssuer("https://login.microsoftonline.com/{tenant}/v2.0"),
simpleauth.WithPkce(true),
),
simpleauth.WithSecurityKeys(hashKey, encryptionKey),
simpleauth.WithPaths(
simpleauth.WithHomePath("/"),
simpleauth.WithLoginPath("/login"),
simpleauth.WithLogoutPath("/logout"),
simpleauth.WithRedirectPath("/auth/callback"),
),
)
// Register handlers. The {provider} path segment selects the provider and
// must match a provider name configured via WithProvider.
mux := http.NewServeMux()
mux.HandleFunc("/auth/login/{provider}", auth.LoginHandler())
mux.HandleFunc("/auth/logout", auth.LogoutHandler())
mux.HandleFunc("/auth/callback/{provider}", auth.CallbackHandler())
mux.Handle("/", auth.InfoHandler(auth.RequireAuthorizedHandler(handler)))
Each provider needs its own absolute redirect URL containing its path segment
(e.g. https://app.com/auth/callback/google), registered at the identity
provider. After login the provider name is stored in an encrypted cookie so
subsequent requests and token refreshes are verified against the correct
provider.
Session Authentication
// Define SessionHandler
type MySessionHandler struct{}
func (h *MySessionHandler) ValidatePassword(ctx context.Context, userID string, password string) (bool, error) {
return password == "correct-password", nil
}
func (h *MySessionHandler) GenToken(ctx context.Context, userID string, userAgent string) (string, error) {
return "session-token", nil
}
func (h *MySessionHandler) CheckToken(ctx context.Context, token string, userID string, userAgent string) (bool, error) {
return token == "session-token", nil
}
// Create Auth instance with session support
auth, err := simpleauth.New[string, *MyUser](
context.Background(),
simpleauth.Handlers[string, *MyUser]{
User: &MyUserHandler{},
Session: &MySessionHandler{},
},
slog.Default(),
simpleauth.WithProvider("google",
simpleauth.WithOAuthClient("google-client-id", "google-client-secret", "https://app.com/auth/callback/google"),
simpleauth.WithIssuer("https://accounts.google.com"),
),
simpleauth.WithSecurityKeys(hashKey, encryptionKey),
simpleauth.WithMaxSessionLength(72*time.Hour),
)
// Authenticate user with email/password
func loginHandler(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
password := r.FormValue("password")
if err := auth.NewSession(w, r, email, password); err != nil {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
http.Redirect(w, r, "/dashboard", http.StatusFound)
}
API Key Authentication
// Define KeyHandler
type MyKeyHandler struct{}
func (h *MyKeyHandler) HashKey(key string) string {
sum := blake2b.Sum256([]byte(key))
return base64.RawURLEncoding.EncodeToString(sum[:])
}
func (h *MyKeyHandler) Valid(ctx context.Context, hashedKey string) (bool, error) {
return true, nil
}
func (h *MyKeyHandler) KeyOwner(ctx context.Context, hashedKey string) (string, error) {
return "owner-id", nil
}
// Create Auth instance with API key support
apiAuth, err := simpleauth.New[string, *MyUser](
context.Background(),
simpleauth.Handlers[string, *MyUser]{
User: &MyUserHandler{},
Key: &MyKeyHandler{},
},
slog.Default(),
simpleauth.WithProvider("google",
simpleauth.WithOAuthClient("google-client-id", "google-client-secret", "https://app.com/auth/callback/google"),
simpleauth.WithIssuer("https://accounts.google.com"),
),
simpleauth.WithSecurityKeys(hashKey, encryptionKey),
)
// Register middleware
mux := http.NewServeMux()
mux.HandleFunc("/api/data", apiAuth.InfoHandler(handler))
mux.HandleFunc("/api/private", apiAuth.InfoHandler(apiAuth.APIRequireAuthorizedHandler(handler)))
Configuration
Auth Options
Global options passed to New:
| Option | Description |
|---|---|
WithSecurityKeys |
Hash key (32/64 bytes) and encryption key (16/24/32 bytes) |
WithProvider |
Register an OIDC provider by name with one or more ProviderOptions |
WithInsecureHost |
Allow HTTP cookies |
WithCookiePrefix |
Cookie prefix (default: auth) |
WithMinAPIKeyLength |
Minimum API key length (default: 32) |
WithPaths |
Path configuration |
WithMaxSessionLength |
Max session duration (default: 24h) |
ProviderOptions passed to WithProvider(name, ...ProviderOption):
| Option | Description |
|---|---|
WithOAuthClient |
Client ID, secret, and redirect URL |
WithClientID |
OAuth client ID |
WithClientSecret |
OAuth client secret |
WithRedirectURL |
OAuth redirect URL |
WithIssuer |
OIDC issuer URL |
WithScopes |
OAuth scopes (default: openid, email, profile) |
WithPkce |
Enable PKCE flow |
WithJWKSKey |
JWT signing key path and key ID |
WithResponseMode |
OAuth response mode |
WithInsecureProvider |
Skip TLS verification for this provider |
Paths
| Field | Description | Default |
|---|---|---|
| Home | Post-login redirect | / |
| Login | Login page prefix | /auth/login |
| Logout | Logout handler | /auth/logout |
| Redirect | OAuth callback prefix | /auth/callback |
| Unauthorized | Unauthorized redirect | - |
The Login and Redirect prefixes are registered with a /{provider} segment,
e.g. /auth/login/{provider} and /auth/callback/{provider}. Login is also
used as the redirect target when an unauthenticated user hits a protected page:
with a single provider the library redirects directly to
{Login}/{provider}; with zero or multiple providers it redirects to Login,
so point it at a password login page or a page that offers a provider chooser.
Migrating to Multi-Provider
This release replaces the single global OIDC configuration with a per-provider configuration. Existing setups need the following changes:
-
Options become provider options. The single-provider options (
WithOAuthClient,WithIssuer,WithClientID,WithClientSecret,WithRedirectURL,WithScopes,WithPkce,WithJWKSKey,WithResponseMode,WithInsecureProvider) are nowProviderOptions and must be passed toWithProvider(name, ...).Before:
simpleauth.WithOAuthClient("id", "secret", "https://app.com/auth/callback"), simpleauth.WithIssuer("https://issuer.com"),After:
simpleauth.WithProvider("default", simpleauth.WithOAuthClient("id", "secret", "https://app.com/auth/callback/default"), simpleauth.WithIssuer("https://issuer.com"), ), -
Mux patterns gain a
{provider}segment. Register/auth/login/{provider}and/auth/callback/{provider}(Go 1.22+ mux). The segment must match a provider name fromWithProvider. -
UserHandler.GetUserIDsignature changed. It now takes(ctx, subject, provider). The subject is only unique within the provider; store the composite(subject, provider)identity. -
Providers are now optional. Password-only and API-key deployments may configure zero providers on the same
Authinstance.
Existing sessions (cookies from before the upgrade, which lack the provider cookie) are treated as unauthenticated and redirected to login once. Acceptable for a breaking release.
Interfaces
UserHandler
type UserHandler[ID any, U User] interface {
CreateUser(ctx context.Context, claims *oidc.IDTokenClaims) (ID, error)
GetUserID(ctx context.Context, subject, provider string) (ID, error)
GetUser(ctx context.Context, id ID) (U, error)
UpdateUser(ctx context.Context, id ID, claims *oidc.IDTokenClaims) (U, error)
}
GetUserID's subject is only unique within the given provider, so handlers
must store the composite (subject, provider) identity. CreateUser and
UpdateUser receive the full ID token claims (including the issuer) and are
unchanged.
User
type User interface {
Authorized(path string) bool
NeedsUpdate(claims *oidc.IDTokenClaims) bool
GetInfo() (UserInfo, error)
}
KeyHandler
type KeyHandler[ID any] interface {
HashKey(key string) string
Valid(ctx context.Context, hashedKey string) (bool, error)
KeyOwner(ctx context.Context, hashedKey string) (ID, error)
}
SessionHandler
type SessionHandler[ID any] interface {
ValidatePassword(ctx context.Context, userID ID, password string) (bool, error)
GenToken(ctx context.Context, userID ID, userAgent string) (string, error)
CheckToken(ctx context.Context, token string, userID ID, userAgent string) (bool, error)
}
Middleware
InfoHandler
Extracts authentication info and adds it to the request context. Handles OIDC, session, and API key auth based on request attributes. Must be used before RequireAuthorizedHandler.
mux.Handle("/protected", auth.InfoHandler(handler))
RequireAuthorizedHandler
For web pages - redirects to login/unauthorized pages when not authenticated/authorized.
mux.Handle("/dashboard", auth.InfoHandler(auth.RequireAuthorizedHandler(handler)))
APIRequireAuthorizedHandler
For API endpoints - returns HTTP 401/403 with WWW-Authenticate header instead of redirects.
mux.Handle("/api/data", auth.InfoHandler(auth.APIRequireAuthorizedHandler(handler)))
Session Management
NewSession
Creates a new session for a user given email and password. Validates credentials via SessionHandler.ValidatePassword, generates a token via SessionHandler.GenToken, and sets an encrypted session cookie.
err := auth.NewSession(w, r, email, password)
RefreshSession
Refreshes the session issuance timestamp to extend its lifetime without re-authentication. Automatically called when the session is within 30 minutes of expiry.
err := auth.RefreshSession(w, r, sessionToken)
Errors
| Error | Description |
|---|---|
ErrNoUser |
Returned by UserHandler.GetUserID when user not found |
ErrInvalidApiKey |
Invalid or expired API key |
ErrInvalidSession |
Session token validation failed |
ErrInvalidPassword |
Invalid email or password |
ErrExpiredSession |
Session has exceeded max lifetime |
Accessing User Information
func handler(w http.ResponseWriter, r *http.Request) {
info := auth.GetInfo(r)
if !info.Authenticated {
// Redirect to your login page, which typically presents a provider
// chooser linking to /auth/login/{provider}.
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// info.ID - User ID
// info.Name - Display name
// info.Picture - Profile URL
// info.Authorized - Path authorization status
}
Accessing API Key Information
func handler(w http.ResponseWriter, r *http.Request) {
info := apiAuth.GetInfo(r)
if !info.Authenticated {
w.Header().Set("WWW-Authenticate", `Bearer realm="Restricted"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// info.HashedKey - BLAKE2b-hashed API key
}