ADR-0004: Authentication — explicit dev identity mode, Entra OIDC seam
Status: Accepted · 2026-07-14Decision
- 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 carriesx-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 501not_configured(never a silent fallback to dev), and 404s whenauthMode !== "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.
- Dev identity mode (
- Production lockout (implemented + tested): if
FMD_ENV=production, the server refuses to boot withFMD_AUTH_MODE=devand with the defaultFMD_SESSION_SECRET(hardConfigErrorat 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
principalsrow is keyed to an identity provider by an immutable(tenant_id, issuer, subject)tuple — never email/display name. This exists today only as theuq_principals_issuer_subjectunique index (migrations/0001_identity_governance.sql); dev-mode principals useexternal_idand are not populated viaissuer/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.mdandSTATUS.md). - Session mechanics (implemented): a signed, HTTP-only,
SameSite=Laxsession cookie (fmd_session,securein production), a per-session CSRF token required via thex-fmd-csrfheader on all mutating routes, and server-side session revocation (identity/sessions.ts,kernel/http.ts).