Skip to content

Deployment checklist

Use this when moving from local development to production, especially for secrets, multi-worker deployments, and shared stores (Redis) where in-memory defaults are insufficient.

Process topology

  • Single worker / dev — in-memory JWT denylist, in-memory rate limiting, and in-memory TOTP stores are acceptable for local testing only.
  • Multiple workers or restarts that matter — use Redis (or equivalent shared stores) for: JWT jti denylist, the auth rate-limit config, totp_enrollment_store, totp_pending_jti_store, and totp_used_tokens_store. When one async Redis client should back auth rate limiting plus the TOTP stores, use litestar_auth.contrib.redis.RedisAuthPreset as the shared-client path and keep the three TOTP stores conceptually separate: pending-enrollment secrets, pending-token JTI deduplication, and used-code replay protection.

Declare known process topology with LitestarAuthConfig.deployment_worker_count. None means the plugin cannot reliably infer the ASGI host's worker count and preserves warning-only startup diagnostics. 1 means a known single-worker deployment. Values greater than 1 mean known multi-worker: startup fails with ConfigurationError when any enabled auth rate-limit endpoint uses a process-local backend such as InMemoryRateLimiter. Use RedisRateLimiter or RedisAuthPreset before declaring a multi-worker production deployment.

Secrets and keys

  • Store production secrets in a secrets manager or KMS-backed runtime secret source. Do not commit them, bake them into images, or share one value across auth roles for convenience.
  • Rotate secrets independently by role. Plan rotation for JWT signing keys, verify/reset-token secrets, CSRF secrets, TOTP Fernet keys, OAuth token encryption keys, API-key signing-secret Fernet keys, OAuth flow-cookie secrets, and opaque-token hash secrets before the first production incident.
  • For the full manager/password contract, including PasswordHelper sharing, password_validator_factory, and the UserEmailField / UserPasswordField schema helpers, see Configuration. The checklist below only calls out production consequences.
  • For plugin-managed apps, configure manager-scoped secrets via LitestarAuthConfig.user_manager_security.
  • JWT signing secret (or private key) — high entropy; rotation plan.
  • verification_token_secret and reset_password_token_secret — configure both through user_manager_security; each must satisfy the production strength gate enforced by validate_production_secret (32+ characters plus the default entropy floor).
  • login_identifier_telemetry_secret — optional dedicated key for failed-login identifier_digest log correlation. Configure it through user_manager_security only when you want stable cross-request digests; otherwise the digest field is omitted.
  • totp_secret_keyring — configure through user_manager_security when TOTP is enabled; required in production because stored TOTP secrets and pending-enrollment secrets must be encrypted at rest. Use FernetKeyringConfig(active_key_id=..., keys=...) so old key ids remain readable during rotation. The one-key totp_secret_key field remains available when a single active Fernet key is enough. Existing plaintext persisted TOTP secrets must be encrypted, rotated, or cleared before upgrading to versions that enforce encrypted-only TOTP secret storage.
  • csrf_secret — required for meaningful CSRF protection when using cookie-based auth with the plugin’s CSRF wiring.
  • totp_pending_secret — required when TOTP is enabled; protects pending login payloads.
  • oauth_token_encryption_keyring — required when OAuth providers are configured (encrypts tokens at rest in the DB). The one-key oauth_token_encryption_key field remains available for single-key deployments.
  • oauth_flow_cookie_secret — required when OAuth providers are configured (HKDF-derived into the Fernet key that encrypts and authenticates transient OAuth state plus PKCE verifier material in the browser-held flow cookie).
  • token_hash_secret (database opaque token strategy) — protects digest-at-rest storage for DB tokens.
  • api_keys.secret_encryption_keyring — required when API-key request signing is enabled and signing-required API keys can be created. This keyring encrypts the recoverable signing secret copy stored in api_key.encrypted_secret; bearer keys do not use it and remain digest-only.
  • Keep verification_token_secret, reset_password_token_secret, login_identifier_telemetry_secret when configured, totp_pending_secret, every configured TOTP Fernet key, and oauth_flow_cookie_secret distinct. Production configuration rejects reuse with ConfigurationError; only explicit unsafe_testing=True test setups bypass this validation. For plugin-managed apps the error is raised during LitestarAuth(config) validation; direct BaseUserManager(...) construction enforces the same rule for its manager-owned secret roles. Distinct values are the supported posture: litestar-auth:verify, litestar-auth:reset-password, and litestar-auth:2fa-pending / litestar-auth:2fa-enroll already separate JWT audiences, while failed-login telemetry, TOTP Fernet keys, and oauth_flow_cookie_secret should remain dedicated keys with no JWT audience.

