Skip to main content

ADR-0004: Authentication — explicit dev identity mode, Entra OIDC seam

Status: Accepted · 2026-07-14

Decision

  • Two authentication modes selected by FMD_AUTH_MODE, sharing one session layer and one downstream authorization stack:
    • Dev identity mode (FMD_AUTH_MODE=dev) — the implemented and exercised path in this build. Sign-in is a persona picker over seeded fixture principals (POST /api/auth/dev/login, GET /api/auth/dev/personas). Every API response carries x-fmd-auth-mode: dev; the UI shows a persistent, non-dismissable “Development identities — not for production” banner. All feature tests run through this path.
    • Entra OIDC (FMD_AUTH_MODE=entra) — the intended production path, present only as a seam in this build. The seam is a single endpoint, GET /api/auth/entra/login, which returns an explicit 501 not_configured (never a silent fallback to dev), and 404s when authMode !== "entra". There is no OIDC code-flow implementation, token/JWT validation, or issuer/ audience-verification code yet — that is deliberately deferred work (packages/server/src/identity/auth-routes.ts). The standards-based flow (authorization code, issuer/audience/tenant validation, nonce/state, short-lived sessions) is the design contract to implement against this seam.
  • Production lockout (implemented + tested): if FMD_ENV=production, the server refuses to boot with FMD_AUTH_MODE=dev and with the default FMD_SESSION_SECRET (hard ConfigError at startup — kernel/config.ts), and dev-mode session cookies are rejected at request time even if present (kernel/http.ts).
  • The data model reserves the external-identity mapping for the future OIDC path: a principals row is keyed to an identity provider by an immutable (tenant_id, issuer, subject) tuple — never email/display name. This exists today only as the uq_principals_issuer_subject unique index (migrations/0001_identity_governance.sql); dev-mode principals use external_id and are not populated via issuer/subject.

Consequences

  • All authorization logic downstream is identical regardless of how the session was established; the authz stack is fully exercised through dev identities without weakening the production path — but the production sign-in path itself is not yet built and must be implemented on the 501 seam before any production deployment (tracked in docs/roadmap.md and STATUS.md).
  • Session mechanics (implemented): a signed, HTTP-only, SameSite=Lax session cookie (fmd_session, secure in production), a per-session CSRF token required via the x-fmd-csrf header on all mutating routes, and server-side session revocation (identity/sessions.ts, kernel/http.ts).