- Rotate the session token at the OIDC callback and restore the full lifetime; cap pre-auth sessions at 15 minutes and write no session for bare anonymous requests - Treat db-dsn as a secret: accept db-dsn-file, log only host, port, database and user, and never echo a malformed DSN in an error - Guard the logout callback with a state cookie so a forged visit cannot end a live session - Collapse FedWiki site actions on a foreign tenant's domain to the not-found answer, as for a domain that does not exist
Authentication (internal/auth)
This package implements an OAuth 2.0 Authorization Code Grant with PKCE flow, enhanced by OpenID Connect (OIDC) for user identity.
Purpose: Document the implementation details, configuration variables, endpoints, and security considerations for the OIDC authentication flow implemented by the
internal/authpackage.
Overview
The internal/auth package uses the Authorization Code grant combined with PKCE (Proof Key for Code Exchange) to authenticate users against an OpenID Connect (OIDC) identity provider (IdP). This flow is secure and recommended for both public and confidential clients as it prevents authorization code interception attacks.
The codebase integrates with Keycloak (or another OIDC-compatible IdP) and performs the following responsibilities:
- Initiate authorization using PKCE and OIDC
nonceandstateparameters - Exchange authorization code for a token using the code verifier
- Verify the ID token (signature, nonce, issuer, audience)
- Provision or update a local user record in the database
- Manage a session using secure cookies
- Initiate and verify single sign-out via the IdP
Grant Type
- OAuth 2.0: Authorization Code Grant
- PKCE extension: RFC 7636 (SHA-256-based
S256method) - OIDC: Identity layer using
openIDscope + ID token verification
Key Implementation Details
PKCE Flow
-
A random
code_verifieris generated for each login attempt:codeVerifier := generateRandomString(32) -
The
code_challengeis computed by taking the SHA-256 hash of the code verifier and encoding it via base64url without padding:hashVal := sha256.Sum256([]byte(codeVerifier)) codeChallenge := base64.RawURLEncoding.EncodeToString(hashVal[:]) -
The
code_challengeandcode_challenge_method=S256are added to the initial authorization URL. -
When exchanging the authorization code for tokens, the previously generated
code_verifieris supplied usingoauth2.VerifierOption(codeVerifier).
OIDC and Security Parameters
state: A CSRF token is generated and validated to prevent CSRF attacks.nonce: A token to prevent replay attacks. The nonce is validated against thenonceclaim in the ID token.- ID Token (
id_token) verification is performed using the OIDC provider's public keys via theVerifier. - If the ID token is missing or invalid, the flow is rejected.
Endpoints
The internal/auth RegisterHandlers method hooks the following HTTP routes on the *http.ServeMux:
GET /login—LoginHandler: Initiate authorization (PKCE + OIDC).GET /callback—CallbackHandler: Handle authorization code, exchange tokens, verify ID token, provision user and create session.POST /logout—LogoutHandler: Destroys the session, revokes the refresh token at the IdP, then sends the browser to the IdP's end-session endpoint with a validid_token_hintwhere one can be had:HX-Redirectfor the account menu's htmx request, a 303 for any other POST. A GET at/logoutis a 303 to/and ends nothing, so a link on another origin cannot sign a person out.GET /logout-callback—LogoutCallbackHandler: Where the IdP sends the browser after its own sign-out; redirects to/login. Nothing is verified here because nothing is left to protect.GET /register—RegistrationHandler: Redirects to the IdP registration path (or the auth endpoint with registration params) usingcode_challengeandnoncesimilarly to/login.
Configuration & Environment Variables
The auth package depends on the application configuration (via viper) to initialize.
Required config keys (from viper):
oidc-idp-issuer-url— Identity Provider issuer URL (e.g.,https://idp.example.com/realms/realm)oidc-sp-client-id— OAuth client ID (service provider client identifier)oidc-sp-client-secret— OAuth client secret (if using a confidential client; optional for public clients)base-url— Application base redirect URL (e.g.,https://app.example.com)valkey-addr— Valkey/Redis address for the server-side session store (defaultlocalhost:6379)env—productionor other (controls the cookieSecureflag)
Session & Cookie Management
- Session store:
scs(alexedwards/scs) with a Valkey/Redis backend (redisstore) — sessions are server-side, so no signing/encryption secret is needed - Cookie options:
HttpOnly: true— prevents JS access to cookieSecure: trueonly whenenvisproductionSameSite: Lax— helps prevent CSRF while allowing top-level navigationMaxAge: 7 daysby default
- The session stores these keys:
state,nonce,code_verifierduring the authentication initiationauthenticated,id_token,refresh_token,person_id,org_id,workspace_id,oidc_subject,email,name,username,roles,identity_refreshed_atafter authentication;identity_retry_atwhile the provider is not answering
DB Integration
-
The code uses the
db.Querierinterface (SQLC-generated queries) to get, create, or update users:GetUserByOIDCSubject(ctx, subject)— find existing user bysubclaimCreateUser(ctx, CreateUserParams)— create new user when not foundUpdateUser(ctx, UpdateUserParams)— update user info on subsequent logins
-
When a new user logs in for the first time, the implementation creates a new database user record with relevant OIDC claims:
OidcSubject— OIDCsubclaimUsername—preferred_usernameEmail—email
Identity Re-derivation
- Every five minutes, on the first authenticated request after the interval, the middleware re-derives the session's identity and roles with a refresh-token grant: the new ID token is verified as at sign-in, its roles replace the session's, and the new ID token and refresh token replace the stored ones.
invalid_grant(session ended at the IdP, account disabled, token revoked) ends the console session and bounces to/login. No answer keeps the session and retries after a minute. A token naming a different subject ends the session.- Concurrent requests crossing the interval share one refresh (
singleflight, keyed by session token).
Logout Flow
- Sign-out is a POST, made by the account menu's button; the cross-origin protection covers it and a GET at
/logoutonly redirects to/.LogoutHandlerdestroys the console session first, then sends the browser to the IdP'send_session_endpoint, read from discovery at startup (HX-Redirectto the htmx request, since fetch cannot follow a redirect to another origin). A provider that publishes none fails startup; there is no fallback path. - The request carries
id_token_hintwhenever a valid ID token can be had: the stored one while it is still valid, otherwise a fresh one minted from the stored refresh token in one bounded call to the token endpoint. A valid hint is what lets the IdP end its session without asking the person to confirm. When no hint can be had the IdP asks; the console session is gone either way. - Between the two, every refresh token that was live (the stored one, and a rotated one when the refresh returned it) is revoked at the IdP's
revocation_endpoint(RFC 7009) when discovery publishes one, so no copy can mint tokens after sign-out. Best effort: a failure is logged and the sign-out proceeds. LogoutCallbackHandlerdestroys the session once more (expiring a cookie the browser may still carry) and redirects to/login.
Quick Start: Configuration Example
Example viper/env config for a development environment:
oidc-idp-issuer-url:https://idp.example.com/realms/demooidc-sp-client-id:member-consoleoidc-sp-client-secret:<client-secret>(optional for public clients)base-url:http://localhost:8080valkey-addr:localhost:6379env:development
Once configured, the app can be started and these endpoints will be active on the configured host.
References
- OAuth 2.0 Authorization Framework (RFC 6749)
- Proof Key for Code Exchange (PKCE) (RFC 7636)
- OpenID Connect Core 1.0
Notes & Tips
- PKCE is mandatory for public clients (e.g., SPA, mobile) and recommended for confidential clients to protect against code interception attacks.
- The
nonceis stored in session and validated against the ID tokennonceclaim as an additional replay protection. - The logout and revocation endpoints are discovered (
end_session_endpoint,revocation_endpoint). The registration path is still Keycloak's; another IdP may need a different route for registration. - When deploying to production, ensure
base-urlreflects your secure domain andvalkey-addrpoints at your session store.
Related code files
internal/auth/auth.go— implementation of the handlers and flowinternal/auth/— this README and other auth related filesinternal/db/*— database schema & SQLC queries (GetUserByOIDCSubject,CreateUser,UpdateUser)