For non-keyring secrets (verification_token_secret, reset_password_token_secret, login_identifier_telemetry_secret, csrf_secret, totp_pending_secret, oauth_flow_cookie_secret, and opaque-token hash secrets), write an application runbook before production. Rotation is role-specific: verification/reset/TOTP pending JWT secret rotation invalidates outstanding tokens for that flow, OAuth flow-cookie rotation invalidates in-progress OAuth handshakes, CSRF secret rotation affects active browser sessions, and login telemetry rotation starts a new digest correlation window. Keep token lifetimes short enough that planned rotation can happen without a long dual-secret compatibility period.

Deployment security contract

These requirements are deployment preconditions, not library toggles that make unsafe infrastructure safe. Configure them before exposing browser auth, rate-limited auth endpoints, OAuth token storage, TOTP, or API-key request signing to production traffic.

Reverse-proxy and trust boundaries

Set trusted_proxy=True only on rate-limit endpoints whose traffic reaches the application through a trusted reverse proxy or load balancer that overwrites the configured trusted_headers. This applies to EndpointRateLimit.trusted_proxy, EndpointRateLimit.trusted_headers, EndpointRateLimit.trusted_proxy_hops, and the shared builders that copy those settings into endpoint limiters, including SharedRateLimitConfigOptions.trusted_proxy and RedisAuthRateLimitConfigOptions.trusted_proxy.

The default trusted header is X-Forwarded-For. When trusted_proxy=True, the rate-limit key builder uses trusted_proxy_hops=1 by default, selecting the rightmost X-Forwarded-For value because that is the value appended by the immediately upstream trusted proxy in common proxy_add_x_forwarded_for deployments. Leftmost entries may be client-controlled and spoofed. For multi-proxy chains such as CDN -> load balancer -> app, set trusted_proxy_hops to the number of trusted proxy hops between the real client IP and the application, counted from the right. If the header carries fewer entries than trusted_proxy_hops, rate-limit identity fails closed to the direct client host. Leave trusted_proxy=False when those conditions are not true; the direct client host is the fail-closed rate-limit identity source.

Browser cookie authentication requires HTTPS in production. Keep CookieTransportConfig.secure=True and CookieTransport.secure=True, terminate TLS before the app, and set HSTS at the edge. The transport rejects samesite="none" with secure=False; cross-site browser flows that need CookieTransportConfig.samesite="none" must therefore run over HTTPS and need explicit CSRF protection because browsers automatically attach those cookies to cross-origin requests.

For plugin-owned cookie auth, configure LitestarAuthConfig.csrf_secret; production validation fails closed when CookieTransport is used without it unless you explicitly opt into a non-browser or externally managed CSRF posture. The escape valves are narrow by design: CookieTransportConfig.allow_insecure_cookie_auth=True / CookieTransport.allow_insecure_cookie_auth for controlled non-browser cookie use, or manual route setups that set csrf_protection_managed_externally=True only when app-owned CSRF middleware protects unsafe methods. Use LitestarAuthConfig.csrf_header_name to align clients with the expected CSRF header.

Secrets at rest and key rotation

Persisted OAuth tokens, persisted TOTP secrets, pending TOTP enrollment secrets, and API-key signing-required raw secrets are secrets at rest. Configure their encryption fields before production: OAuthConfig.oauth_token_encryption_keyring or the one-key oauth_token_encryption_key; UserManagerSecurity.totp_secret_keyring or the one-key totp_secret_key; and ApiKeyConfig.secret_encryption_keyring, surfaced in plugin config as api_keys.secret_encryption_keyring. API-key signing stores the reversible ciphertext in api_key.encrypted_secret; bearer API keys remain digest-only and are not signing-secret rotation candidates.

Use FernetKeyringConfig(active_key_id=..., keys=...) for deployments that need no-downtime rotation. The caller-owned migration pattern is dual-key: deploy old and new keys together, switch active_key_id to the new key for fresh writes, re-encrypt stored rows one at a time, verify that no stored values still require re-encryption, and only then remove the old key id. The library provides row-level helpers such as OAuthTokenEncryption.requires_reencrypt(...) / OAuthTokenEncryption.reencrypt(...), BaseUserManager.totp_secret_requires_reencrypt(...) / BaseUserManager.reencrypt_totp_secret_for_storage(...), and BaseUserManager.api_key_signing_secret_requires_reencrypt(...) / BaseUserManager.reencrypt_api_key_signing_secret(...); it does not provide a global database sweep, locking strategy, batching job, audit-log table, or full Fernet key-rotation service. Those migration concerns remain application-owned until the library ships a built-in helper.

