Documentation
¶
Overview ¶
Package oidcauth provides OIDC login for Go web apps using only net/http. It is a thin wrapper over github.com/coreos/go-oidc/v3 and golang.org/x/oauth2 that handles the authorization-code flow with PKCE (S256), state and nonce validation, and a signed session cookie.
It targets any conformant OIDC issuer; nothing in this package is specific to a particular identity provider.
Identity rule: key on sub, never on email ¶
Apps must key user accounts on the ID token's `sub` claim, never on email. OIDC guarantees `sub` is unique per issuer and never reassigned; it guarantees nothing about email. Emails are mutable and reassignable. Store email alongside `sub` for display and migration, but only `sub` is the key. See the README for the full rationale.
Index ¶
- type Auth
- func (a *Auth) CallbackHandler() http.Handler
- func (a *Auth) CallbackPath() string
- func (a *Auth) ClearSession(w http.ResponseWriter)
- func (a *Auth) LoginHandler() http.Handler
- func (a *Auth) LoginPath() string
- func (a *Auth) LogoutHandler() http.Handler
- func (a *Auth) LogoutPath() string
- func (a *Auth) Mount(mux *http.ServeMux)
- func (a *Auth) RequireAuth(next http.Handler) http.Handler
- func (a *Auth) User(r *http.Request) (u User, ok bool)
- type Config
- type Option
- func ForceApprovalIfNewUser(known func(sub string) bool) Option
- func WithCookieName(name string) Option
- func WithExtraClaims(names ...string) Option
- func WithForceConsentParams(params map[string]string) Option
- func WithLoginPath(p string) Option
- func WithLogoutPath(p string) Option
- func WithPostLogoutRedirect(p string) Option
- func WithSessionTTL(d time.Duration) Option
- type User
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Auth ¶
type Auth struct {
// contains filtered or unexported fields
}
Auth performs the OIDC authorization-code flow and manages the app session cookie. Construct with New or NewFromEnv; mount its handlers with Auth.Mount or individually.
func New ¶
New validates cfg, performs OIDC discovery against cfg.Issuer, and returns an Auth ready to serve. The context governs discovery and is retained for background JWKS refreshes.
func NewFromEnv ¶
func (*Auth) CallbackHandler ¶
CallbackHandler completes the flow: it checks state against the signed cookie, exchanges the code (with the PKCE verifier), verifies the ID token (issuer, audience, expiry, signature) and its nonce, then sets the app session cookie and redirects to the `next` path captured at login.
func (*Auth) CallbackPath ¶
CallbackPath returns the path component of the redirect URL, where Auth.CallbackHandler must be mounted.
func (*Auth) ClearSession ¶
func (a *Auth) ClearSession(w http.ResponseWriter)
ClearSession removes the app session cookie without redirecting. Use it inside app handlers that end the session as part of their own flow — e.g. account deletion — where redirecting the in-flight POST to the POST-only logout endpoint is not possible. It only ends the app's session, exactly like LogoutHandler.
func (*Auth) LoginHandler ¶
LoginHandler starts the authorization-code flow: it generates state, nonce, and a PKCE (S256) verifier, binds them to the browser via a signed short-lived cookie, and redirects to the issuer.
Query parameters:
- next: relative path to return to after login (default "/").
- consent_restart: "1" marks this login as the one-time forced-consent restart that ForceApprovalIfNewUser triggers from the callback: the auth URL gains the force-consent parameters, and the flag is carried in the signed state cookie so the callback completes instead of restarting again. Anyone can forge a link with this parameter, but harmlessly so — the worst outcome is an unnecessary consent screen.
func (*Auth) LoginPath ¶
LoginPath returns where Auth.LoginHandler expects to be mounted.
func (*Auth) LogoutHandler ¶
LogoutHandler clears the app session cookie and redirects. It only ends the app's session: the issuer's own session and any upstream (e.g. Google) consent are unaffected.
Logout is POST-only, for two reasons. CSRF: with SameSite=Lax, cross-site subresources (<img>, script) never carry the session cookie, but top-level GET navigation does — so a GET logout is forceable by any cross-site link, while Lax withholds cookies from cross-site POSTs, making POST + Lax a complete defense with no CSRF token. Accidents: link prefetchers, browser prerendering, and email scanners issue GETs and would randomly end sessions; nothing prefetches a form. Log out from a form (or fetch) that issues a POST.
func (*Auth) LogoutPath ¶
LogoutPath returns where Auth.LogoutHandler expects to be mounted.
func (*Auth) Mount ¶
Mount registers the login, callback, and logout handlers on mux at their configured paths.
func (*Auth) RequireAuth ¶
RequireAuth wraps next so it only runs with a valid app session. The verified User is placed in the request context for UserFromContext. Unauthenticated GET/HEAD requests are redirected to the login handler with `next` set to the requested path; other methods get 401.
func (*Auth) User ¶
User returns the request's session user, if any. Use it on public pages that adapt to login state without requiring it (e.g. showing login vs logout links). Handlers behind Auth.RequireAuth should prefer UserFromContext.
type Config ¶
type Config struct {
// Issuer is the OIDC issuer URL, e.g. "https://auth.example.com".
// Discovery and JWKS URLs derive from it.
Issuer string
// ClientID is the OAuth2 client id; it is also the expected `aud`
// of every ID token this Auth accepts.
ClientID string
// ClientSecret is the OAuth2 client secret.
ClientSecret string
// RedirectURL is the app's absolute OAuth callback URL, e.g.
// "https://app.example.com/auth/callback". Its path is where
// [Auth.CallbackHandler] must be mounted ([Auth.Mount] does this).
// A "http://" scheme (local dev) disables the cookie Secure flag.
RedirectURL string
// CookieSecret is the HMAC key for state and session cookies.
// It must be at least 32 bytes, e.g. from `openssl rand -hex 32`.
CookieSecret string
}
Config holds the settings required to construct an Auth. Use FromEnv to populate it from AUTH_* environment variables.
type Option ¶
Option customizes an Auth.
func ForceApprovalIfNewUser ¶
ForceApprovalIfNewUser makes the callback restart the auth request with a forced consent prompt when known(sub) reports an unfamiliar user, so each user sees an explicit consent screen exactly once per app. known must be fast and non-blocking (e.g. a map or indexed DB lookup). The restart happens at most once per login attempt. The parameters sent on the restart are issuer-neutral by default; see WithForceConsentParams.
func WithCookieName ¶
WithCookieName sets the session cookie name (default "_oidcauth"). The state cookie is named "<name>_state".
func WithExtraClaims ¶
WithExtraClaims names additional ID-token claims to carry into the session, exposed raw on User.Extra. Use it when the issuer emits claims beyond the standard profile set that apps need at login — e.g. upstream-identity claims a broker adds for account-migration durability. Claims absent from a token are simply omitted. Keep the list small: the session rides in a cookie.
func WithForceConsentParams ¶
WithForceConsentParams replaces the extra authorization-request parameters sent when ForceApprovalIfNewUser triggers a consent restart. The default sends both the standard OIDC `prompt=consent` and the pre-OIDC `approval_prompt=force`: conformant issuers must ignore parameters they don't recognize (RFC 6749 §3.1), so each issuer honors the one it knows — no issuer-specific configuration needed. Override only for an issuer that rejects the combination (Google, used directly, errors on conflicting prompt parameters):
oidcauth.WithForceConsentParams(map[string]string{"prompt": "consent"})
func WithLoginPath ¶
WithLoginPath sets where Auth.LoginHandler is mounted (default "/auth/login"). Auth.RequireAuth redirects unauthenticated GET requests here.
func WithLogoutPath ¶
WithLogoutPath sets where Auth.LogoutHandler is mounted (default "/auth/logout").
func WithPostLogoutRedirect ¶
WithPostLogoutRedirect sets where Auth.LogoutHandler redirects after clearing the session (default "/").
func WithSessionTTL ¶
WithSessionTTL sets the app session lifetime (default 24h).
type User ¶
type User struct {
// Issuer is the verified `iss` of the ID token. Together with Sub
// it forms the account key: sub is unique only within an issuer.
Issuer string `json:"iss"`
// Sub is the issuer-unique, never-reassigned user identifier.
// (Issuer, Sub) is the ONLY pair apps may key accounts on.
Sub string `json:"sub"`
// Email is for display and recovery only — never an account key,
// and never a basis for automatically linking accounts.
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Name string `json:"name"`
// Extra holds the raw JSON of claims requested via
// [WithExtraClaims], keyed by claim name; absent claims have no
// entry. Unmarshal into app types as needed.
Extra map[string]json.RawMessage `json:"extra,omitempty"`
}
User holds the verified identity claims stored in the app session.
func UserFromContext ¶
UserFromContext returns the User stored by Auth.RequireAuth. ok is false outside a RequireAuth-wrapped handler.