Skip to content

Security and DI

Use this page for CSRF settings, token downgrade policy, schemas, dependency keys, and shared configuration helpers.

Security and token policy

Field Default Meaning
csrf_secret None Enables Litestar CSRF config when cookie transports are used.
csrf_header_name "X-CSRF-Token" Header Litestar expects for CSRF token.
unsafe_testing False Explicit per-config test-only override for generated fallback secrets, single-process validation shortcuts, and startup-warning suppression. Never enable it for production traffic.
login_minimum_response_seconds 0.4 Minimum wall-clock duration for plugin-owned POST /auth/login account-state failure, pending-TOTP, and success responses.
register_minimum_response_seconds 0.4 Minimum wall-clock duration for plugin-owned POST /auth/register success and domain-failure responses. This is defense-in-depth against registration timing enumeration and is independent of rate limiting.
verify_minimum_response_seconds 0.4 Minimum wall-clock duration for plugin-owned POST /auth/verify success and invalid-token responses.
request_verify_minimum_response_seconds 0.4 Minimum wall-clock duration for plugin-owned POST /auth/request-verify-token responses, including manager failures after rate-limit accounting runs.
id_parser None Parse path/query user ids (e.g. UUID). Effective parser: user_manager_security.id_parser when set; otherwise LitestarAuthConfig.id_parser is applied (including the no-user_manager_security default builder path).
account_lockout_config AccountLockoutConfig() Opt-in per-account password-login lockout. Disabled by default; when enabled it uses failure_threshold=5, window_seconds=900.0, and an in-memory store unless store_factory supplies a shared store.

When csrf_secret is set, the plugin wires Litestar CSRF for cookie transports using the cookie name litestar_auth_csrf (same as DEFAULT_CSRF_COOKIE_NAME in litestar_auth.plugin / litestar_auth._plugin.config). Frontend code must read that cookie to mirror the value into csrf_header_name (default X-CSRF-Token); see also cookie CSRF cookbook and OAuth associate.

Failed-login telemetry is not a LitestarAuthConfig field. Set optional UserManagerSecurity.login_identifier_telemetry_secret so structured logs can carry an HMAC digest of the identifier on failures (no raw identifier in logs). Details: Manager customization — Login failure telemetry secret.

Per-account password-login lockout

LitestarAuthConfig.account_lockout_config enables a per-account counter for plugin-owned password login failures. It is separate from IP/request rate limiting: account lockout tracks the normalized login identifier behind a keyed digest, while rate_limit_config.login still throttles by its configured request identity.

The shipped fields are:

AccountLockoutConfig field Default Meaning
enabled False Leaves account lockout inert unless explicitly enabled.
failure_threshold 5 Number of failed password checks for the same account key before the key is locked.
window_seconds 900.0 Counter TTL and lockout window in seconds. A successful password login before the threshold resets the counter.
store_factory None Optional zero-argument factory returning an AccountLockoutStore. When omitted, the config memoizes an InMemoryAccountLockoutStore.

Plugin-managed lockout key derivation uses UserManagerSecurity.login_identifier_telemetry_secret. Set a high-entropy, role-separated value before enabling lockout:

from litestar_auth import LitestarAuthConfig, UserManagerSecurity
from litestar_auth.ratelimit import AccountLockoutConfig

config = LitestarAuthConfig(
    user_model=User,
    user_manager_class=UserManager,
    session_maker=session_maker,
    account_lockout_config=AccountLockoutConfig(enabled=True),
    user_manager_security=UserManagerSecurity(
        login_identifier_telemetry_secret=login_identifier_secret,
        reset_password_token_secret=reset_password_secret,
        verification_token_secret=verification_token_secret,
    ),
)

The default InMemoryAccountLockoutStore is process-local. It is appropriate for single-process development, tests, and deployments that explicitly accept single-worker state. Production multi-worker deployments need a shared store such as RedisAccountLockoutStore; when deployment_worker_count > 1, plugin startup fails closed if enabled account lockout resolves to a process-local store.

The in-memory store also has a bounded key capacity. If it reaches max_keys after expired counters are pruned, unknown account keys are treated as locked and the login path fails closed. That protects existing lockout counters from being dropped under capacity pressure, but a flood of distinct identifiers can deny new logins until counters expire. For publicly exposed, high-volume, or multi-tenant deployments, prefer RedisAccountLockoutStore; if you intentionally keep the in-memory store, supply it through store_factory with a max_keys sized for the expected identifier cardinality and memory budget:

from litestar_auth.ratelimit import AccountLockoutConfig, InMemoryAccountLockoutStore

account_lockout_config = AccountLockoutConfig(
    enabled=True,
    failure_threshold=5,
    window_seconds=900.0,
    store_factory=lambda: InMemoryAccountLockoutStore(
        failure_threshold=5,
        window_seconds=900.0,
        max_keys=500_000,
    ),
)
from litestar_auth.ratelimit import AccountLockoutConfig, RedisAccountLockoutStore

account_lockout_config = AccountLockoutConfig(
    enabled=True,
    failure_threshold=5,
    window_seconds=900.0,
    store_factory=lambda: RedisAccountLockoutStore(
        redis=redis_client,
        failure_threshold=5,
        window_seconds=900.0,
    ),
)

