ADR-0036 — Authentication policy: OAuth 2.1, MFA/passkeys, step-up, token lifecycle
- Status: Proposed
- Date: 2026-06-11
- Related: security/03 authentication, ADR-0010
V1 Freeze (2026-06-12): Proposed. Basic authn (OAuth 2.1 + argon2id passwords + API tokens) is V1 via ADR-0010; MFA/passkeys/step-up/DPoP hardening is agreed but not committed to V1.
Context
Authentication is OWASP API2 and the entry point to everything; weak token handling and missing MFA are leading breach causes. We need a concrete, modern policy for humans and the tokens that carry identity — not just “we use OIDC.”
Decision
- OAuth 2.1 baseline: Authorization Code + PKCE mandatory for all clients; no implicit, no ROPC grants; exact redirect-URI matching.
- MFA required for humans; passkeys (WebAuthn) preferred (phishing-resistant), TOTP fallback. Step-up auth for sensitive operations (key ops, external mass-share, admin/role changes, billing).
- Tokens (RFC 9700): short-lived access tokens (5–15 min sensitive, 30–60 min
general); refresh-token rotation with reuse detection (reuse → invalidate the entire
family + force re-auth); DPoP/mTLS sender-constraining for high-assurance; strict JWT
validation (
exp/nbf/aud/iss, signature, rejectalg:none); JWKS key rotation. - Sessions:
HttpOnly+Secure+SameSitecookies; idle and absolute timeout; rotate on privilege change. Tenant context derives from the verified token and is re-checked on every authz decision (ADR-0007). - Passwords (fallback/self-host): argon2id + breach-password check.
Consequences
Positive
- Closes code-interception (PKCE), token-replay (rotation + DPoP), and phishing (passkeys); step-up decouples “logged in” from “allowed to do something dangerous”.
- Short TTLs + reuse detection bound the value of a stolen token.
Negative / costs
- DPoP/passkeys add client complexity; step-up adds UX friction on sensitive ops (a deliberate, scoped trade).
- Refresh-token families + reuse detection require careful server state.
Alternatives considered
- OAuth 2.0 with implicit/ROPC: deprecated, insecure (token in URL, password grant). Rejected (OAuth 2.1).
- Long-lived access tokens, no rotation: simpler but a stolen token is long-lived gold. Rejected.
- TOTP-only MFA: phishable; kept as fallback, passkeys preferred.
Scaling
Stateless access-token validation at the gateway (cached JWKS); refresh-family state in Redis/Postgres; step-up decisions cached briefly.