Skip to main content

TOTP MFA

IdentSphere implements RFC 6238 TOTP — compatible with Google Authenticator, 1Password, Authy, Bitwarden, and every other authenticator app on the market.

Enrollment flow

[client] [IdentSphere]
│ POST /v1/auth/mfa/setup │
│ ───────────────────────────►│
│ │ mint 20-byte secret
│ { secret, qr_code_base64 } │ persist as user.mfa_secret
│ ◄─────────────────────────── │ mfa_enabled stays false
│ │
│ user scans QR in app │
│ │
│ POST /v1/auth/mfa/enable │
│ { code: "123456" } │
│ ───────────────────────────►│
│ │ verify code (±1 window)
│ { recovery_codes: [...] } │ mfa_enabled = true
│ ◄─────────────────────────── │ insert 10 recovery codes

The client must show the recovery codes to the user EXACTLY ONCE; the plaintext is never recoverable after this response.

Login flow (MFA enrolled)

POST /v1/auth/login → 200 { status: "mfa_required", mfa_token, mfa_token_expires_in: 300 }

POST /v1/auth/mfa/challenge { mfa_token, code }
→ 200 { status: "success", user, capabilities, access_token, ... }

The mfa_token is short-lived (5 min) and single-use. A successful challenge "burns" the token's JTI so concurrent or replayed challenges fail.

Recovery codes

10 alphanumeric 10-char codes are issued at enrollment. They're stored as SHA-256 hashes. The user can spend each one ONCE to sign in if they lose their authenticator.

Regenerate via POST /v1/auth/mfa/recovery-codes — password-gated.

Step-up

For sensitive operations inside an authenticated session, call POST /v1/auth/mfa/verify with the user's current TOTP code. The middleware stores a "recently verified" assertion in session_cache keyed by session_family_id for the configured TTL (default 30 min).

Recovery codes are NOT accepted for step-up — they're for sign-in recovery only.

Brute-force protection

The challenge endpoint counts failed attempts per mfa_token JTI. After 5 failures, further attempts return 429 rate_limited for the rest of the token's 5-minute lifetime.

Disabling

Password-gated. Calling POST /v1/auth/mfa/disable:

  1. Verifies the user's current password.
  2. Sets mfa_enabled = false.
  3. Clears mfa_secret.
  4. Deletes all recovery codes.
  5. Invalidates step-up cache entries for the user's active sessions.

Storage details

WhatWhere
TOTP secretusers.mfa_secret (base32, RFC 4648 no padding)
Recovery code hashesuser_recovery_codes.code_hash (SHA-256 hex)
Step-up assertionsession_cache key IdentSphere:mfa_verified:{sid}
Challenge attemptssession_cache key IdentSphere:mfa_attempts:{jti}
Burned challenge tokenssession_cache key IdentSphere:mfa_burned:{jti}

What MFA does NOT do

  • Phone-based SMS codes. SS7 is too leaky and we don't want to ship a product that recommends it.
  • Push notifications. Out of scope for v0.1.
  • WebAuthn (passkey). That's a separate flow — see Passkeys.

For phishing-resistant MFA, prefer passkeys over TOTP.