Locked accounts intentionally receive the same LOGIN_BAD_CREDENTIALS response as a wrong password or missing user. The response has no lockout-specific status, error code, or Retry-After header, so attackers cannot distinguish account existence or lock state through the login endpoint.

This non-enumerating property is timing-safe only while login_minimum_response_seconds dominates the Argon2 verification cost. A locked account short-circuits before password hashing, whereas wrong-password and unknown-account failures both pay the Argon2 verification; the shared response-time floor pads all three paths (including the locked short-circuit) to the same duration, masking the difference. The default 0.4s floor comfortably exceeds typical Argon2 cost. If you lower it below your Argon2 verification time the locked path becomes timing-distinguishable, reopening account enumeration — plugin startup emits a SecurityWarning when account lockout is enabled and login_minimum_response_seconds is below 0.2s for this reason.

The plugin-managed JWT/TOTP security surfaces use the same shared posture wording as runtime startup and validation:

Surface Runtime contract Compatibility / tradeoff Plugin-managed production behavior
JWTStrategy(allow_inmemory_denylist=True) JWTStrategy.revocation_posture JWTStrategy requires an explicit denylist_store, or allow_inmemory_denylist=True for single-process in-memory revocation. Plugin-managed production has no separate JWT revocation compatibility flag; startup warns when a strategy is explicitly wired to process-local in-memory revocation.
user_manager_security.totp_secret_keyring / totp_secret_key BaseUserManager.totp_secret_storage_posture BaseUserManager.totp_secret_storage_posture is the Fernet-encrypted at-rest contract; omitting both totp_secret_keyring and totp_secret_key means non-null persisted TOTP secrets cannot be stored or read. Pending-enrollment secrets are plaintext only under explicit unsafe_testing=True. With totp_config enabled, plugin-managed production requires user_manager_security.totp_secret_keyring or the one-key totp_secret_key shortcut unless unsafe_testing=True or a custom user_manager_factory explicitly owns that wiring. totp_enrollment_store is also required unless unsafe_testing=True.

For direct/manual wiring, the underlying runtime objects report their own posture explicitly:

  • JWTStrategy(secret=..., denylist_store=RedisJWTDenylistStore(...)) reports revocation_posture.key == "shared_store" and revocation_is_durable == True.
  • JWTStrategy(secret=..., allow_inmemory_denylist=True) reports revocation_posture.key == "in_memory" and revocation_is_durable == False. InMemoryJWTDenylistStore fails closed when its max_entries cap is hit and no expired JTIs can be pruned: new revocations are skipped (logged) instead of dropping an active revocation entry. JWTDenylistStore.deny returns False in that case; JWTStrategy.destroy_token raises TokenError, and plugin HTTP logout surfaces 503 with TOKEN_PROCESSING_FAILED. The same capacity signal applies to TOTP pending-login JTI recording after a successful code check: verification responds with 503 instead of issuing a session when the pending-token denylist cannot store the spent JTI.
  • LitestarAuthConfig(account_token_denylist_store=RedisJWTDenylistStore(...)) (or the same account_token_denylist_store= argument on BaseUserManager) makes verify and reset-password tokens single-use server-side: the token's jti is checked on entry and consumed on a successful verify/reset, so a captured token cannot be replayed even before the password fingerprint / verification state rotates. It reuses the JWTDenylistStore protocol (share the access-token store or use a dedicated one); when unset (default), account-token single-use rests on fingerprint/verification-state rotation only. Optional and fully backward-compatible.
  • Direct BaseUserManager(..., security=UserManagerSecurity(...)) reports totp_secret_storage_posture.key == "fernet_encrypted" for persisted TOTP secrets. Setting user_manager_security.totp_secret_keyring=FernetKeyringConfig(...) on LitestarAuthConfig (passed through to UserManagerSecurity) or on a direct UserManagerSecurity(...) bundle enables encrypted reads and writes with active-key rotation. The one-key totp_secret_key field remains available for single-key deployments; omitting both key inputs leaves disabled TOTP (None) readable but makes non-null TOTP secret persistence fail closed. The TOTP controller uses the same keyring/key to encrypt pending-enrollment secret values before writing them to totp_enrollment_store.
  • ApiKeyConfig(signing_enabled=True, secret_encryption_keyring=FernetKeyringConfig(...)) enables encrypted storage for signing-required API-key secrets. In operator docs, this field is named api_keys.secret_encryption_keyring because it belongs to the nested API-key config, not UserManagerSecurity. Rotate it with the same staged keyring shape as OAuth and TOTP: deploy old+new ids, switch active_key_id, re-encrypt rows, verify, then remove the retired id.

API-key signing-secret rotation

Only signing-required API-key rows participate in api_keys.secret_encryption_keyring rotation. Bearer rows remain digest-only (hashed_secret only), do not have recoverable plaintext, cannot be upgraded to signing mode, and should not be passed to signing-secret rotation helpers.

