Dashboard: Authentication

The console wraps its entire UI in a LoginGate component (pages/auth/LoginGate.tsx) that handles login before anything else renders. This page documents the actual browser-side login UX; see guides/security/authentication.md for the server-side [auth] config (mode = "local" | "oidc" | "disabled") that determines which of the paths below is live.

Flow: OIDC (Authorization Code + PKCE)

When auth.mode = "oidc", the console implements a standard browser-based OIDC login (api/auth.ts) — Dex, Microsoft Entra, Google Workspace, or any standards-compliant IdP:

  1. On load, the console checks for a stored token. If missing or expired, it shows a "Sign In" card and redirects to the IdP on click.
  2. The user authenticates at the IdP.
  3. The IdP redirects back to the console with ?code=....
  4. The console exchanges the code for tokens using PKCE (no client secret ever touches the browser) — the code verifier and CSRF state are round-tripped through sessionStorage.
  5. The resulting id_token is stored in sessionStorage (not localStorage — it does not survive a browser restart) and attached as a Bearer token on every subsequent API call.

The server never sees login credentials directly — it only validates the JWT the IdP issued.

LoginGate states

The gate is a small state machine (loadingdisabled | unauthenticated | authenticated):

StateMeaningWhat renders
loadingChecking for a ?code= callback, then fetching OIDC configA centered "Loading..." card
disabledServer reports auth.mode is not OIDC-backedThe app renders directly — dev-mode/no-auth bypass
unauthenticatedOIDC enabled, no valid tokenA "Sign in with your identity provider" card with a Sign In button
authenticatedValid, unexpired token present (or a callback just succeeded)The app renders normally

A stored token that has expired is cleared automatically before falling back to unauthenticated — the user sees a fresh login prompt rather than an error about the session being expired for its own sake.

Dev-mode bypass

If the server's OIDC config reports enabled: false (i.e. auth.mode = "local" or "disabled"), the gate renders the app directly with no login step at all. This is by design for local/auth.mode = "local" deployments — those use the same API-key infrastructure as the rest of the platform (POST /auth/keys) rather than a browser login flow; the console doesn't currently have its own API-key entry UI, so a local-mode deployment's console access is effectively unauthenticated unless fronted by something else.

What's not here

There is no role-aware UI gating in the console today — see security-overview.md for what that means in practice (the identity/role is displayed, not enforced client-side).


Related: