MFA + step-up

TOTP for owners

Any user holding owner in any workspace is required to enroll TOTP before their next sensitive action. Enforcement is server-side via the app_private.user_has_mfa(user_id) helper, called at the top of the gated server actions.

Gated actions (planned set):

  • Billing change — Stripe Checkout + Customer Portal
  • Role change involving owner (promote / demote)
  • Member removal
  • Workspace deletion
  • Impersonation start

The MFA enrolment UI is the remaining piece — until it lands, the helper still works but the gates are bypassed in dev with a warning. Don't deploy to production without enrolment wired.

Impersonation step-up

Impersonation specifically uses a 6-digit OTP delivered to the admin's email (or surfaced inline in dev mode). On verify, the server mints a 60-minute JWT that becomes the new effective session. See Audit log + impersonation for the JWT shape and the doubly-logged audit pattern.

AAL2

Supabase Auth's AAL2 (session-level MFA flag) is checked additionally for billing changes. AAL2 alone isn't sufficient — a malicious actor with magic-link access could otherwise sidestep MFA without ever enrolling. The server-side user_has_mfa check is the load-bearing gate; AAL2 is the freshness signal.