← Docs

Security

Multi-factor authentication (TOTP)

Self-service, from your own account — not something an Organization admin turns on for you. Manage it from Security, or directly:

GET  /api/v1/mfa/status/                  -> { enabled, backup_codes_remaining }
POST /api/v1/mfa/enroll/start/             -> { secret, otpauth_uri, qr_code_data_uri }
POST /api/v1/mfa/enroll/confirm/  {code}   -> { backup_codes: [...] }  (shown once)
POST /api/v1/mfa/disable/  {current_password?}
POST /api/v1/mfa/backup-codes/regenerate/  -> { backup_codes: [...] }  (shown once)

Once enabled, every sign-in for that account requires a second factor (a 6-digit TOTP code or a backup code) at token-exchange time — this replaces the device/location step-up gate below entirely for that account; a fresh TOTP code is at least as strong a re-proof as the trust gate exists to demand, so both firing would be redundant friction, not additional security.

Disabling MFA requires either your current password or a recent step-up-verified session — never a bare access token alone, since disabling your own second factor is exactly the kind of action a stolen-but-still-valid token should not be able to take unassisted.

Device & location trust — the step-up gate

For an account without MFA enabled, signing in from a genuinely new device or a new country triggers a step-up challenge instead of completing immediately — the password/Google credential was correct, but that alone isn't treated as enough from an unrecognized context. The response is a real, distinct OAuth error:

error: "step_up_required"

Completing a magic link, email code, or passkey verification satisfies the step-up immediately (those are already a real second proof). "Device" is fingerprinted from the user-agent alone — deliberately stable across a network/IP change, so switching wifi networks on the same physical browser never re-triggers this. "Location" is tracked separately, at country granularity — a recognized device signing in from a genuinely new country still gets its own step-up, independent of whether the device itself is known.

Manage your own trusted devices from Security:

GET  /api/v1/sessions/me/trusted-devices/
POST /api/v1/sessions/me/trusted-devices/{id}/revoke/

Revoking a trusted device does not revoke any active Session on it — it only means the next sign-in from that device is treated as new again, and will be asked to step up. To end an active session outright, revoke the Session itself — see Sessions.

Bot protection (Cloudflare Turnstile)

Signup and every first-factor request that can be attempted anonymously (password sign-in, magic-link request, email-OTP request) requires a valid Turnstile token, verified server-side against Cloudflare before the request is processed at all — a failed or missing token is rejected with turnstile_verification_failed before any real credential/account lookup happens, so it can't be used to probe account existence either.

Login rate limiting & lockout

Repeated failed password attempts against the same account, from the same IP, lock that specific (account, IP) combination out for a real cooldown window after 5 consecutive failures — a single success resets the counter immediately. This is independent of, and in addition to, the per-endpoint request-rate throttling every auth endpoint already carries.

What's not built yet

No hardware security key (FIDO2 roaming authenticator) support distinct from passkeys — a passkey covers this today for any WebAuthn-capable authenticator, platform or roaming. No account-recovery flow for a user who has lost both their password and their MFA device/backup codes simultaneously; account_recovery_initiated/account_recovery_completed exist as reserved audit-log event types but nothing writes them yet.