Skip to content

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.

All manage endpoints require the account’s own session (admin or project).

  1. POST /auth/2fa/enroll → { otpauth_url, secret }. Render otpauth_url as a QR code (or show secret for manual entry). Re-enrolling before activation overwrites the pending secret; a 409 means 2FA is already enabled.
  2. POST /auth/2fa/activate { code } → verifies a live code, enables 2FA, and returns recovery_codes once. Store them somewhere safe — they are the only way in if the authenticator is lost, and are shown only here.
  3. GET /auth/2fa/status → { enabled }. POST /auth/2fa/disable { code } (a TOTP or a recovery code) turns it off.

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": "…" } + cookie

code 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.