Privileged controllers and role administration

The contrib role-admin controller (create_role_admin_controller(...)) defaults to a single is_superuser guard when guards=None. An explicit guard sequence replaces that default verbatim — including an empty list, which leaves the role-administration endpoints with no authorization guard. The empty-override is intentional (it lets you compose a fully custom guard stack), but it is a footgun: never pass guards=[] to a privileged controller meaning "use the defaults". In production, either leave guards=None to keep the is_superuser default, or supply an explicit non-empty guard sequence that enforces at least equivalent privilege. The role-catalog invariants still fail closed regardless of guards — they refuse to modify the system-managed superuser role or remove the final superuser assignment — but those are integrity guards, not an authorization substitute.

DB refresh-session metadata

Session/device management is a DB-backed refresh-token feature. Before enabling include_session_devices=True in production, verify the active refresh-token table has the required metadata columns:

  • session_id — unique non-sensitive public id for the refresh session;
  • created_at — original row creation timestamp;
  • last_used_at — nullable timestamp updated on successful refresh rotation;
  • client_metadata — nullable bounded JSON for safe hints such as normalized User-Agent.

For bundled models, these fields come from RefreshTokenMixin. Existing installations must run an application-owned migration before rollout: add missing columns, backfill each existing row with a unique UUID-style session_id, leave last_used_at null for historical sessions, and only store bounded non-secret metadata. Also create the bundled refresh_token_consumed_digest table; it stores keyed consumed digests as the primary key plus indexed session_id values so replay detection does not scan the active refresh-token table. If you are upgrading from a version that populated the legacy consumed_token_digests JSON column, backfill those digest values into the lookup table before dropping the column and serving the new code. Custom refresh-token models must expose the same mapped attributes or DatabaseTokenModels validation fails at startup. Custom consumed-digest models supplied through DatabaseTokenModels.consumed_refresh_token_digest_model must expose mapped token_digest, session_id, and consumed_at attributes.

Do not migrate by copying raw token values into public metadata. The API exposes only session_id and safe metadata; raw access tokens, raw refresh tokens, stored token digests, and keyed token digests must remain server-side storage details. Stored consumed digests are not public metadata and must not be logged or returned by application APIs.

Versioned Fernet key rotation

Persisted OAuth provider tokens and TOTP secrets use the same versioned Fernet-at-rest envelope: fernet:v1:<key_id>:<ciphertext>. The key_id is not secret; it tells the library which configured Fernet key decrypts the value. Do not log or publish the ciphertext portion.

Generate a Fernet key with:

python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Configure production keyrings with named ids instead of treating one unversioned key as the long-term shape:

from litestar_auth import FernetKeyringConfig, OAuthConfig, UserManagerSecurity

oauth_token_keyring = FernetKeyringConfig(
    active_key_id="oauth_2026_04",
    keys={
        "oauth_2026_04": settings.oauth_fernet_key_2026_04,
    },
)
totp_secret_keyring = FernetKeyringConfig(
    active_key_id="totp_2026_04",
    keys={
        "totp_2026_04": settings.totp_fernet_key_2026_04,
    },
)

oauth_config = OAuthConfig(
    oauth_token_encryption_keyring=oauth_token_keyring,
    # other OAuth fields...
)
user_manager_security = UserManagerSecurity(
    totp_secret_keyring=totp_secret_keyring,
    # verification/reset/password fields...
)

No-downtime rotation is a staged data migration:

  1. Generate a new Fernet key and add it to the relevant keyring while keeping the current key id active. This proves every running instance can read both old and new key ids.
  2. Deploy the same key map with active_key_id changed to the new key id. New OAuth and TOTP writes store fernet:v1:<new_key_id>:...; old rows remain readable because the old key id is still configured.
  3. Run an application-owned migration job over stored OAuth access_token / refresh_token columns, stored TOTP secret columns, and API-key signing rows with non-null encrypted_secret. The library exposes row-level helpers, not an automatic database sweep: call OAuthTokenEncryption.requires_reencrypt(value) / OAuthTokenEncryption.reencrypt(value) for OAuth token values, BaseUserManager.totp_secret_requires_reencrypt(value) / BaseUserManager.reencrypt_totp_secret_for_storage(value) for TOTP values, and BaseUserManager.api_key_signing_secret_requires_reencrypt(row) / BaseUserManager.reencrypt_api_key_signing_secret(row_or_key_id) for one API-key signing row.
  4. Verify that a fresh scan finds no stored values requiring re-encryption. Treat unknown key ids, malformed values, or decryption failures as migration errors; do not skip them silently.
  5. Remove retired key ids only after all app instances run the new config and the verification scan is clean. Keeping retired keys indefinitely is not a rotation strategy.

