Files
member-console/internal/auth
cgalo5758 2d71aa8db0 Describe the boot that serves before its dependencies
Seventh section of #141, the docs. docs/production-deployment.md's first
boot section lists the steps in the order start runs them, says what
serves from the moment the listener opens, and describes the two loops
that run beside it: the identity provider's discovery, with sign-in at
503 until a usable document is published and roles not re-derived
before a document with the configured issuer is read, and Temporal,
retried for the life of the process with its log turning to Error after
temporal-connect-timeout. The service list says that Stripe and
Discourse need Temporal. README.md and docs/plan-management.md lose
their description of a bounded retry before the server comes up.

docs/environment-reference.md rewrites the temporal-connect-timeout and
temporal-host rows; docs/stripe.md and docs/discourse-setup.md say why
temporal-host is required; docs/building-an-integration.md documents
NeedsTemporal. docs/fedwiki-setup.md says the console answers the
certificate ask as soon as it serves. docs/identity-provider-setup.md
and internal/auth/README.md replace "refuses to start" with what the
console does, and docs/temporal-authorization-setup.md explains the
token URL. Comments in the FedWiki and Stripe integrations and the
FedWiki workflows README that still described the old boot are
corrected. Every quoted log line is the one the code writes.

No code changes. make test passes with the test stack up, and go test
-race -p 1 over cmd, auth, server, retryloop, workflows and the three
integrations after reset-test-db.sh finds no data race.
2026-10-23 21:07:48 -04:00
..

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/auth package.


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 nonce and state parameters
  • 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 S256 method)
  • OIDC: Identity layer using openID scope + ID token verification

Key Implementation Details

PKCE Flow

  • A random code_verifier is generated for each login attempt: codeVerifier := generateRandomString(), which draws randomStringBytes (32 octets) and base64url-encodes them to the 43 characters RFC 7636 section 4.1 recommends

  • The code_challenge is 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_challenge and code_challenge_method=S256 are added to the initial authorization URL.

  • When exchanging the authorization code for tokens, the previously generated code_verifier is supplied using oauth2.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 the nonce claim in the ID token.
  • ID Token (id_token) verification is performed using the OIDC provider's public keys via the Verifier.
  • 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 valid id_token_hint where one can be had: HX-Redirect for the account menu's htmx request, a 303 for any other POST. A GET at /logout is 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) using code_challenge and nonce similarly 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 (default localhost:6379)
  • env — production or other (controls the cookie Secure flag)

  • 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 cookie
    • Secure: true only when env is production
    • SameSite: Lax — helps prevent CSRF while allowing top-level navigation
    • MaxAge: 7 days by default
  • The session stores these keys:
    • state, nonce, code_verifier during the authentication initiation
    • authenticated, id_token, refresh_token, person_id, org_id, workspace_id, oidc_subject, email, name, username, roles, identity_refreshed_at after authentication; identity_retry_at while the provider is not answering

Sign-in resolution

The callback hands the verified claims to provisioning.ResolveSignIn, which reads the user by the sub claim. A subject no user carries is a first sign-in: provisioning.AutoProvision creates the user, the person, the personal organization and its workspace in one transaction. A known subject is a returning sign-in: the login time and remote address are recorded, the person's name and email are resynced from the claims, and the session is bound to the first organization the person owns and that organization's first workspace, each by name (#122).


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. No session is re-derived before the background loop has read a discovery document with the configured issuer; until then the middleware keeps the session's identity and roles.
  • 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 /logout only redirects to /. LogoutHandler destroys the console session first, then sends the browser to the IdP's end_session_endpoint, read from the discovery document the background loop fetches (HX-Redirect to the htmx request, since fetch cannot follow a redirect to another origin). A provider that publishes none leaves sign-in unavailable, and the console logs at Error until the provider publishes one; the console still serves. Before the endpoint is known, POST /logout ends the console session, revokes the stored refresh token when the verifier is published and the document carries a revocation endpoint, mints no id_token_hint, and lands on /. The provider's own session stays.
  • The request carries id_token_hint whenever 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.
  • LogoutCallbackHandler destroys 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/demo
  • oidc-sp-client-id: member-console
  • oidc-sp-client-secret: <client-secret> (optional for public clients)
  • base-url: http://localhost:8080
  • valkey-addr: localhost:6379
  • env: 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 nonce is stored in session and validated against the ID token nonce claim 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-url reflects your secure domain and valkey-addr points at your session store.

  • internal/auth/auth.go — implementation of the handlers and flow
  • internal/auth/ — this README and other auth related files
  • internal/db/* — database schema & SQLC queries (GetUserByOIDCSubject, CreateUser, UpdateUser)