TOTP¶
Use this page for TotpConfig fields and the plugin-owned TOTP route contract.
TOTP — totp_config: TotpConfig | None¶
| Field | Default | Meaning |
|---|---|---|
totp_pending_secret |
(required) | Secret for pending-2FA JWTs; must align with auth controller. |
totp_backend_name |
None |
Which named AuthenticationBackend issues tokens after 2FA. |
totp_issuer |
"litestar-auth" |
Issuer in otpauth URI. |
totp_algorithm |
"SHA256" |
TOTP hash algorithm; supported values are "SHA256" and "SHA512". |
totp_pending_jti_store |
None |
JWT JTI denylist for pending login tokens. Required unless the owning config/controller explicitly sets unsafe_testing=True. Use a shared store such as RedisJWTDenylistStore for multi-worker deployments. |
totp_enrollment_store |
None |
Server-side pending-enrollment store. Required unless the owning config/controller explicitly sets unsafe_testing=True. Use a shared store such as RedisTotpEnrollmentStore for multi-worker deployments. |
totp_used_tokens_store |
None |
Replay store for consumed TOTP codes (required unless the owning config/controller explicitly sets unsafe_testing=True). See Redis-backed auth surface for the Redis setup and import paths. |
totp_require_replay_protection |
True |
Fail startup without a store unless unsafe_testing=True. |
totp_enable_requires_password |
True |
Step-up password for /2fa/enable. |
totp_pending_require_client_binding |
True |
Bind pending-login JWTs to hashed client IP and User-Agent fingerprints and reject mismatches at /2fa/verify. |
Routes: {auth_path}/2fa/.... See TOTP guide.
When LitestarAuthConfig.totp_config is set, the plugin derives an internal TOTP extension and
mounts the bundled TOTP controller through the same extension contribution mechanism used by other
optional route surfaces. This is an internal wiring detail: applications enable plugin-owned TOTP
with totp_config, not by instantiating a public TOTP extension. The extension contributes the
same /2fa controller and keeps controller-build validation in the factory path, so missing replay
stores or production TOTP secret encryption still fail when the controller is built during plugin
startup.
The plugin-owned TOTP flow follows LitestarAuthConfig.requires_verification, which defaults
to True. Manual create_totp_controller(...) wiring should keep that flag aligned with the auth
controller so unverified accounts cannot complete the second authentication step. When both checks
fail, the shared account-state policy reports inactive users before unverified users.
totp_pending_secret signs pending-2FA JWTs for the controller flow. It is separate from
user_manager_security.totp_secret_keyring, which encrypts the TOTP secret at rest on the user
record and before writing the short-lived pending-enrollment secret to
totp_enrollment_store. The enrollment JWT returned by /2fa/enable carries only lookup claims,
not the secret. The plugin forwards the configured keyring into
create_totp_controller(..., totp_secret_keyring=...) automatically. Manual controller wiring keeps
the same keyword names, typed by TotpControllerOptions. In production,
totp_secret_keyring or the one-key totp_secret_key shortcut is required —
create_totp_controller fails closed with ConfigurationError when both are omitted and
unsafe_testing=False. Persisted user-row TOTP secrets are stored as
fernet:v1:<key_id>:<ciphertext> values and unprefixed plaintext rows are rejected fail-closed.
Use BaseUserManager.totp_secret_requires_reencrypt(...) and
BaseUserManager.reencrypt_totp_secret_for_storage(...) from migration code that rewrites stored
values under the active configured key.
Startup validation for plugin-owned TOTP store requirements fails with ConfigurationError.
This includes missing totp_pending_jti_store, totp_enrollment_store, and
totp_used_tokens_store when unsafe_testing=False. This is an intentional breaking change from
the earlier ValueError raised for missing pending-token and enrollment stores; catch
ConfigurationError around plugin configuration validation if the application handles startup
misconfiguration explicitly.
For rotation, add a new Fernet key id to user_manager_security.totp_secret_keyring.keys, deploy the
expanded keyring first, then switch active_key_id to the new id. New pending-enrollment and
persisted TOTP writes use the new key id while old versioned rows remain readable. An
application-owned migration can scan persisted TOTP secret values, call
BaseUserManager.totp_secret_requires_reencrypt(value), and rewrite only values that return True
through BaseUserManager.reencrypt_totp_secret_for_storage(value). Verify a final scan before
removing the retired key id. The full staged checklist is in
Deployment.
Legacy unversioned Fernet values are migration input only because they do not carry a key id. They
must be decrypted with explicit old key material and rewritten to fernet:v1:<key_id>:<ciphertext>.
Plaintext persisted TOTP rows remain unsupported and fail closed; clear or encrypt them before
production use.
If totp_backend_name is omitted, the plugin uses the primary startup backend. Set a backend name
only when a secondary startup backend should issue post-2FA tokens.
Pending-token JTI store
The plugin-owned controller forwards TotpConfig.totp_pending_jti_store into create_totp_controller(..., pending_jti_store=...). In production, missing pending-token replay storage fails closed unless unsafe_testing=True.
Pending-token client binding
The plugin-owned auth and TOTP controllers both forward TotpConfig.totp_pending_require_client_binding. Keep the default True unless your proxy topology cannot provide stable client metadata and you accept pending-token replay from a different client. The fingerprints are SHA-256 hex values, not raw IP or User-Agent strings.
The client-IP fingerprint resolves through the same trusted_proxy, trusted_headers, and trusted_proxy_hops settings as the /2fa/verify rate limiter — these are mirrored from totp_verify so the binding keys on the same X-Forwarded-For hop the limiter does. In a multi-proxy deployment set totp_verify.trusted_proxy_hops=N (see rate limiting); otherwise the binding would fingerprint the inner-proxy address shared by every client behind it and the IP component would no longer distinguish callers.
Pending-enrollment store
The plugin-owned controller forwards TotpConfig.totp_enrollment_store into create_totp_controller(..., enrollment_store=...). In production, missing enrollment storage fails closed unless unsafe_testing=True; each /2fa/enable replaces the previous pending enrollment for that user, and /2fa/enable/confirm consumes the matching jti once.
TOTP step-up for sensitive operations¶
TOTP step-up is the server-side proof that an authenticated user recently completed an app-code
TOTP verification. The marker is session-scoped, stored by the configured token strategy, and
expires server-side. Sensitive controller operations check that marker before mutating state, or
verify an inline totp_code when the request body supports one.
The relevant LitestarAuthConfig fields are:
| Field | Default | Meaning |
|---|---|---|
totp_stepup_ttl_seconds |
300 |
Lifetime, in seconds, for a recent-TOTP marker. The value must be a non-negative integer. |
totp_stepup_policy |
{} |
Per-endpoint overrides. Valid modes are required_when_enrolled, always_required, and off. Unknown endpoint keys fail startup validation. |
totp_stepup_allow_recovery |
False |
Controls whether successful recovery-code verification at {auth_path}/2fa/verify may issue the recent-TOTP marker. Keep the default when recovery codes should only complete login. |
Default endpoint policy:
totp_stepup_policy key |
Built-in route surface | Default mode | Inline proof field |
|---|---|---|---|
users.update_self |
PATCH {users_path}/me email changes |
required_when_enrolled |
totp_code |
api_keys.create |
POST /api-keys and POST {users_path}/{user_id}/api-keys |
required_when_enrolled |
totp_code on create bodies |
api_keys.update |
PATCH /api-keys/{key_id} |
required_when_enrolled |
totp_code |
api_keys.revoke |
DELETE /api-keys/{key_id} and DELETE {users_path}/{user_id}/api-keys/{key_id} |
required_when_enrolled |
None; use a recent marker |
oauth.associate |
GET {auth_path}/associate/{provider}/callback |
required_when_enrolled |
None; use a recent marker |
totp.disable |
POST {auth_path}/2fa/disable |
required_when_enrolled |
code |
totp.regenerate_recovery_codes |
POST {auth_path}/2fa/recovery-codes/regenerate |
required_when_enrolled |
totp_code |
required_when_enrolled preserves existing behavior for users without TOTP. If the user has an
active TOTP secret, the operation requires either a recent marker for the same authenticated session
or a valid inline TOTP code. always_required also blocks users without an active readable TOTP
secret, which is useful only when enrollment is mandatory before the protected operation can be used.
off disables the built-in gate for that endpoint.
When the gate fails, bundled controllers return HTTP 403 with ErrorCode.TOTP_STEPUP_REQUIRED
(TOTP_STEPUP_REQUIRED) in the response extra.code. Clients should prompt for a fresh TOTP code,
submit it inline when the endpoint accepts the field, or complete {auth_path}/2fa/verify and retry
from the same session.
Recovery codes are intentionally narrower than app-code verification. With the default
totp_stepup_allow_recovery=False, a recovery code can complete the pending login flow but does not
create a marker that unlocks other sensitive operations. POST {auth_path}/2fa/disable still accepts
a recovery code as its own unlock factor so users can disable MFA when the authenticator app is
unavailable.
API-key authenticated requests cannot complete an interactive TOTP challenge. API-key management
routes also require requires_password_session, so API-key callers are rejected before the step-up
gate instead of being allowed to satisfy it with the same delegated credential they are trying to
manage. Use a bearer or cookie login session for API-key create/update/revoke flows.