Security model¶
This page summarizes protections and conscious trade-offs shipped by the library.
Operator checklist¶
Before production rollout, verify the deployment-owned security contract in Deployment: reverse-proxy and trust boundaries, cookie transport security requirements, and secrets at rest and key rotation.
Implemented controls¶
- Passwords — hashing via
pwdlib; hash upgrade on login when parameters change. - Credential rotation — self-service password changes use
POST /users/me/change-passwordwith current-password re-verification. Self-service profile updates do not accept password changes, so stale profile-edit clients cannot rotate credentials without proving the current password; admin-initiated rotation remains privileged throughAdminUserUpdate. - Reset tokens — signed JWT-style reset tokens with password fingerprint so tokens die after password change. Library-issued verify and reset tokens include JOSE
typ=JWT; missing or unexpectedtypheaders are rejected before the normal signed decode. - JWT — library-issued access tokens include JOSE
typ=JWT, and access-token decode rejects missing or unexpectedtypheaders before signature, audience, issuer, algorithm, and required-claim validation. This is defense-in-depth against token-class confusion, not a substitute for those primary controls. Access-token validation also enforcesexp/iat/aud; optionaliss; a smallexp/nbfleeway for ordinary clock skew; andjtidenylist support (InMemoryJWTDenylistStore,RedisJWTDenylistStore) with an explicitJWTStrategy.revocation_posturecontract. The in-memory denylist prunes expired JTIs on each revoke and fails closed whenmax_entriesis reached with no reclaimable slots: it does not insert the new revocation and does not drop active revocations (useRedisJWTDenylistStoreor raise the cap for high revoke volume). Callers are not misled into thinking logout succeeded:JWTStrategy.destroy_tokenraisesTokenError, andAuthenticationBackend.logoutmaps that to HTTP 503 withTOKEN_PROCESSING_FAILEDfor API routes using the bundled exception handler. - Session fingerprint — optional claim on JWT tying tokens to current password/email state.
- Cookie auth — secure defaults (
HttpOnly,Secure,SameSite); CSRF for unsafe methods when wired (see Guides — Security). - TOTP — pending enrollment secrets stay server-side in
totp_enrollment_store; replay protection is enforced whentotp_used_tokens_storeis configured; production fails fast without required stores; persisted TOTP secrets require encrypted-at-rest storage throughBaseUserManager.totp_secret_storage_postureplusUserManagerSecurity.totp_secret_keyringor the one-keytotp_secret_key; recovery codes are 112-bit lowercase hex values stored as HMAC lookup digests mapped to Argon2 hashes; pending-login tokens are bound to hashed client IP and User-Agent fingerprints by default; successful app-code verification can record a short-lived server-side step-up marker for downstream sensitive operations. - OAuth — state and PKCE verifier evidence in a short-lived
HttpOnlyflow cookie encrypted/authenticated with a Fernet key HKDF-derived fromoauth_flow_cookie_secret; strict state validation; optional encryption at rest for provider tokens (oauth_token_encryption_keyringor one-keyoauth_token_encryption_key); OAuth token persistence accepts only current-moduleOAuthTokenEncryptionpolicies; write-time plaintext snapshots are restored after successful writes and cleared on rollback; guarded associate-by-email rules (oauth_trust_provider_email_verifiedon plugin-owned routes,trust_provider_email_verifiedon manual controllers, andoauth_associate_by_email); the associate authorize route is POST + CSRF-protected so a victim'sSameSite=Laxsession cookie cannot be abused by a cross-site top-level navigation to attach an attacker-controlled provider account to the victim's local user. Login authorize stays GET because anonymous OAuth login has no victim session to abuse. - Opaque DB tokens — keyed digest at rest; plugin-managed DB-token wiring uses
DatabaseTokenAuthConfigplusLitestarAuthConfig(..., database_token_auth=...). - API keys — opt-in user-owned credentials with digest-only bearer storage, one-time raw-secret
create responses, soft revocation, expiry, active-key caps, allowed-scope validation, and
route-time downscoping by current user roles when
scope_subset_check=True. Self-service list/read/create/update/revoke routes and superuser admin mint/list/revoke routes requirerequires_password_session, so API-key callers cannot enumerate or maintain API-key inventory or cross the password-session boundary. OptionalLSA1-HMAC-SHA256request signing adds timestamp skew and nonce replay checks and caps pre-auth body buffering withapi_keys.signed_body_max_bytes, but signing-required keys store an encrypted copy of the raw secret viaapi_keys.secret_encryption_keyring; this is a deliberate reversible-storage trade-off compared with bearer keys' digest-only storage. - Failed-login telemetry — failed-login logs never include the submitted email/username. Configure
UserManagerSecurity.login_identifier_telemetry_secretwhen you want a stable, non-reversibleidentifier_digest; the digest is omitted when that dedicated secret is unset. - Rate limiting — optional per-endpoint limits; in-memory backend is single-process only and fails closed for new keys when its capacity cap is reached.
- Route-level role checks —
is_superuser,has_any_role(...), andhas_all_roles(...)reuse the same normalized flat-role semantics as persistence and manager writes, and they fail closed if the authenticated user does not expose the documented role-capable contract. Role guard matching uses fixed-work internal comparisons over normalized role strings to avoid role-membership short-circuit predicates; this is a defense-in-depth posture, not a claim of cryptographic constant-time behavior across Python or the network.
Plugin-managed security posture paths¶
The plugin keeps these security-sensitive paths explicit and ties them to the same runtime posture contracts used by startup warnings and fail-closed validation where applicable:
| 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. |
Direct/manual posture contracts¶
When you assemble JWTStrategy or BaseUserManager yourself, inspect the runtime posture objects directly instead of inferring security behavior from constructor kwargs later:
JWTStrategy(secret=..., denylist_store=RedisJWTDenylistStore(...))reports the durableshared_storeposture.JWTStrategy(secret=..., allow_inmemory_denylist=True)reports the explicit process-localin_memoryposture.revocation_is_durablestaysFalseand logout / revoke remains single-process.- Plugin-managed JWT revocation notices consume the live
JWTRevocationPostureexported fromlitestar_auth.authentication.strategy.jwtand returned byJWTStrategy.revocation_posture; posture-shaped wrappers or objects retained from earlier module identities are ignored. BaseUserManager.totp_secret_storage_posturereports thefernet_encryptedpersisted-secret contract. Supplyingtotp_secret_keyring=FernetKeyringConfig(...)onUserManagerSecurity(...)lets direct/custom integrations store and read encrypted TOTP secrets with an active key id and configured old keys. The one-keytotp_secret_keyfield remains a deliberate ergonomic shortcut and is encoded under thedefaultkey id.- New persisted TOTP secret writes use
fernet:v1:<key_id>:<ciphertext>values.BaseUserManager.totp_secret_requires_reencrypt(...)andBaseUserManager.reencrypt_totp_secret_for_storage(...)are the manager helpers for explicit at-rest rotation jobs. - With
totp_secret_keyringandtotp_secret_keyomitted,Nonestill represents disabled 2FA, but non-null TOTP secret writes and unprefixed legacy plaintext reads fail closed. Encrypt, rotate, or clear existing plaintext TOTP secret rows before upgrading. - TOTP step-up for sensitive operations is documented in
TOTP step-up for sensitive operations,
including
totp_stepup_ttl_seconds,totp_stepup_policy,totp_stepup_allow_recovery, theTOTP_STEPUP_REQUIRED403 contract, default endpoint policies, recovery-code behavior, and the API-key transport rationale. BaseUserManagerusesUserManagerSecurity.login_identifier_telemetry_secretonly for failed-login identifier digests. It is optional; omitting it keeps logs correlation-safe by leavingidentifier_digestout rather than reusing another auth secret.
Secret-at-rest rotation¶
OAuth token encryption and TOTP secret encryption share the same versioned Fernet storage format:
fernet:v1:<key_id>:<ciphertext>. The key id is operational metadata, not secret material; the
ciphertext remains sensitive.
Use FernetKeyringConfig(active_key_id=..., keys=...) for production. During rotation, deploy a
keyring that contains both the old and new ids, switch active_key_id to the new id for writes, run
row-level re-encryption with OAuthTokenEncryption.requires_reencrypt(...) /
OAuthTokenEncryption.reencrypt(...) and BaseUserManager.totp_secret_requires_reencrypt(...) /
BaseUserManager.reencrypt_totp_secret_for_storage(...).
API-key signing secrets use the same Fernet envelope through
api_keys.secret_encryption_keyring, but only signing-required keys have reversible storage. Bearer
API keys remain digest-only, cannot be upgraded to signing mode, and are not rotation candidates.
For signing rows, scan rows where signing_required is true and encrypted_secret is non-null, call
BaseUserManager.api_key_signing_secret_requires_reencrypt(row), and rewrite one row at a time with
BaseUserManager.reencrypt_api_key_signing_secret(row_or_key_id). The helper returns the updated row
metadata, never the plaintext signing secret, and it does not invoke API-key create/revoke/use
lifecycle hooks. Verify no rows still require rotation, and only then remove the retired key id. The
full operator checklist lives in Deployment.
Unknown key ids, malformed Fernet envelopes, missing encrypted_secret values on signing rows, and
bearer rows must be treated as migration errors or explicit data-cleanup cases. Do not catch and
ignore those failures while removing an old key id.
Legacy unversioned Fernet values must be handled as explicit migration input because they do not identify the decrypting key. They are not a general runtime compatibility mode. Plaintext persisted TOTP secrets remain fail-closed and must be cleared or encrypted before production use.
Additional explicit opt-ins to weaker behavior:
| Surface | Risk |
|---|---|
totp_enable_requires_password=False |
Weakens step-up for TOTP enrollment. |
csrf_secret unset with plugin-owned cookie auth |
Plugin validation fails closed unless cookie auth explicitly opts out. |
csrf_protection_managed_externally=True on manual cookie auth |
You are asserting that app-owned CSRF middleware or an equivalent framework-level control protects the manual route table. |
CookieTransport(allow_insecure_cookie_auth=True) |
Allows cookie auth without CSRF for controlled non-browser scenarios only. |
ApiKeyConfig(signing_enabled=True, secret_encryption_keyring=...) |
Enables request signing, but stores an encrypted copy of signing-required key secrets so signatures can be verified. |
InMemoryApiKeyNonceStore |
Process-local API-key signing replay cache; use RedisApiKeyNonceStore for multi-worker deployments. |
Bearer failure-code taxonomy¶
Bearer API-key authentication returns the same outer response shape for unknown, revoked, and expired keys:
HTTP 401 with a JSON code value. The distinct API_KEY_INVALID, API_KEY_REVOKED, and
API_KEY_EXPIRED codes are a deliberate client-semantics trade-off, not an accidental disclosure
channel.
The bearer credential embeds a generated key_id with at least 128 bits of entropy before the raw
secret is verified against the stored HMAC digest. Blind enumeration of real key_id values is
therefore impractical under ordinary online attack constraints. Once a request names a valid
high-entropy key_id, separate codes let clients choose the correct remediation:
API_KEY_INVALID— the key id is unknown or the presented raw secret does not match; rotate or report the credential rather than retrying indefinitely.API_KEY_REVOKED— the key row is known but was intentionally disabled; stop using the credential and create a replacement through a password-backed session.API_KEY_EXPIRED— the key row is known but past its configured expiry; refresh the credential through normal key-management flows.
Do not depend on the specific code to grant access. All three outcomes are authentication failures and must remain non-authorizing.
Limitations (by design)¶
- No built-in email sending — you must implement hooks.
- No RBAC or WebAuthn in core — the built-in role guards are flat membership checks only; extend in your application for permission matrices or object-level policy.
- Durable JWT revocation requires an explicit shared store —
JWTStrategy(secret=...)withoutdenylist_storeorallow_inmemory_denylist=Truefails closed at construction time. Use Redis (or equivalent) denylist for multi-worker production if you rely on revoke; reserveallow_inmemory_denylist=Truefor single-process development or tests. - API keys are user-owned delegated credentials only. Service-account-only keys, HKDF child keys, IP allowlists, per-key audit tables, and mTLS binding are intentionally outside this release.
- API-key signing-secret rotation is operator-owned row processing. The library does not provide built-in batching, locking, audit-log storage, per-key audit tables, or an automatic database migration service for this workflow.
Further reading¶
- Guides — Security — CSRF, cookies, headers.
- Deployment — production checklist.
- Security and DI — CSRF, JWT/TOTP policy, dependency keys; OAuth token encryption is configured on OAuth.