Skip to content

Multi-factor authentication

MFA is opt-in per project: Authentication → Settings → Multi-factor authentication. Users add an authenticator app (TOTP, 6 digits, 30 s). Once they did, password sign-in asks for a code.

// enroll (signed in): render data.totp.uri as a QR code, or show data.totp.secret
const { data: f } = await base.auth.mfa.enroll({ friendlyName: "Phone" })
const { data: v } = await base.auth.mfa.verify({ factorId: f!.id, code: "123456" })
showOnce(v!.recoveryCodes) // 10 single-use codes, only returned here
// sign in: password first, then the second step
const r = await base.auth.signInWithPassword({ email, password })
if (r.data.mfaRequired) {
await base.auth.mfa.challenge({ ticket: r.data.ticket!, code }) // or { recoveryCode }
}
// step up a session that signed in another way (magic link, OAuth...)
const { data: aal } = await base.auth.mfa.getAuthenticatorAssuranceLevel()
if (aal!.currentLevel !== aal!.nextLevel) await base.auth.mfa.verify({ code })
await base.auth.mfa.listFactors() // never returns secrets
await base.auth.mfa.unenroll({ factorId }) // a verified factor needs an aal2 session
await base.auth.mfa.regenerateRecoveryCodes() // aal2 session

All MFA calls return { data, error }; error.code is mfa_disabled until the project enables MFA.

Tokens carry aal (aal1 or aal2). Enforce MFA on the data, not only in the app:

create policy "payouts need MFA" on public.payouts as restrictive for insert to authenticated
with check ((select auth.aal()) = 'aal2');

The dashboard policy editor has a Require MFA (aal2) option in Custom mode.

  • The level belongs to the session: a refresh keeps aal2, a new sign-in starts at aal1.
  • With “required for all” users, sessions of users without a factor carry mfa_enrollment_required: true so you can prompt them to enroll. Enforcement on data is still your aal2 policy.
  • A code cannot be used twice (mfa_code_reused). Five wrong codes in five minutes lock the second step for five minutes (429, doubling up to one hour). Recovery codes count toward the same limit.
  • Lost device: open the user in Authentication → Users and choose Reset MFA.