TOTP (two-factor authentication)¶
TOTP is enabled by setting totp_config: TotpConfig on LitestarAuthConfig. Routes are mounted under the auth prefix, e.g. /auth/2fa/....
Enrollment (two-step)¶
POST .../2fa/enable— returns a secret, otpauth URI, and short-livedenrollment_token(JWT). The secret is not stored until confirmation.POST .../2fa/enable/confirm— sendsenrollment_token+ TOTP code; on success the secret is persisted and the response returns one-time recovery codes.
By default totp_enable_requires_password=True, so step 1 also requires the current password (step-up).
Because the /2fa/enable response necessarily includes the plaintext TOTP secret and otpauth URI
for QR-code rendering, production deployments must serve this route only over HTTPS.
Enrollment-token confidentiality¶
The enrollment_token does not carry the freshly generated TOTP secret.
It carries only short-lived lookup claims (sub, jti, and an encoding marker).
The secret is stored server-side in totp_enrollment_store, encrypted first with
user_manager_security.totp_secret_keyring — the same keyring used to encrypt
the persisted secret. The one-key totp_secret_key shortcut remains available
for deployments with a single active Fernet key. In production,
totp_secret_keyring or totp_secret_key is required together with
totp_enrollment_store; plaintext, process-local enrollment state is only
created automatically when the owning config/controller explicitly sets
unsafe_testing=True.
Each /2fa/enable call replaces any previous pending enrollment for that user,
and /2fa/enable/confirm atomically consumes the matching jti. A stale token,
reused token, token from an older /enable, or token consumed by an invalid code
cannot be confirmed later.
Successful confirmation returns TotpConfirmEnableResponse with enabled: true
and recovery_codes. The plaintext recovery codes are issued only in that
response; the user model stores HMAC-SHA-256 lookup digests mapped to Argon2
hashes in recovery_codes.
Generated recovery codes are 28 lowercase hex characters (112 bits).
Recovery-code rotation¶
POST .../2fa/recovery-codes/regenerate— authenticated users can replace the active recovery-code set. The response returns the new plaintextrecovery_codesonce; old recovery codes stop working immediately.
By default this route follows the same password step-up policy as enrollment:
when totp_enable_requires_password=True, send
TotpRegenerateRecoveryCodesRequest with current_password. A wrong password
returns the same LOGIN_BAD_CREDENTIALS failure shape as login. Because recovery-code
rotation is also protected by TOTP step-up for enrolled users, the body can include
totp_code; when password step-up is disabled, totp_code is the interactive proof
for callers that do not already have a recent TOTP marker.
Login completion¶
When a login requires a second factor, the client finishes with:
POST .../2fa/verify— pending token + TOTP code, or pending token + an unused recovery code in the samecodefield. A recovery code is consumed on successful login and cannot be reused.
Pending login JWTs use a JTI denylist internally. In production, configure TotpConfig.totp_pending_jti_store on the plugin-managed path or pass pending_jti_store to create_totp_controller manually. Missing pending-token replay storage now fails closed unless the owning config/controller explicitly sets unsafe_testing=True.
Pending login JWTs are client-bound by default. With
TotpConfig.totp_pending_require_client_binding=True, /login adds SHA-256
fingerprint claims for the trusted-proxy-aware client IP (cip) and
User-Agent (uaf), and /2fa/verify recomputes them before accepting either a
TOTP code or recovery code. A mismatch returns the same 400
TOTP_PENDING_BAD_TOKEN shape as an expired or malformed pending token. Set
the flag to False only when your deployment accepts cross-client
pending-token replay; the controller logs that weaker posture at factory time.
Disable¶
POST .../2fa/disable— requires a valid current TOTP code or an unused recovery code. A recovery-code disable consumes the matching code, clears the TOTP secret, and clears any remaining recovery-code hashes.
Recovery-code lookup secret and schema migration¶
Production totp_config deployments must set
UserManagerSecurity.totp_recovery_code_lookup_secret to a distinct
CSPRNG-generated secret that clears validate_production_secret. The user table stores recovery codes in recovery_codes
(dict[str, str] | None), where each key is a server-side HMAC-SHA-256 lookup
digest and each value is the Argon2 hash of the code.
Migration checklist:
- Configure
totp_recovery_code_lookup_secretonUserManagerSecurity. - Run a data migration that drops or nulls the legacy
recovery_codes_hashescolumn and adds nullable JSONrecovery_codes. - Deploy the application.
- Tell users with TOTP enabled to log in using their authenticator app and
regenerate recovery codes at
/auth/2fa/recovery-codes/regenerate.
Replay protection¶
Production deployments should configure totp_used_tokens_store so codes cannot be reused. Without it, the library fails fast unless the owning config/controller explicitly opts into unsafe_testing=True.
When the same async Redis client should back auth rate limiting plus the TOTP Redis stores, use
the shared-client recipe in
Configuration. That is the maintained
RedisAuthPreset flow for build_rate_limit_config(),
build_totp_enrollment_store(), build_totp_pending_jti_store(), and
build_totp_used_tokens_store() together. Keep manual totp_enrollment_store /
pending_jti_store / totp_used_tokens_store wiring as the direct path when
you intentionally use separate backends or bespoke key prefixes.
The three production stores are still distinct even in the shared-client recipe:
totp_enrollment_storestores pending enrollment secrets and enforces latest-only, single-use confirmation.totp_pending_jti_storeprevents pending-login JWT replay.totp_used_tokens_storeprevents consumed TOTP-code replay.
For pytest-driven plugin tests, see the testing guide. Under unsafe_testing=True, the plugin can run without totp_used_tokens_store, but that is a single-process testing convenience rather than a production-safe replay-protection setup.
Algorithm defaults to SHA256 (totp_algorithm). Supported algorithms are SHA256 and SHA512.
Related¶
- Configuration — Redis-backed production recipe for rate limiting plus the TOTP Redis stores.
- TOTP —
TotpConfig. - TOTP API — helpers and types.
- Manager API — manager hooks for secrets and lifecycle.
- Testing plugin-backed apps — explicit
unsafe_testing, request-scoped sessions, and store-isolation boundaries.