ADR-024 — Authentication & MFA strategy
1. Context
We have partial MFA work in flight (existing issues — must not duplicate):
noxys-api#92— MFA/TOTP implementation (open)noxys-console#69— MFA TOTP + backup codes + session management (open)noxys-api#336— WebAuthn / Passwordless FIDO2 (open)noxys-console#119— UX Wave 1 Settings MFA (closed)
These exist but lack a unifying architecture spec covering:
- Encryption of TOTP secrets + backup codes (must use per-tenant KMS per ADR-019).
- Recovery flow detail when device + backup codes both lost.
- Session policy (MFA challenge TTL, remember-device 30d, re-auth on sensitive actions).
- Tenant-wide MFA enforcement policy (admin requires all users have MFA).
- Tenant API token reveal in admin Settings (separate from user MFA but same auth surface).
This ADR consolidates the architectural decisions, amends acceptance criteria of existing issues by reference, and files only the NEW sprints not covered: tenant token reveal + tenant-wide MFA enforcement.
2. Decision summary
- Auth methods supported: password+TOTP (#92), WebAuthn passkeys (#336 — preferred), backup codes (#69).
- Method priority: WebAuthn > TOTP > backup code (recovery only). Push WebAuthn as default in onboarding once #336 lands.
- TOTP secrets + backup codes encryption: per-tenant KMS key per ADR-019. Stored in
users.mfa_totp_secret_encrypted+mfa_backup_codes_encryptedjsonb arrays. - Backup codes: 10 codes, 8 chars alphanumeric, single-use, regenerated on demand (invalidates old set).
- Session policy after MFA challenge:
- JWT carries
mfa_verified_atclaim. - Default session TTL after MFA: 12h.
- "Remember this device 30d" opt-in: stores device fingerprint cookie + bypass MFA challenge for 30d on that device only (admin-disable-able tenant-wide).
- Sensitive actions (rotate API token, change role, delete user, break-glass raw access) require fresh MFA challenge regardless of session TTL.
- JWT carries
- Recovery flow: lost device + lost backup codes = tenant admin reset only. No email-link reset (phishing risk). Tenant admin role marks the user as
mfa_reset_pending, user re-enrolls on next login. If the tenant admin themselves is locked out, contact Noxys support → 2-person break-glass procedure (per ADR-019 access control pattern). - Tenant-wide MFA enforcement: tenant admin can require MFA for all users (with N-day grace for existing users to enroll). Free tier locked out of this feature (sales differentiator).
- Tenant API token reveal: admin-only (
tenant_adminornoxys_staff) eye toggle in Settings → API; reveal logged to audit immutably; auto-hide 30s; multi-token support with scope/expiry/label per token.
3. Cross-reference: existing issues + amendments
noxys-api#92 — MFA/TOTP implementation
Augment with:
- TOTP secret stored as
mfa_totp_secret_encrypted(bytea) wrapped via tenant KMS DEK (per ADR-019). - Algorithm: RFC 6238 TOTP, SHA-1, 6 digits, 30s window, ±1 step skew tolerance.
- QR code generation server-side (otpauth URI), one-time display only.
- MFA challenge endpoint at
/auth/mfa/verifyissues short-lived (12h) JWT withmfa_verified_at. - Sensitive actions middleware checks
mfa_verified_at <= 5minfor re-auth-required routes.
noxys-console#69 — MFA TOTP + backup codes + sessions
Augment with:
- 10 backup codes generated at TOTP enrollment, single-use, sha256-hashed-then-encrypted (so we never store plaintext).
- "Active sessions" view in Account → Security: list of devices with last-seen + IP + location + revoke button.
- "Trusted devices" subview: 30d remember-device list with revoke.
- "Regenerate backup codes" action requires fresh MFA challenge.
noxys-api#336 — WebAuthn / Passwordless FIDO2
Augment with:
- WebAuthn = preferred enrollment path. Onboarding wizard offers WebAuthn first; TOTP labeled as "alternative for unsupported devices".
- Multiple credentials per user (laptop + phone + hardware key).
- User-friendly credential names (admin can rename: "MacBook M3" instead of UA fingerprint).
- Passkey roaming (iCloud Keychain / 1Password / etc.) supported by default.
- Hardware key path uses platform=cross-platform attestation.
4. Tenant API token reveal — full spec (NEW)
UI
Settings → Tenant → API & Tokens section.
Visible to: tenant_admin + noxys_staff only. Other roles: section hidden (not just disabled — completely absent from rendered DOM).
For each token row:
- Label (admin-set: "Production CI", "Slack bot", "Datadog integration", etc.)
- Created date
- Last-used (relative time)
- Scope (multi-select: read / write / admin)
- Expiry (optional, default 1y)
- Status (active / revoked / expired)
- Eye toggle 👁 — masked default
tk_live_••••••••XXXX, click reveals full - Copy button (always available, copies full even when masked)
- Rotate button (issues new token, old valid 1h grace then revoked)
- Revoke button (immediate, with confirmation)
Reveal behavior:
- 30s auto-hide countdown shown next to revealed token
- Reveal generates audit event:
{
"event_type": "tenant_token_revealed",
"actor": "user-uuid",
"actor_email": "...",
"ip": "...",
"user_agent": "...",
"token_id": "tok-uuid",
"occurred_at": "..."
} - Multiple reveals allowed but rate-limited (10/hour/user, then soft-cap with admin alert).
Multi-token model
CREATE TABLE tenant_api_tokens (
id uuid PRIMARY KEY,
tenant_id uuid NOT NULL REFERENCES tenants(id),
label text NOT NULL,
token_hash text NOT NULL, -- bcrypt of plaintext (verify on use)
token_prefix text NOT NULL, -- first 8 chars for display (tk_live_xxxxxxxx)
scopes text[] NOT NULL, -- {'read','write','admin'}
created_by uuid NOT NULL REFERENCES users(id),
created_at timestamptz NOT NULL DEFAULT now(),
last_used_at timestamptz,
expires_at timestamptz,
revoked_at timestamptz,
revoked_by uuid REFERENCES users(id)
);
CREATE INDEX idx_tokens_tenant_active ON tenant_api_tokens(tenant_id) WHERE revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now());
Plaintext token stored client-side once at creation (modal "Copy your token now — won't be shown again unless reveal action by admin"). Server stores bcrypt hash only.
Reveal mechanism: server stores token_encrypted (AES-GCM with per-tenant KMS key) IN ADDITION to token_hash (bcrypt). Reveal endpoint decrypts via tenant KMS DEK → returns plaintext + logs audit.
5. Tenant-wide MFA enforcement (NEW)
Settings → Security → "Require MFA for all users in this tenant" toggle.
Behavior:
- ON + grace period (default 14d): existing users see banner "MFA enrollment required by
"; new users prompted at first login. - After grace expires: user blocked from accessing console until enrolled. Extension/agent continue working (already authenticated via long-lived tokens).
- Admin override: per-user exemption with reason (logged in audit). Use case: service accounts.
- Free tier: this toggle is hidden (Pro+ feature, sales differentiator).
- Enterprise: scriptable via API for fleet management.
Enforcement check happens at JWT issuance — users.mfa_required_by_policy=true AND users.mfa_enabled=false AND policy.grace_expires_at < now() → reject login + redirect to enrollment.
6. Sovereignty + privacy
- TOTP secrets + backup codes encrypted with per-tenant KMS key (OVH KMS default per ADR-019).
- WebAuthn credentials stored in
webauthn_credentialstable — public key only (private key never leaves user device by spec). - Audit events stored in same audit log as ADR-019 break-glass events — immutable + 6y retention (legal).
- Reveal events:
actor_ip+user_agentPII per GDPR — access controlled to tenant admins + Noxys DPO only.
7. Sprint plan
Existing sprints (already filed, do not duplicate):
- noxys-api#92 — MFA/TOTP implementation (P0/P1, ongoing)
- noxys-console#69 — MFA TOTP + backup codes + sessions (P1)
- noxys-api#336 — WebAuthn passkeys (P2)
NEW sprints to file (this ADR introduces):
| ID | Title | Repo | Window | Effort | Priority |
|---|---|---|---|---|---|
| TENANT-TOKEN-REVEAL-1 | Admin eye-reveal tenant API token + audit log + multi-token model + rotate/revoke | noxys-api + noxys-console | W23-24 | 1d | P1 |
| MFA-POLICY-1 | Tenant-wide MFA enforcement policy + grace period + per-user exemption | noxys-api + noxys-console | W27-28 | 1.5d | P2 |
8. Action items on existing issues
Comment on noxys-api#92, noxys-console#69, noxys-api#336 with:
Ref: ADR-024 Authentication & MFA strategy. Acceptance criteria amendments per §3 of that ADR. Specifically: (a) per-tenant KMS encryption for TOTP secrets + backup codes (ADR-019 alignment); (b) WebAuthn = preferred enrollment path going forward; (c) Sensitive-action re-auth window 5min
mfa_verified_at; (d) "Remember device 30d" UX with admin-disable-able tenant policy.
Update each issue's body to reference ADR-024 in the Refs section.
9. Open questions
- TOTP fallback when WebAuthn lands: should TOTP be allowed as alt always, or admin-disable-able once tenant has 100% WebAuthn adoption? Defer decision until WebAuthn ships, then revisit.
- Backup codes legal retention: GDPR Art 17 erasure same as user PII (key shred per ADR-019). Confirm legal acceptability for incident-investigation evidence preservation.
- Phone/SMS MFA: NOT supported v1 (SS7 vulnerabilities + non-sovereign telcos). Revisit for compliance frameworks that mandate it (rare in EU).
- Magic-link / email-link login: NOT supported v1 (phishing risk). Revisit for low-friction Free tier onboarding only.
10. Non-goals
- SMS-based MFA (security + sovereignty concerns).
- SSO replacement (separate concern; SAML/OIDC owned by other roadmap items).
- Service-account auth refactor (separate ADR if needed).
- Per-IP / geo-fencing access (Enterprise feature, separate).
11. Refs
- ADR-018 Subscription lifecycle (JWT claim format)
- ADR-019 Storage tiers (KMS encryption pattern reused for TOTP secrets + backup codes)
- ADR-022 UsersPage V2 (MFA status col + bulk reset action — feeds from this ADR)
- noxys-api#92 (existing TOTP work)
- noxys-console#69 (existing backup codes + sessions work)
- noxys-api#336 (existing WebAuthn work)
- noxys-console#119 (closed UX Wave 1 baseline)
- Future ADR-027 CRM choice (renumbered through ADR-019..ADR-026 — ADR-026 = API/OpenAPI policy; ADR-025 unused)
- ADR-026 API completeness & OpenAPI policy (every endpoint defined in this ADR must have OpenAPI 3.1 spec entry; backfill obligation in §9)