# MFA Setup (KAR-534)

> Closes KAR-534. ASVS V6.5.1.
> Date: 2026-05-23.
> Scope: Multi-factor authentication for admin + masteradmin role accounts before BMW pilot go-live.

This is a procedure document for the operator. The app code already lets Supabase Auth handle MFA — what is missing is the configuration in the Supabase dashboard and the policy that enforces enrolment for admin accounts.

## Why MFA matters for the pilot

Admin / masteradmin accounts can read every project, every assessment, every evaluation. A compromised admin account is a tenant-wide breach. ASVS L2 V6.5 makes MFA on privileged accounts a hard requirement; this pilot inherits that floor.

Non-admin BMW user accounts are *not* required to enable MFA at L2 — only L3 mandates that. We may decide to require it anyway later; that is a product decision, not a security minimum.

## Step-by-step setup (operator action)

### 1. Enable MFA factors in Supabase

Dashboard → **Authentication** → **Multi-Factor Authentication** (or *Providers > MFA* depending on Supabase version).

- Enable **TOTP** (Time-based One-Time Password). Apps like Aegis, 1Password, Authy work out of the box.
- Optional: enable **WebAuthn** (passkeys / hardware keys). Recommended once the user base is comfortable with passwordless flows; not required for the pilot.

Leave **SMS** disabled — SMS as a second factor is consensus-deprecated in 2024+ (SS7 attacks, SIM swap risk). NIST SP 800-63B also discourages it.

### 2. Force MFA enrolment for admin role on next login

This is enforced by the app, not by Supabase out of the box. The flow:

1. After successful sign-in, `lib/auth/permissions.ts::getUserSession()` fetches the role.
2. If role is `admin` or `masteradmin` AND the user has zero verified MFA factors, redirect to `/auth/mfa-enroll`.
3. `/auth/mfa-enroll` walks through Supabase `mfa.enroll({ factorType: 'totp' })` → QR code → `mfa.challenge` → `mfa.verify`.
4. On verify success, set a profile flag (`user_profiles.mfa_enrolled_at = now()`) so future logins skip the enrolment redirect.

Implementation lives in a follow-up PR (the doc commits with this PR; the UI follows in a focused PR so reviewers see the auth flow change in isolation).

### 3. Recovery flow

Operator-supervised: if an admin loses their TOTP secret, **they cannot self-recover**. The recovery path is:

1. Admin contacts the operator via out-of-band channel (phone, in-person).
2. Operator verifies identity via the matched out-of-band credential (see `service-role-key-rotation.md`).
3. Operator clears the user's MFA factors via Supabase Dashboard → Authentication → Users → User Detail → Reset MFA.
4. Admin re-enrols on next sign-in.

**Do not** wire a "self-service MFA reset via email" flow. Email is the primary factor; making it the recovery factor too defeats the purpose of MFA.

### 4. Backup codes (optional, recommended)

Supabase TOTP does not ship backup codes by default. The operator can:

- Encourage admins to **store the TOTP secret QR code** as a backup at setup time (most password managers handle this automatically).
- Or implement a `mfa_backup_codes` table populated at enrolment with 8 single-use codes, hashed.

For the pilot, the first option is enough. The second is a V2 ergonomics improvement.

## Enforcement in proxy.ts (server-side guard)

In addition to the enrolment redirect, the middleware (`lib/supabase/proxy.ts`) should refuse admin-API requests if the JWT's `amr` (authentication methods reference) claim does not include a second-factor entry. This is the belt-and-suspenders check:

```
if (role === 'admin' || role === 'masteradmin') {
  const factors = claims.amr ?? []
  const hasMfa = factors.some(f => ['totp', 'webauthn'].includes(f))
  if (!hasMfa) return NextResponse.redirect(new URL('/auth/mfa-enroll', req.url))
}
```

This guard goes into the follow-up PR alongside the `/auth/mfa-enroll` route. Without it, an admin could in theory bypass the enrolment redirect via direct API calls.

## Test plan

Once the follow-up PR is wired:

- E2E: sign in as a fresh admin account → expect redirect to `/auth/mfa-enroll`.
- E2E: enrol via mock TOTP secret → expect redirect back to original route.
- E2E: subsequent sign-in → expect normal flow (no redirect).
- E2E: admin without MFA tries `/api/admin/*` → expect 401 + enrolment redirect.
- E2E: non-admin user signs in → no MFA prompt (Pilot scope).

## Status

| Item | Status |
|---|---|
| TOTP factor enabled in Supabase | **Operator action** (this PR's only ask) |
| WebAuthn factor enabled in Supabase | Defer |
| `/auth/mfa-enroll` route implemented | Follow-up PR |
| `proxy.ts` MFA guard for admin routes | Follow-up PR |
| Recovery procedure documented | This doc ✓ |
| Backup-codes mechanism | Defer to V2 |

## References

- KAR-522 audit Source 6 (OWASP ASVS V6.5)
- Supabase Auth — Multi-Factor Authentication docs
- NIST SP 800-63B § 5.1.3.3 — out-of-band authenticators (SMS deprecation rationale)
- `docs/security/service-role-key-rotation.md` (out-of-band recovery flow precedent)
