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
jtidenylist, the auth rate-limit config,totp_enrollment_store,totp_pending_jti_store, andtotp_used_tokens_store. When one async Redis client should back auth rate limiting plus the TOTP stores, uselitestar_auth.contrib.redis.RedisAuthPresetas 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
PasswordHelpersharing,password_validator_factory, and theUserEmailField/UserPasswordFieldschema 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_secretandreset_password_token_secret— configure both throughuser_manager_security; each must satisfy the production strength gate enforced byvalidate_production_secret(32+ characters plus the default entropy floor).login_identifier_telemetry_secret— optional dedicated key for failed-loginidentifier_digestlog correlation. Configure it throughuser_manager_securityonly when you want stable cross-request digests; otherwise the digest field is omitted.totp_secret_keyring— configure throughuser_manager_securitywhen TOTP is enabled; required in production because stored TOTP secrets and pending-enrollment secrets must be encrypted at rest. UseFernetKeyringConfig(active_key_id=..., keys=...)so old key ids remain readable during rotation. The one-keytotp_secret_keyfield 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-keyoauth_token_encryption_keyfield 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 OAuthstateplus 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 inapi_key.encrypted_secret; bearer keys do not use it and remain digest-only.- Keep
verification_token_secret,reset_password_token_secret,login_identifier_telemetry_secretwhen configured,totp_pending_secret, every configured TOTP Fernet key, andoauth_flow_cookie_secretdistinct. Production configuration rejects reuse withConfigurationError; only explicitunsafe_testing=Truetest setups bypass this validation. For plugin-managed apps the error is raised duringLitestarAuth(config)validation; directBaseUserManager(...)construction enforces the same rule for its manager-owned secret roles. Distinct values are the supported posture:litestar-auth:verify,litestar-auth:reset-password, andlitestar-auth:2fa-pending/litestar-auth:2fa-enrollalready separate JWT audiences, while failed-login telemetry, TOTP Fernet keys, andoauth_flow_cookie_secretshould 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.
Cookie transport security requirements¶
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:
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:
- 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.
- Deploy the same key map with
active_key_idchanged to the new key id. New OAuth and TOTP writes storefernet:v1:<new_key_id>:...; old rows remain readable because the old key id is still configured. - Run an application-owned migration job over stored OAuth
access_token/refresh_tokencolumns, stored TOTP secret columns, and API-key signing rows with non-nullencrypted_secret. The library exposes row-level helpers, not an automatic database sweep: callOAuthTokenEncryption.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, andBaseUserManager.api_key_signing_secret_requires_reencrypt(row)/BaseUserManager.reencrypt_api_key_signing_secret(row_or_key_id)for one API-key signing row. - 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.
- 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:
- 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.
- Deploy the same key map with
active_key_idset to the new id. New signing-required API keys writefernet:v1:<new_key_id>:...envelopes. - Run an operator-owned job that scans signing-required rows with non-null
encrypted_secret, checks each row withBaseUserManager.api_key_signing_secret_requires_reencrypt(row), and callsawait BaseUserManager.reencrypt_api_key_signing_secret(row_or_key_id)for rows that still use a non-active key id. - 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.
Redis (recommended for scaled deployments)¶
Use Redis-backed components when you run multiple workers or need durability:
- JWT denylist —
RedisJWTDenylistStoreinstead of in-memory. - Shared auth surface — use
litestar_auth.contrib.redis.RedisAuthPresetwhen one async Redis client should back auth rate limiting plus the TOTP stores. The maintained production recipe lives in Configuration; it wiresbuild_rate_limit_config(),build_totp_enrollment_store(),build_totp_pending_jti_store(), andbuild_totp_used_tokens_store()from the public Redis contrib surface. - Distinct TOTP stores — keep
totp_enrollment_storefor pending enrollment secrets,totp_pending_jti_storefor pending-login JWT replay prevention, andtotp_used_tokens_storefor 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 directRedisTotpEnrollmentStore(...)/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_keyringfor any configured providers, or the one-keyoauth_token_encryption_keyfor a single active key. - Set
oauth_flow_cookie_secretfor 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_urluses a publichttps://...origin. Plain HTTP and loopback hosts are only supported behind local/test overrides such asAppConfig(debug=True)orunsafe_testing=True. - Manual/custom OAuth controllers require the same public
https://...baseline forredirect_base_url, enforced at controller construction time with no localhost or plain-HTTP override. - The
redirect_base_urlhost is additionally validated against non-routable / SSRF-adjacent ranges (loopback, RFC 1918 private, RFC 3927 link-local including the169.254.169.254cloud 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 rebindingredirect_base_urlcannot reach internal infrastructure. - Set
oauth_redirect_dns_strict=Falseon 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: keepFalseunless you understand identity linking risk. IfTrueon the plugin-owned route table, pair it withoauth_trust_provider_email_verified=Trueonly for providers that cryptographically assert email ownership. Manual OAuth controllers use the lower-leveltrust_provider_email_verified=Trueflag 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=Truefor browser sessions. - Serve TOTP enrollment (
POST /auth/2fa/enableby 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
secureflags on transports — never in production. - Set
trusted_proxy=Trueon rate-limit endpoints only when a trusted reverse proxy overwrites the configured forwarded headers. Otherwise, leave the defaultFalseso callers cannot spoof rate-limit keys.
Observability¶
- Monitor 429 rates on auth endpoints (brute force / abuse).
- Monitor
REFRESH_SESSION_NOT_FOUNDrates on session/device revoke routes. Spikes can indicate stale clients, repeated foreign-session guesses, or UI bugs sending the wrong publicsession_id. - Monitor
SESSION_MANAGEMENT_UNSUPPORTEDafter 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_digestis emitted only whenlogin_identifier_telemetry_secretis 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_passwordoron_after_request_verify_tokencan reintroduce account enumeration at the application boundary.
Testing vs production¶
- See the testing guide for the plugin-backed pytest recipe.
unsafe_testing=Trueis 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_testingis enabled.
Documentation builds¶
Published docs should match the released package version. Build with just docs-build before tagging releases.