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

### CSRF cookie name and login telemetry

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](../cookbook/cookie_csrf.md) and [OAuth associate](../cookbook/oauth_associate.md).

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](manager.md#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:

```python
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:

```python
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,
    ),
)
```

```python
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:

--8<-- "docs/snippets/plugin_security_tradeoffs.md"

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](manager.md#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` {#shared-helpers--litestar_authconfig}

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

## Related

- [HTTP API](../http_api.md) — routes controlled by the flags above.
- [Security](../security.md) — production interpretation of sensitive flags.
- [Plugin API](../api/plugin.md) — mkdocstrings for `LitestarAuth`, configs, and `litestar_auth.config`.