Legacy unversioned Fernet rows such as fernet:<ciphertext> do not carry a key id and are not the normal runtime compatibility path. If you have them from an older release, migrate them with known knowledge of the old key material and rewrite them to fernet:v1:<key_id>:<ciphertext> before relying on the versioned keyring. Plaintext persisted TOTP secrets are still unsupported; clear those rows or encrypt them before production rollout.

API-key signing-secret rotation runbook

API-key request signing is the exception to the bearer-key digest-only model. Signing-required keys need the raw secret for HMAC verification, so the server stores an encrypted copy in api_key.encrypted_secret using api_keys.secret_encryption_keyring. That storage is reversible by design. Bearer API keys have only hashed_secret, cannot be converted to signing mode, and should be replaced with newly issued signing-required keys if a client needs request signing.

For no-downtime rotation:

  1. Deploy a keyring that includes both the retired id and the new id, while the old id is still active. Confirm every application instance can read existing signing rows.
  2. Deploy the same key map with active_key_id set to the new id. New signing-required API keys write fernet:v1:<new_key_id>:... envelopes.
  3. Run an operator-owned job that scans signing-required rows with non-null encrypted_secret, checks each row with BaseUserManager.api_key_signing_secret_requires_reencrypt(row), and calls await BaseUserManager.reencrypt_api_key_signing_secret(row_or_key_id) for rows that still use a non-active key id.
  4. Run the scan again and require a zero-candidate result before removing the retired key id from api_keys.secret_encryption_keyring.

The SQLAlchemy store exposes list_signing_keys_requiring_reencrypt(...) for candidate discovery, and custom stores should provide the same row-level behavior. Keep batching, locking, retries, and operator audit logs in application-owned migration tooling; the library does not own a global sweep or built-in audit-log table.

async def rotate_api_key_signing_secrets(manager: BaseUserManager, api_key_store: BaseApiKeyStore) -> int:
    rows = await api_key_store.list_signing_keys_requiring_reencrypt(
        manager.api_key_signing_secret_requires_reencrypt,
    )
    for row in rows:
        await manager.reencrypt_api_key_signing_secret(row)
    return len(rows)

Do not log raw API keys, encrypted_secret ciphertexts, decrypted signing secrets, or exception payloads from failed rotation. Unknown key ids, malformed envelopes, rows missing encrypted_secret, or bearer rows passed to the helper are fail-closed conditions that need data cleanup before old Fernet key ids are removed.

Use Redis-backed components when you run multiple workers or need durability:

  • JWT denylistRedisJWTDenylistStore instead of in-memory.
  • Shared auth surface — use litestar_auth.contrib.redis.RedisAuthPreset when one async Redis client should back auth rate limiting plus the TOTP stores. The maintained production recipe lives in Configuration; it wires build_rate_limit_config(), build_totp_enrollment_store(), build_totp_pending_jti_store(), and build_totp_used_tokens_store() from the public Redis contrib surface.
  • Distinct TOTP stores — keep totp_enrollment_store for pending enrollment secrets, totp_pending_jti_store for pending-login JWT replay prevention, and totp_used_tokens_store for consumed-code replay prevention, even when all three are derived from the same Redis client.
  • Low-level direct builders — keep AuthRateLimitConfig.from_shared_backend(RedisRateLimiter(...)) plus direct RedisTotpEnrollmentStore(...) / RedisJWTDenylistStore(...) / RedisUsedTotpCodeStore(...) construction when you intentionally need separate backends or bespoke key prefixes.

Use Configuration as the maintained source for the RedisAuthPreset flow, the AUTH_RATE_LIMIT_* helper exports, namespace families, migration recipe, fallback low-level builder/store APIs, and the litestar_auth.ratelimit versus litestar_auth.contrib.redis import split. Deployment adds the production requirement: those Redis-backed stores are the supported path once multiple workers or restarts matter.

The in-memory rate limiter, in-memory denylist, and in-memory TOTP stores are not sufficient across processes. With deployment_worker_count=None, the plugin may log startup warnings when in-memory rate limiting or in-memory TOTP state is detected outside tests. With deployment_worker_count > 1, process-local auth rate-limit endpoint backends fail closed at startup because per-worker counters cannot enforce one shared budget.

Rate limiting behavior

When rate_limit_config is set, throttled endpoints return 429 with Retry-After. Covered surfaces include login, register, forgot/reset password, change-password, refresh, verify / request-verify-token, and TOTP enable / confirm / verify / disable (see Rate limiting guide).