Use the manager helpers for one row at a time:

  • BaseUserManager.api_key_signing_secret_requires_reencrypt(row) returns whether the row's encrypted_secret is readable but not encrypted under the active key id.
  • await BaseUserManager.reencrypt_api_key_signing_secret(row_or_key_id) rewrites that one signing-required row under the active key id and returns the updated row metadata. It does not print, log, or return the plaintext signing secret, and it does not run API-key create, revoke, or use lifecycle hooks.

Failure handling is fail-closed. Missing api_keys.secret_encryption_keyring, bearer rows, signing rows without encrypted_secret, raw bearer credential input, unknown key ids, malformed envelopes, and replacement races surface as errors for the migration job to handle. Treat those as operator cleanup cases; do not remove retired Fernet key ids until a fresh verification scan finds no signing-required rows that still need re-encryption.

Password hash policy

PasswordHelper.from_defaults(), bare PasswordHelper(), BaseUserManager(..., password_helper=None), and config.resolve_password_helper() use an Argon2-only default policy. Unsupported stored password hashes therefore fail closed: verification returns False, and verify_and_update() does not emit a replacement hash for an unsupported stored value.

Before deploying that default into an environment with unsupported stored hashes, rotate or reset those credentials out of band. If you inject UserManagerSecurity(password_helper=...), that password policy is fully application-owned and outside the library default described here.

Password hashing and TOTP recovery-code hashing run through a shared AnyIO worker-thread limiter instead of the event loop. The default cap is 8 concurrent password operations per event loop — equivalent to per process for the typical one-loop server. Set LITESTAR_AUTH_PASSWORD_WORKER_THREAD_LIMIT to a positive integer before importing litestar_auth when a deployment needs a different cap. Size this with the memory formula limit * Argon2 memory cost; with the bundled default Argon2 policy, plan for roughly limit * 64 MiB per event loop before other application memory. Invalid values fail fast with ConfigurationError at import time.

Schemas and DI

Field Default Meaning
user_read_schema None msgspec struct for safe user responses returned by register/verify/reset/users flows. The built-in UserRead includes normalized roles.
user_create_schema None msgspec struct for registration/create request bodies; built-in registration defaults to UserCreate.
user_update_schema None msgspec struct for user PATCH bodies on the self-service /users/me route. The built-in UserUpdate accepts email plus current_password proof for email changes — privileged fields (is_active, is_verified, roles) live on AdminUserUpdate for the privileged PATCH /users/{id} route, and self-service requests that include them are rejected at msgspec decode (forbid_unknown_fields=True).
db_session_dependency_key "db_session" Litestar DI key for AsyncSession. Must be a valid non-keyword Python identifier because Litestar matches dependency keys to callable parameter names.
db_session_dependency_provided_externally False Skip plugin session provider when your app already registers the key.
session_scope_key None (uses Advanced Alchemy SESSION_SCOPE_KEY) Advanced Alchemy scope key for the request session. When using SQLAlchemyPlugin, prefer bind_auth_session_to_alchemy(alchemy_config) or set this to SQLAlchemyAsyncConfig.session_scope_key after the config is constructed.

user_*_schema customizes registration and user CRUD surfaces. It does not rename the built-in auth lifecycle request structs: LoginCredentials, RefreshTokenRequest, RequestVerifyToken, VerifyToken, ForgotPassword, ResetPassword, or the TOTP payloads.

If you keep the built-in role-aware user surface, align custom read/update schemas with that contract by adding roles to both types. If you intentionally omit roles, treat that as an explicit compatibility choice for role-less user models rather than the default library contract.

When app-owned user_create_schema or user_update_schema structs keep email or password fields, import UserEmailField / UserPasswordField from litestar_auth.schemas instead of copying the built-in email regex or local msgspec.Meta(min_length=12, max_length=128) annotations. See Manager password surface for the full contract: those aliases keep schema metadata aligned, while runtime password validation still flows through password_validator_factory or the manager's default validator.

Dependency keys (constants)

Used by the plugin internally; override only if you integrate custom controllers:

  • litestar_auth_config, litestar_auth_user_manager, litestar_auth_backends, litestar_auth_user_model (see litestar_auth._plugin.config).

Shared helpers — litestar_auth.config

validate_production_secret, validate_secret_length, _resolve_token_secret, MINIMUM_SECRET_LENGTH, and the secret-role helpers keep token validation and explicit unsafe-testing behavior consistent. Production secrets must clear both the length floor and the default 128-bit entropy floor. The estimator rejects short repeated structured patterns such as copied fixture phrases as well as single-character strings, and additionally rejects low-complexity sequential codepoint walks (for example abc...xyz123) that clear the raw frequency-based entropy floor but are trivially guessable; generate production values with secrets.token_hex(32) or secrets.token_urlsafe(32). Low-entropy fixture strings are accepted only behind explicit unsafe_testing=True paths. Operators that intentionally own a different secret-quality gate can pass minimum_entropy_bits=0 to the helper to keep length validation while skipping the entropy check.

  • HTTP API — routes controlled by the flags above.
  • Security — production interpretation of sensitive flags.
  • Plugin API — mkdocstrings for LitestarAuth, configs, and litestar_auth.config.