Two-factor authentication (TOTP)
Admin and project accounts can protect login with TOTP (the 6-digit codes from Google Authenticator / 1Password / Authy). It is self-contained (no external service, no SMTP): the secret is generated server-side, encrypted at rest (AES-256-GCM), and verified with a ±1 step window.
Enrolment
Section titled “Enrolment”All manage endpoints require the account’s own session (admin or project).
POST /auth/2fa/enroll→{ otpauth_url, secret }. Renderotpauth_urlas a QR code (or showsecretfor manual entry). Re-enrolling before activation overwrites the pending secret; a409means 2FA is already enabled.POST /auth/2fa/activate{ code }→ verifies a live code, enables 2FA, and returnsrecovery_codesonce. Store them somewhere safe — they are the only way in if the authenticator is lost, and are shown only here.GET /auth/2fa/status→{ enabled }.POST /auth/2fa/disable{ code }(a TOTP or a recovery code) turns it off.
Login (two-step, when 2FA is enabled)
Section titled “Login (two-step, when 2FA is enabled)”POST /auth/login stays backward-compatible: with 2FA off it returns the
session as before. With 2FA on it does NOT issue a session — it returns a
short-lived challenge instead:
POST /auth/login → { "two_factor_required": true, "challenge_token": "…" }POST /auth/2fa/login → { challenge_token, code } → { "token": "…" } + cookiecode is either a current TOTP or a single-use recovery code (consumed on
use). The challenge_token is a 5-minute JWT with role 2fa_challenge — it is
excluded from the session type, so no guard anywhere accepts it as a real
session; only /auth/2fa/login does.