Authentication
Users sign in via generic OIDC SSO — Keycloak, Entra ID, Okta, Google, GitHub, or anything speaking discovery + PKCE. Users never need cluster credentials; the app's ServiceAccount executes the K8s calls.
How login works
Browser ──GET /api/auth/login──▶ Krypton ──discovery──▶ OIDC issuer
│ │ PKCE + nonce + state cookies
│◀──────── redirect to IdP ────────┘
│── user authenticates ──▶ IdP ──code──▶ /api/auth/callback
│ code exchange, claim mapping
▼
krypton_session cookie (HS256 JWT, 8h)
GET /api/auth/loginbuilds the authorization URL (PKCES256, nonce, state) and redirects to the IdP.GET /api/auth/callbackrequires all three transaction cookies, consumes them even on failure, and validates state, nonce, and PKCE during the code exchange. It reads ID-token claims and mints a stateless session JWT in anHttpOnly,Secure-in-production,SameSite=Laxcookie.GET /api/auth/mereturns the currentSessionUser(sub,email,name,groups) or401with{ user: null }.GET /api/auth/logoutclears the session cookie.
See the Auth API for exact shapes.
An incomplete or failed transaction requires a new login. The IdP enforces
single-use authorization codes; Krypton keeps no server-side transaction store.
When UserInfo is available, its sub must match the validated ID-token sub
before any claims are merged. A subject mismatch rejects login. Providers with
no UserInfo endpoint, or unavailable optional UserInfo, can still authenticate
using their ID-token claims alone.
Signing key and upgrade behavior
With auth enabled, SESSION_SECRET must be an explicit random key of at least
32 characters (generate one with openssl rand -hex 32). Missing, empty, short,
whitespace-only, and the former public development fallback keys fail closed:
login cannot create a session and cookies cannot authenticate. Validation errors
name the setting, never its value. Use the same key on every replica.
On upgrade, replace the old development fallback in copied examples or deployment
configuration before enabling auth. Changing the key invalidates existing
sessions; users sign in again. Key validation happens when auth is used, allowing
builds without runtime secrets and explicit AUTH_DISABLED=true local bypass
without a JWT.
Only HS256 session JWTs with issuer krypton, nonempty string sub and email,
numeric iat/exp, and an unexpired lifetime are accepted. Future-issued sessions
and malformed name/group claims are rejected. Omitted names and groups keep the
email/empty-list defaults.
Claim mapping
| Variable | Default | Maps to |
|---|---|---|
OIDC_GROUPS_CLAIM | groups | SessionUser.groups (authz input) |
OIDC_USERNAME_CLAIM | email | SessionUser.email (identity) |
OIDC_NAME_CLAIM | name | SessionUser.name (display) |
OIDC_SCOPES | openid profile email groups | Requested scopes |
email is the identity: admin matching, allowed-users, and
last-modified-by all key off it (lowercased). groups is the authorization
input for allowed-groups, namespace rules, and admin groups.
Groups use the first nonempty string-array claim from the configured
OIDC_GROUPS_CLAIM path, groups, roles, then nested realm_access.roles
(Keycloak). Dotted configured paths are supported; empty or unusable candidates
fall through to the next one. Non-string group entries are ignored when mapping
provider claims.
Issuer rules
OIDC_ISSUERis the issuer identifier, not the discovery URL — the app appends/.well-known/openid-configurationitself and requires an exact match with the document'sissuerfield.- Redirect URI defaults to
<NEXT_PUBLIC_APP_URL>/api/auth/callback(override withOIDC_REDIRECT_URI); register the same value at your IdP. Behind a TLS-terminating proxy, the code exchange uses that registered public URI rather than the internal request host, preserving the callback parameters. Use a redirect URI without its own query string or fragment. - OIDC configuration is cached per process — restart the app after Dex/IdP restarts or issuer changes.
http://issuers fail closed: loopback hostnames only, explicitOIDC_ALLOW_HTTP=trueonly, and rejected entirely whenNODE_ENV=production.
Dev bypass and headless sessions
AUTH_DISABLED=truereturns a mock admin (dev@krypton.local) for every request; login redirects to/without minting a session token or requiring a signing key. Local development only — assertauthDisabled: falseviaGET /api/configin real environments.sandbox/scripts/mint-jwt.mjsmints session JWTs directly (must matchSESSION_SECRET) for headless authz checks:
ALICE=$(node sandbox/scripts/mint-jwt.mjs --email alice@company.com --groups dev-team)
curl --cookie "krypton_session=$ALICE" localhost:3000/api/auth/me