Public login, refresh, and registration controllers warn at assembly time when their matching AuthRateLimitConfig slot is missing. Treat that SecurityWarning as a production configuration gap unless an upstream proxy or another edge control owns throttling; unsafe_testing=True suppresses the warning only for controlled tests and local-only scaffolding.

OAuth

  • Set oauth_token_encryption_keyring for any configured providers, or the one-key oauth_token_encryption_key for a single active key.
  • Set oauth_flow_cookie_secret for any configured providers. This value is HKDF-derived into the Fernet key that protects the short-lived flow cookie containing OAuth state and the PKCE verifier; keep it high-entropy and distinct from every other auth secret.
  • Plugin-owned OAuth startup fails closed unless oauth_redirect_base_url uses a public https://... origin. Plain HTTP and loopback hosts are only supported behind local/test overrides such as AppConfig(debug=True) or unsafe_testing=True.
  • Manual/custom OAuth controllers require the same public https://... baseline for redirect_base_url, enforced at controller construction time with no localhost or plain-HTTP override.
  • The redirect_base_url host is additionally validated against non-routable / SSRF-adjacent ranges (loopback, RFC 1918 private, RFC 3927 link-local including the 169.254.169.254 cloud metadata endpoint, multicast, reserved, unspecified); hostnames are DNS-resolved once at validation time and checked the same way. This gate fails closed by default (oauth_redirect_dns_strict=True): DNS resolver failures and empty/unusable answers reject startup. It does not defend against DNS rebinding (the address resolved at validation time may differ at runtime), so still pair it with runtime egress controls on the OAuth callback path so a misconfigured or rebinding redirect_base_url cannot reach internal infrastructure.
  • Set oauth_redirect_dns_strict=False on plugin or manual OAuth configuration only when startup must tolerate unavailable DNS (offline or sandboxed environments); this restores the fail-open behavior where resolver failures are accepted and only resolvable internal-only answers are rejected.
  • oauth_associate_by_email: keep False unless you understand identity linking risk. If True on the plugin-owned route table, pair it with oauth_trust_provider_email_verified=True only for providers that cryptographically assert email ownership. Manual OAuth controllers use the lower-level trust_provider_email_verified=True flag instead (see OAuth guide).

Cookies

  • Keep oauth_cookie_secure=True (default) behind HTTPS.
  • Terminate only HTTPS in production, set HSTS at the edge, and keep CookieTransport.secure=True for browser sessions.
  • Serve TOTP enrollment (POST /auth/2fa/enable by default) only over HTTPS; that response includes the plaintext secret and otpauth URI needed to render the QR code.
  • For local HTTP dev you may relax cookie secure flags on transports — never in production.
  • Set trusted_proxy=True on rate-limit endpoints only when a trusted reverse proxy overwrites the configured forwarded headers. Otherwise, leave the default False so callers cannot spoof rate-limit keys.

Observability

  • Monitor 429 rates on auth endpoints (brute force / abuse).
  • Monitor REFRESH_SESSION_NOT_FOUND rates on session/device revoke routes. Spikes can indicate stale clients, repeated foreign-session guesses, or UI bugs sending the wrong public session_id.
  • Monitor SESSION_MANAGEMENT_UNSUPPORTED after configuration changes. It means session/device routes were enabled for a strategy that does not implement the DB refresh-session contract.
  • Log authentication failures without storing secrets or raw tokens. Failed-login identifier_digest is emitted only when login_identifier_telemetry_secret is configured; it is keyed and non-reversible, but still belongs in your privacy notice if you use it for abuse correlation.
  • Emails are normalized account identifiers. Built-in rate-limit keys use scoped PBKDF2-HMAC-SHA-256 digests for normalized identifiers before writing backend keys, but these digests are not a privacy boundary against brute-force guessing of common identifiers. Database encryption at rest and privacy disclosures remain application/operator responsibilities.
  • Send reset/verify emails through a queue or background worker and perform equivalent work for existing and non-existing accounts. Synchronous SMTP/API differences in on_after_forgot_password or on_after_request_verify_token can reintroduce account enumeration at the application boundary.

Testing vs production

  • See the testing guide for the plugin-backed pytest recipe.
  • unsafe_testing=True is a per-instance test-only override. Keep it out of local manual runs, staging, and production traffic.
  • Request-scoped DB-session sharing is still per HTTP request in tests. Separate login, refresh, authenticated, and logout requests each get their own request-local session.
  • Single-process testing conveniences such as in-memory JWT revocation, in-memory rate limiting, and relaxed TOTP store requirements do not become production-safe because unsafe_testing is enabled.

Documentation builds

Published docs should match the released package version. Build with just docs-build before tagging releases.