Authentication
How the Chompardo App authenticates users via the Identity service using OIDC, and how sessions are managed in the BFF.
Overview
The Identity service (main-identity.chompardo.dev) is the platform's OIDC provider, powered by Better Auth with its OAuth Provider plugin. It supports multiple authentication methods (social providers like Google, email/password, and more in future) and issues tokens to relying-party applications.
The App frontend (main.chompardo.dev) acts as a Backend for Frontend (BFF) — it never exposes tokens to the browser. Instead, its server side handles the full OAuth 2.1 Authorization Code flow with PKCE against Identity, stores tokens server-side, and gives the browser only an HttpOnly session cookie.
This approach follows the architecture established in ADR-003: BFF Architecture.
Two Login Paths
There are two ways a user can end up authenticated at the App:
1. OIDC Flow (from the App)
The user visits main.chompardo.dev and is not authenticated. The App's middleware starts the OIDC flow, which redirects to Identity. After the user signs in at Identity, the OIDC flow completes and the user is redirected back to the App with tokens.
2. Direct Login (at Identity)
The user visits main-identity.chompardo.dev directly and signs in. After sign-in, Identity redirects to a configurable default URL (DEFAULT_REDIRECT_URL, defaults to https://main.chompardo.dev). The App's middleware detects no session and starts the OIDC flow — since Identity already has an active session, it auto-completes the authorize request without showing the login page again.
OIDC Login Flow
If the user already has an active session at Identity (e.g., from a direct login or previous OIDC flow), the authorize endpoint skips the login UI and immediately redirects back with the authorization code.
Direct Login Flow
Logout Flow
Logout clears the App's server-side session and ends the Identity session:
The browser is redirected to Identity's end-session endpoint with the id_token_hint, which ends the Identity session. If no id token or end-session endpoint is available, the browser is redirected to / (home) as a fallback.
Key Components
Middleware (middleware/auth.server.ts)
The root middleware runs on every request and is responsible for:
- Skipping auth routes —
/auth/login,/auth/callback, and/auth/logoutbypass authentication - Session validation — Reads the
__sidcookie, looks up the session in the server-side store - Proactive token refresh — If the access token expires within 60 seconds, the middleware refreshes it using the refresh token before the request proceeds
- Context propagation — Sets the authenticated user, session ID, and access token on the React Router context so loaders and actions can access them
If no valid session exists, the middleware redirects to /auth/login with a return_to parameter preserving the original URL.
Session Store (lib/session.server.ts)
Sessions are stored server-side in Valkey (Redis-compatible) — the browser only holds a signed session ID in an HttpOnly cookie. The implementation uses React Router's createSessionStorage with a Valkey-backed adapter (iovalkey).
Session data includes:
| Field | Description |
|---|---|
userId | The user's sub claim from Identity |
accessToken | OAuth access token (for Go API calls) |
refreshToken | OAuth refresh token |
idToken | OIDC ID token (used for logout) |
accessTokenExpiresAt | Timestamp for proactive refresh |
user | UserInfo object (email, name, picture) |
Flash data (auto-deleted after a single read, used during the OIDC login flow):
| Field | Description |
|---|---|
pkceVerifier | PKCE code verifier for the current flow |
pkceState | OAuth state parameter |
returnTo | URL to redirect to after login |
Session data is stored in Valkey under {env}:session:{id} keys (prefixed with ENV_NAME) with a configurable TTL (SESSION_TTL_SECONDS, default 30 days).
Cookie Security
The only cookie is __sid — an HttpOnly, Secure, SameSite=Lax session ID cookie signed with SESSION_SECRET. PKCE data (verifier, state, returnTo) is stored as flash data in the same session, automatically deleted after a single read.
| Cookie | Purpose | Lifetime | Signed |
|---|---|---|---|
__sid | Session ID | SESSION_TTL_SECONDS (default 30 days) | HMAC-SHA256 |
OIDC Client (lib/oidc.server.ts)
A thin wrapper around the openid-client library that caches the OIDC discovery configuration. The library handles:
- OIDC Discovery — Fetches and caches the Identity service's
.well-known/openid-configuration - PKCE generation —
SHA-256code challenge withbase64urlencoding - Token exchange — Authorization code → tokens via the
/tokenendpoint withclient_secret_basic - Token refresh — Refresh token → new tokens (same endpoint, different grant type)
- UserInfo — Fetches user profile from the
/userinfoendpoint
Config (lib/config.server.ts)
Environment variables are validated at startup using Zod. The server crashes immediately if any required variable is missing or invalid — fail fast, fail loud.
| Variable | Required | Default | Description |
|---|---|---|---|
OAUTH_CLIENT_ID | Yes | — | OAuth client ID from Identity service |
OAUTH_CLIENT_SECRET | Yes | — | OAuth client secret |
OAUTH_ISSUER | Yes | — | Identity service base URL |
APP_BASE_URL | Yes | — | App's own base URL |
SESSION_SECRET | Yes | — | HMAC signing key (min 32 chars) |
SESSION_TTL_SECONDS | No | 2592000 | Session lifetime in seconds (default 30 days) |
VALKEY_URL | Yes | — | Valkey/Redis connection URL |
Identity Configuration
The Identity service's login page uses a server-side DEFAULT_REDIRECT_URL (env var, defaults to https://main.chompardo.dev) to determine where users go after a direct login. This is passed to Better Auth's callbackURL for social sign-in and used for client-side redirect after email sign-in.
During OIDC flows, Better Auth's OAuth Provider plugin automatically resumes the authorize flow after login, overriding the default redirect.
API Token Validation
The Go API validates JWT access tokens independently — it never calls back to the Identity service or the BFF to check a token. This is possible because tokens are signed with asymmetric keys (EdDSA/Ed25519) and the API caches the public keys from the Identity service's JWKS endpoint.
The API validates:
- Signature — using the public key matching the token's
kidfrom the cached JWKS - Algorithm — only
EdDSAis accepted (Better Auth's default) - Issuer — must match
AUTH_ISSUER(the Identity service URL) - Audience — must match
AUTH_AUDIENCE(the API's own URL, configured viaOAUTH_VALID_AUDIENCESin Identity) - Expiration — expired tokens are rejected with 401
- Subject — the
subclaim must be present
If validation fails, the API returns 401 with a WWW-Authenticate: Bearer realm="api" header. The BFF's middleware handles token refresh proactively (see below), so under normal operation the API rarely sees expired tokens.
Token Refresh
Access tokens are short-lived. Rather than waiting for a 401 from the Go API, the middleware proactively refreshes tokens that will expire within 60 seconds:
- Middleware checks
accessTokenExpiresAt - 60_000 < Date.now() - If expiring soon, calls the Identity
/tokenendpoint withgrant_type=refresh_token - Updates the session in Valkey with new tokens and commits the updated cookie
If refresh fails (e.g., refresh token revoked), the session is destroyed and the user sees a "Session expired" error with a link to sign in again.
TLS in Local Development
Both the App and Identity make server-to-server HTTPS calls through Traefik (e.g., token exchange, JWKS fetch). Traefik uses mkcert-generated certificates, so Node.js needs to trust the mkcert root CA:
NODE_EXTRA_CA_CERTS=$(mkcert -CAROOT)/rootCA.pem
This is set in each app's .env.development and evaluated by dotenvx before the process starts. Run pnpm generate-certs from the repo root to generate the certs and trust the CA.
Without this, server-to-server calls fail with unable to get local issuer certificate.
File Map
| File | Responsibility |
|---|---|
app/lib/config.server.ts | Zod-validated environment config |
app/lib/session.server.ts | Session store + HMAC cookie helpers |
app/lib/oidc.server.ts | PKCE, token exchange, refresh, userinfo |
app/lib/context.ts | Typed React Router context definition |
app/middleware/auth.server.ts | Root middleware (session check, refresh, redirect) |
app/routes/auth.login.ts | Initiates OIDC flow (PKCE + redirect) |
app/routes/auth.callback.ts | Handles OIDC callback (code exchange, session creation) |
app/routes/auth.logout.ts | Destroys session, ends identity session, redirects to Identity |