Skip to content

Strategies

Token strategies validate or issue credentials and pair with transports inside an AuthenticationBackend (see Backends: transports and strategies). Three concrete implementations ship with litestar-auth:

  • JWTStrategy issues and verifies stateless signed JWTs with your configured signing keys. Library-issued access tokens include JOSE typ=JWT; decode rejects tokens with a missing or unexpected typ header before the normal signed validation. This header check is defense-in-depth against token-class confusion and does not replace signature, algorithm allowlist, audience, issuer, or required-claim validation. Use this strategy when you want bearer or cookie flows without storing each access token in a database or Redis—scaling and rotation are typically driven by expiry and refresh semantics rather than per-token rows. Use JWTStrategyConfig(...) when you want the signing, validation, revocation, lifetime, and session-fingerprint settings carried as one typed object. The default session-fingerprint HMAC key is HKDF-SHA256-derived from the signing secret with a JWT fingerprint domain; tokens minted by older versions with raw-secret fingerprints are intentionally rejected and users must re-login once after deployment.

  • DatabaseTokenStrategy stores opaque tokens in your application database (hashed at rest). Use it when you need durable revocation, per-token metadata, refresh-session/device listing, or audit trails aligned with your ORM models. Use DatabaseTokenStrategyConfig(...) when the session, token models, token hash secret, access lifetime, refresh lifetime, and token-size settings should travel together.

  • RedisTokenStrategy keeps opaque token state in Redis with TTL-backed keys and a per-user token index. Use it when you want fast invalidation and shared token state across app instances without adding DB round-trips for every validation. invalidate_all_tokens(...) atomically bumps a per-user invalidation epoch and deletes indexed token and TOTP step-up marker keys. Token reads validate that epoch, so orphaned token keys missing from the per-user index are rejected on their next use after invalidation without a keyspace scan. The per-user index key hashes the serialized user id before adding it to the Redis key, so custom id values cannot inject key delimiters or reshape the namespace. You can construct it with RedisTokenStrategyConfig(...) when you want the Redis client, hash secret, TTL, key prefix, token byte count, and optional subject decoder carried as one typed settings object.

  • ApiKeyStrategy verifies user-owned API keys against a BaseApiKeyStore. Configure it with ApiKeyStrategyConfig or equivalent keyword arguments. Bearer keys compare the presented secret with the stored HMAC digest. Signing-required keys use the encrypted stored secret to verify LSA1-HMAC-SHA256 normalized request signatures, validate X-Auth-Date within signing_skew_seconds, and reject replayed X-Auth-Nonce values through an ApiKeyNonceStore. Successful reads return ApiKeyAuthenticationResult with the resolved user and ApiKeyContext; middleware exposes that context as request.auth.

For plugin-oriented setup, DatabaseTokenAuthConfig on LitestarAuthConfig is the direct shortcut for wiring opaque database-backed tokens (hash secret, optional backend naming, and related compatibility flags) without hand-assembling the strategy and related pieces in isolation. Full wiring for the preset, route flags, and related options is covered in Backends; ORM mixins, token tables, and SQLAlchemyUserDatabase contracts are covered in User and manager.

Refresh-session management support

The session/device HTTP API is backed by a strategy protocol rather than by controller-side database queries. DatabaseTokenStrategy implements that protocol and can:

  • list the authenticated user's active, non-expired refresh sessions;
  • revoke one current-user session by public session_id;
  • revoke all other current-user sessions, preserving the current session when the current refresh credential can be identified;
  • identify a public session_id from a raw refresh token by hashing the supplied value and comparing it with stored digests;
  • record consumed refresh-token digests during rotation and revoke the whole refresh-session chain when a consumed token is presented again.

JWTStrategy and RedisTokenStrategy do not currently provide the session/device dashboard contract. If plugin-owned session/device routes are enabled against an unsupported strategy, the route returns 400 with SESSION_MANAGEMENT_UNSUPPORTED; it does not synthesize empty session data. The API never returns raw tokens, access tokens, refresh tokens, stored token digests, or keyed token digests.

litestar_auth.authentication.strategy

Issue, validate, rotate, and revoke tokens (JWT, database, or Redis).

Strategies pair with :mod:litestar_auth.authentication.transport implementations inside :class:~litestar_auth.authentication.backend.AuthenticationBackend.

DatabaseTokenModels is the explicit contract for DatabaseTokenStrategy when you swap in mixin-composed token ORM classes. The explicit bundled-token bootstrap helper lives at litestar_auth.models.import_token_orm_models().

ApiKeyContext(key_id, scopes, prefix_env, scope_subset_check=True, scope_authority=None) dataclass

Authentication context exposed as request.auth for API-key requests.

ApiKeyNonceStore

Bases: Protocol

Persistence contract for API-key signing nonces.

mark_used(*, key_id, nonce, ttl_seconds) async

Record nonce for key_id if it was not already seen.

Source code in litestar_auth/authentication/strategy/_api_key_nonce_store.py
async def mark_used(self, *, key_id: str, nonce: str, ttl_seconds: int) -> ApiKeyNonceStoreResult:
    """Record ``nonce`` for ``key_id`` if it was not already seen."""

ApiKeyNonceStoreResult(stored, rejected_as_replay=False) dataclass

Outcome of recording a request-signing nonce.

ApiKeyStrategy(*, config=None, **options)

ApiKeyStrategy(*, config: ApiKeyStrategyConfig)
ApiKeyStrategy(**options: Unpack[ApiKeyStrategyOptions])

Bases: Strategy[UP, ID]

Verify API-key credentials against indexed persisted key rows.

Initialize the API-key strategy.

Raises:

Type Description
ValueError

If config and keyword options are combined.

ConfigurationError

If api_key_hash_secret is not production-safe.

Source code in litestar_auth/authentication/strategy/api_key.py
def __init__(
    self,
    *,
    config: ApiKeyStrategyConfig | None = None,
    **options: Unpack[ApiKeyStrategyOptions],
) -> None:
    """Initialize the API-key strategy.

    Raises:
        ValueError: If ``config`` and keyword options are combined.
        ConfigurationError: If ``api_key_hash_secret`` is not production-safe.
    """
    if config is not None and options:
        msg = "Pass either ApiKeyStrategyConfig or keyword options, not both."
        raise ValueError(msg)
    settings = ApiKeyStrategyConfig(**options) if config is None else config
    try:
        validate_production_secret(
            settings.api_key_hash_secret,
            label="ApiKeyStrategy api_key_hash_secret",
            unsafe_testing=settings.unsafe_testing,
        )
    except ConfigurationError as exc:
        raise ConfigurationError(str(exc)) from exc

    self.api_key_store = settings.api_key_store
    self._api_key_hash_secret = settings.api_key_hash_secret.encode()
    self.prefix_env = settings.prefix_env
    self.prefix = settings.prefix
    self.scope_subset_check = settings.scope_subset_check
    self.scope_authority = settings.scope_authority
    self.signing_skew_seconds = settings.signing_skew_seconds
    self.nonce_store = settings.nonce_store
    self.secret_encryption_keyring = settings.secret_encryption_keyring
    self.unsafe_testing = settings.unsafe_testing

classify_failure_code(token) async

Return the most specific API-key authentication failure code for token.

Source code in litestar_auth/authentication/strategy/api_key.py
async def classify_failure_code(self, token: str | None) -> ErrorCode:
    """Return the most specific API-key authentication failure code for ``token``."""
    return api_key_failure_reason_to_error_code(await self.classify_failure_reason(token))

classify_failure_reason(token) async

Return the most specific API-key authentication failure reason for token.

Source code in litestar_auth/authentication/strategy/api_key.py
async def classify_failure_reason(self, token: str | None) -> ApiKeyFailureReason:
    """Return the most specific API-key authentication failure reason for ``token``."""
    if token == API_KEY_HMAC_SCHEME:
        return await self._classify_signed_failure_reason()
    return await self._classify_bearer_failure_reason(token)

destroy_token(token, user) async

Do nothing because API-key revocation is handled by API-key management flows.

Source code in litestar_auth/authentication/strategy/api_key.py
@override
async def destroy_token(self, token: str, user: UP) -> None:
    """Do nothing because API-key revocation is handled by API-key management flows."""

read_token(token, user_manager) async

Resolve a user from an API-key token.

Returns:

Type Description
UP | None

Resolved user, or None when verification fails.

Source code in litestar_auth/authentication/strategy/api_key.py
@override
async def read_token(self, token: str | None, user_manager: object) -> UP | None:
    """Resolve a user from an API-key token.

    Returns:
        Resolved user, or ``None`` when verification fails.
    """
    # StrategyProtocol accepts object for pluggability; this concrete strategy requires the user-manager contract.
    result = await self.read_token_with_context(
        token,
        user_manager=cast("UserManagerProtocol[UP, ID]", user_manager),
    )
    return None if result is None else result.user

read_token_attempt(token, user_manager) async

Resolve an API-key request and preserve the failed reason when rejected.

Returns:

Type Description
ApiKeyAuthenticationAttempt[UP]

Successful authentication result, or None plus a typed failure reason.

Source code in litestar_auth/authentication/strategy/api_key.py
async def read_token_attempt(
    self,
    token: str | None,
    user_manager: UserManagerProtocol[UP, ID],
) -> ApiKeyAuthenticationAttempt[UP]:
    """Resolve an API-key request and preserve the failed reason when rejected.

    Returns:
        Successful authentication result, or ``None`` plus a typed failure reason.
    """
    if token is None:
        return _api_key_failure(ApiKeyFailureReason.INVALID)
    if token == API_KEY_HMAC_SCHEME:
        return await self._read_signed_request(user_manager)
    return await self._read_bearer_api_key(token, user_manager)

read_token_with_context(token, user_manager) async

Resolve a user and API-key context from a canonical API-key token.

Returns:

Type Description
ApiKeyAuthenticationResult[UP] | None

Resolved user and API-key context, or None when verification fails.

Source code in litestar_auth/authentication/strategy/api_key.py
async def read_token_with_context(
    self,
    token: str | None,
    user_manager: UserManagerProtocol[UP, ID],
) -> ApiKeyAuthenticationResult[UP] | None:
    """Resolve a user and API-key context from a canonical API-key token.

    Returns:
        Resolved user and API-key context, or ``None`` when verification fails.
    """
    return (await self.read_token_attempt(token, user_manager)).result

write_token(user) async

Reject login-token issuance because API keys are manager-issued credentials.

Raises:

Type Description
TokenError

Always, because API keys are not login-flow tokens.

Source code in litestar_auth/authentication/strategy/api_key.py
@override
async def write_token(self, user: UP) -> str:
    """Reject login-token issuance because API keys are manager-issued credentials.

    Raises:
        TokenError: Always, because API keys are not login-flow tokens.
    """
    msg = "ApiKeyStrategy does not issue login tokens."
    raise TokenError(msg)

ApiKeyStrategyConfig(api_key_store, api_key_hash_secret, prefix_env=None, prefix=API_KEY_PREFIX, scope_subset_check=True, scope_authority=None, signing_skew_seconds=300, nonce_store=None, secret_encryption_keyring=None, unsafe_testing=False) dataclass

Configuration for :class:ApiKeyStrategy.

ContextualStrategy

Bases: Protocol

Protocol for strategies that return custom request auth context.

read_token_with_context(token, user_manager) async

Resolve a user plus strategy-specific authentication context.

Source code in litestar_auth/authentication/strategy/base.py
async def read_token_with_context(
    self,
    token: str | None,
    user_manager: UserManagerProtocol[UP, ID],
) -> AuthT | None:
    """Resolve a user plus strategy-specific authentication context."""

DatabaseTokenModels(access_token_model=AccessToken, refresh_token_model=RefreshToken, consumed_refresh_token_digest_model=RefreshTokenConsumedDigest) dataclass

Explicit token ORM contract for DatabaseTokenStrategy.

The supplied access-token model must expose mapped token, created_at, user_id, and user attributes compatible with the persistence operations performed by the DB token strategy. The supplied refresh-token model must also expose session_id, last_used_at, and client_metadata so DB-backed refresh sessions have a non-sensitive public session identifier and bounded client metadata. The consumed refresh-token digest model must expose mapped token_digest, session_id, and consumed_at attributes for replay detection. Defaults preserve the bundled token-table behavior.

__post_init__()

Validate the supplied token-model classes eagerly.

Source code in litestar_auth/authentication/strategy/db_models.py
def __post_init__(self) -> None:
    """Validate the supplied token-model classes eagerly."""
    _validate_token_model_contract(
        self.access_token_model,
        field_name="access_token_model",
        required_attributes=_REQUIRED_ACCESS_TOKEN_MODEL_ATTRIBUTES,
    )
    _validate_token_model_contract(
        self.refresh_token_model,
        field_name="refresh_token_model",
        required_attributes=_REQUIRED_REFRESH_TOKEN_MODEL_ATTRIBUTES,
    )
    _validate_token_model_contract(
        self.consumed_refresh_token_digest_model,
        field_name="consumed_refresh_token_digest_model",
        required_attributes=_REQUIRED_CONSUMED_DIGEST_MODEL_ATTRIBUTES,
    )

DatabaseTokenStrategy(*, config=None, **options)

DatabaseTokenStrategy(*, config: DatabaseTokenStrategyConfig)
DatabaseTokenStrategy(**options: Unpack[DatabaseTokenStrategyOptions])

Bases: _DatabaseRefreshSessionMixin[UP, ID], Strategy[UP, ID], RefreshableStrategy[UP, ID]

Stateful strategy that persists opaque tokens in the database.

Initialize the strategy.

Parameters:

Name Type Description Default
config DatabaseTokenStrategyConfig | None

Database-token strategy configuration.

None
**options Unpack[DatabaseTokenStrategyOptions]

Individual database-token strategy settings. Do not combine with config.

{}

Raises:

Type Description
ValueError

If config and keyword options are combined.

ConfigurationError

When token_hash_secret fails minimum-length requirements.

Source code in litestar_auth/authentication/strategy/db.py
def __init__(
    self,
    *,
    config: DatabaseTokenStrategyConfig | None = None,
    **options: Unpack[DatabaseTokenStrategyOptions],
) -> None:
    """Initialize the strategy.

    Args:
        config: Database-token strategy configuration.
        **options: Individual database-token strategy settings. Do not combine
            with ``config``.

    Raises:
        ValueError: If ``config`` and keyword options are combined.
        ConfigurationError: When ``token_hash_secret`` fails minimum-length requirements.
    """
    if config is not None and options:
        msg = "Pass either DatabaseTokenStrategyConfig or keyword options, not both."
        raise ValueError(msg)
    settings = DatabaseTokenStrategyConfig(**options) if config is None else config
    try:
        validate_production_secret(settings.token_hash_secret, label="DatabaseTokenStrategy token_hash_secret")
    except ConfigurationError as exc:
        raise ConfigurationError(str(exc)) from exc
    validate_token_bytes(settings.token_bytes, label="DatabaseTokenStrategy")

    self.session = settings.session
    self._token_hash_secret = settings.token_hash_secret.encode()
    self.token_models = DatabaseTokenModels() if settings.token_models is None else settings.token_models
    self.access_token_model = self.token_models.access_token_model
    self.refresh_token_model = self.token_models.refresh_token_model
    self.consumed_refresh_token_digest_model = self.token_models.consumed_refresh_token_digest_model
    self._access_token_repository_type = build_token_repository(self.access_token_model)
    self._refresh_token_repository_type = build_token_repository(self.refresh_token_model)
    self.max_age = settings.max_age
    self.refresh_max_age = settings.refresh_max_age
    self.token_bytes = settings.token_bytes
    self.unsafe_testing = settings.unsafe_testing
    self._refresh_token_request_metadata: dict[str, str] | None = None

cleanup_expired_tokens(session) async

Delete expired access and refresh tokens for the configured TTLs.

Returns:

Type Description
int

Total number of deleted access-token and refresh-token rows.

Source code in litestar_auth/authentication/strategy/db.py
async def cleanup_expired_tokens(self, session: AsyncSession) -> int:
    """Delete expired access and refresh tokens for the configured TTLs.

    Returns:
        Total number of deleted access-token and refresh-token rows.
    """
    now = datetime.now(tz=UTC)
    access_cutoff = now - self.max_age
    refresh_cutoff = now - self.refresh_max_age

    access_result = await session.execute(
        delete(self.access_token_model).where(self.access_token_model.created_at <= access_cutoff),
    )
    expired_refresh_session_ids = select(self.refresh_token_model.session_id).where(
        self.refresh_token_model.created_at <= refresh_cutoff,
    )
    await session.execute(
        delete(self.consumed_refresh_token_digest_model).where(
            self.consumed_refresh_token_digest_model.session_id.in_(expired_refresh_session_ids),
        ),
    )
    await session.execute(
        delete(self.consumed_refresh_token_digest_model).where(
            self.consumed_refresh_token_digest_model.consumed_at <= refresh_cutoff,
        ),
    )
    refresh_result = await session.execute(
        delete(self.refresh_token_model).where(self.refresh_token_model.created_at <= refresh_cutoff),
    )
    await session.commit()

    access_rowcount = getattr(access_result, "rowcount", 0) or 0
    refresh_rowcount = getattr(refresh_result, "rowcount", 0) or 0
    return access_rowcount + refresh_rowcount

destroy_token(token, user) async

Delete a persisted token.

Source code in litestar_auth/authentication/strategy/db.py
@override
async def destroy_token(self, token: str, user: UP) -> None:
    """Delete a persisted token."""
    token_digest = self._token_digest(token)
    await self._repository(self._access_token_repository_type).delete_where(token=token_digest, auto_commit=False)
    await self.session.commit()

invalidate_all_tokens(user) async

Delete all persisted access and refresh tokens for the given user.

Source code in litestar_auth/authentication/strategy/db.py
async def invalidate_all_tokens(self, user: UP) -> None:
    """Delete all persisted access and refresh tokens for the given user."""
    await self._repository(self._access_token_repository_type).delete_where(user_id=user.id, auto_commit=False)
    await self._delete_refresh_session_consumed_digests(
        select(self.refresh_token_model.session_id).where(self.refresh_token_model.user_id == user.id),
    )
    await self._repository(self._refresh_token_repository_type).delete_where(user_id=user.id, auto_commit=False)
    await self.session.commit()

read_token(token, user_manager) async

Resolve a user from an opaque database token.

Returns:

Type Description
UP | None

Related user when the token exists and is not expired, otherwise None.

Source code in litestar_auth/authentication/strategy/db.py
@override
async def read_token(
    self,
    token: str | None,
    user_manager: UserManagerProtocol[UP, ID],
) -> UP | None:
    """Resolve a user from an opaque database token.

    Returns:
        Related user when the token exists and is not expired, otherwise ``None``.
    """
    if token is None:
        return None

    access_token = await self._resolve_access_token(token)
    if access_token is None or self._is_token_expired(access_token.created_at, self.max_age):
        return None

    return access_token.user

with_session(session)

Return a copy of the strategy bound to the provided async session.

Source code in litestar_auth/authentication/strategy/db.py
def with_session(self, session: AsyncSessionT) -> DatabaseTokenStrategy[UP, ID]:
    """Return a copy of the strategy bound to the provided async session."""
    return type(self)(
        session=session,
        token_hash_secret=self._token_hash_secret.decode(),
        token_models=self.token_models,
        max_age=self.max_age,
        refresh_max_age=self.refresh_max_age,
        token_bytes=self.token_bytes,
        unsafe_testing=self.unsafe_testing,
    )

write_refresh_token(user) async

Persist and return a new opaque refresh token for the user.

Returns:

Type Description
str

Newly created opaque refresh-token string.

Source code in litestar_auth/authentication/strategy/db.py
@override
async def write_refresh_token(self, user: UP) -> str:
    """Persist and return a new opaque refresh token for the user.

    Returns:
        Newly created opaque refresh-token string.
    """
    token, token_digest = mint_opaque_token(token_bytes=self.token_bytes, token_hash_secret=self._token_hash_secret)
    refresh_token = self.refresh_token_model(
        token=token_digest,
        user_id=user.id,
        client_metadata=self._consume_refresh_token_request_metadata(),
    )
    await self._repository(self._refresh_token_repository_type).add(refresh_token, auto_refresh=True)
    return token

write_token(user) async

Persist and return a new opaque token for the user.

Returns:

Type Description
str

Newly created opaque token string.

Source code in litestar_auth/authentication/strategy/db.py
@override
async def write_token(self, user: UP) -> str:
    """Persist and return a new opaque token for the user.

    Returns:
        Newly created opaque token string.
    """
    token, token_digest = mint_opaque_token(token_bytes=self.token_bytes, token_hash_secret=self._token_hash_secret)
    access_token = self.access_token_model(token=token_digest, user_id=user.id)
    await self._repository(self._access_token_repository_type).add(access_token, auto_refresh=True)
    return token

DatabaseTokenStrategyConfig(session, token_hash_secret, token_models=None, max_age=DEFAULT_MAX_AGE, refresh_max_age=DEFAULT_REFRESH_MAX_AGE, token_bytes=DEFAULT_TOKEN_BYTES, unsafe_testing=False) dataclass

Configuration for :class:DatabaseTokenStrategy.

InMemoryApiKeyNonceStore(*, clock=time.monotonic, max_entries=50000)

Async-safe process-local API-key signing nonce store.

Initialize an empty nonce cache.

Raises:

Type Description
ValueError

If max_entries is less than one.

Source code in litestar_auth/authentication/strategy/_api_key_nonce_store.py
def __init__(self, *, clock: Clock = time.monotonic, max_entries: int = 50_000) -> None:
    """Initialize an empty nonce cache.

    Raises:
        ValueError: If ``max_entries`` is less than one.
    """
    if max_entries < 1:
        msg = "max_entries must be at least 1."
        raise ValueError(msg)
    self._clock = clock
    self.max_entries = max_entries
    self._entries: dict[tuple[str, str], float] = {}
    self._lock = asyncio.Lock()

is_shared_across_workers property

In-memory state is process-local.

mark_used(*, key_id, nonce, ttl_seconds) async

Record a nonce until TTL expiry, rejecting replays fail-closed.

Returns:

Type Description
ApiKeyNonceStoreResult

Stored/replay outcome for the nonce insert.

Source code in litestar_auth/authentication/strategy/_api_key_nonce_store.py
async def mark_used(self, *, key_id: str, nonce: str, ttl_seconds: int) -> ApiKeyNonceStoreResult:
    """Record a nonce until TTL expiry, rejecting replays fail-closed.

    Returns:
        Stored/replay outcome for the nonce insert.
    """
    async with self._lock:
        now = read_clock(self._clock)
        self._prune(now)
        key = (key_id, nonce)
        if key in self._entries:
            return ApiKeyNonceStoreResult(stored=False, rejected_as_replay=True)
        if len(self._entries) >= self.max_entries:
            return ApiKeyNonceStoreResult(stored=False, rejected_as_replay=False)
        self._entries[key] = now + ttl_seconds
        return ApiKeyNonceStoreResult(stored=True)

JWTContext(organization=None) dataclass

Authentication context exposed as request.auth for JWT requests.

JWTStrategy(*, config=None, **options)

JWTStrategy(*, config: JWTStrategyConfig[UP, ID])
JWTStrategy(**options: Unpack[JWTStrategyOptions[UP, ID]])

Bases: Strategy[UP, ID]

Stateless strategy that stores user identifiers inside JWTs.

JWT access tokens issued by this strategy are designed to be short-lived and stateless. Revocation uses the configured denylist keyed by the jti claim so individual tokens can be explicitly revoked before expiration when :meth:destroy_token is called.

Production deployments should pass a shared denylist store such as :class:RedisJWTDenylistStore. Single-process tests, development apps, and consciously single-process deployments can opt into :class:InMemoryJWTDenylistStore with allow_inmemory_denylist=True. Inspect :attr:revocation_posture to determine whether a concrete strategy instance uses process-local or durable shared-store revocation.

Initialize the JWT strategy.

Parameters:

Name Type Description Default
config JWTStrategyConfig[UP, ID] | None

JWT strategy configuration.

None
**options Unpack[JWTStrategyOptions[UP, ID]]

Individual JWT strategy settings. Do not combine with config.

{}
Source code in litestar_auth/authentication/strategy/jwt.py
def __init__(
    self,
    *,
    config: JWTStrategyConfig[UP, ID] | None = None,
    **options: Unpack[JWTStrategyOptions[UP, ID]],
) -> None:
    """Initialize the JWT strategy.

    Args:
        config: JWT strategy configuration.
        **options: Individual JWT strategy settings. Do not combine with
            ``config``.
    """
    settings = _resolve_jwt_strategy_config(config=config, options=options)
    _validate_jwt_algorithm_settings(settings)
    self._apply_settings(settings)

revocation_is_durable property

Return whether token revocation is backed by a shared store.

revocation_posture property

Return the explicit revocation durability contract for this strategy.

destroy_token(token, user) async

Revoke the given token by adding its jti to the configured denylist.

Tokens without a jti claim, or tokens that fail to decode, are ignored.

Raises:

Type Description
TokenError

When the denylist refuses a new revocation (for example, the compatibility in-memory store is at max_entries with no reclaimable slots).

Source code in litestar_auth/authentication/strategy/jwt.py
@override
async def destroy_token(self, token: str, user: UP) -> None:
    """Revoke the given token by adding its ``jti`` to the configured denylist.

    Tokens without a ``jti`` claim, or tokens that fail to decode, are ignored.

    Raises:
        TokenError: When the denylist refuses a new revocation (for example, the
            compatibility in-memory store is at ``max_entries`` with no reclaimable slots).
    """
    try:
        payload = decode_signed_jwt(
            token,
            config=JwtDecodeConfig(
                key=self.verify_key,
                algorithms=[self.algorithm],
                audience=JWT_ACCESS_TOKEN_AUDIENCE,
                options={"verify_exp": False},
                issuer=self.issuer,
            ),
        )
    except InvalidTokenError:
        return

    jti = payload.get("jti")
    exp = payload.get("exp")
    if not isinstance(jti, str):
        return
    ttl_seconds = denylist_ttl_seconds(exp)
    recorded = await self._denylist_store.deny(jti, ttl_seconds=ttl_seconds)
    if not recorded:
        msg = (
            "Could not record JWT revocation in the denylist (in-memory store at capacity). "
            "Use RedisJWTDenylistStore or increase max_entries."
        )
        raise TokenError(msg)

read_token(token, user_manager) async

Decode a JWT token and load its user.

Returns:

Type Description
UP | None

The matching user, or None when the token is invalid.

Source code in litestar_auth/authentication/strategy/jwt.py
@override
async def read_token(
    self,
    token: str | None,
    user_manager: UserManagerProtocol[UP, ID],
) -> UP | None:
    """Decode a JWT token and load its user.

    Returns:
        The matching user, or ``None`` when the token is invalid.
    """
    result = await self._read_verified_user_and_payload(token, user_manager)
    return None if result is None else result[0]

read_token_with_context(token, user_manager) async

Decode a JWT token and expose verified organization claim context.

Returns:

Type Description
JWTAuthenticationResult[UP] | None

Resolved user plus optional organization context, or None when

JWTAuthenticationResult[UP] | None

the token does not authenticate.

Source code in litestar_auth/authentication/strategy/jwt.py
async def read_token_with_context(
    self,
    token: str | None,
    user_manager: UserManagerProtocol[UP, ID],
) -> JWTAuthenticationResult[UP] | None:
    """Decode a JWT token and expose verified organization claim context.

    Returns:
        Resolved user plus optional organization context, or ``None`` when
        the token does not authenticate.
    """
    result = await self._read_verified_user_and_payload(token, user_manager)
    if result is None:
        return None
    user, payload = result

    return JWTAuthenticationResult(user=user, context=JWTContext(organization=_organization_from_payload(payload)))

write_token(user) async

Generate an organization-free JWT token for the provided user.

Returns:

Type Description
str

The encoded JWT token string.

Source code in litestar_auth/authentication/strategy/jwt.py
@override
async def write_token(self, user: UP) -> str:
    """Generate an organization-free JWT token for the provided user.

    Returns:
        The encoded JWT token string.
    """
    return self._build_access_token(user)

write_token_for_organization(user, organization) async

Generate a JWT token bound to a verified active organization.

Callers must verify that user is a member of organization before invoking this method. The strategy only normalizes and signs the organization claim; it performs no membership lookup itself.

Returns:

Type Description
str

The encoded JWT token string carrying the normalized organization claim.

Source code in litestar_auth/authentication/strategy/jwt.py
async def write_token_for_organization(self, user: UP, organization: str) -> str:
    """Generate a JWT token bound to a verified active organization.

    Callers must verify that ``user`` is a member of ``organization`` before
    invoking this method. The strategy only normalizes and signs the
    organization claim; it performs no membership lookup itself.

    Returns:
        The encoded JWT token string carrying the normalized organization claim.
    """
    return self._build_access_token(user, organization=organization)

JWTStrategyConfig(secret, verify_key=None, algorithm=DEFAULT_ALGORITHM, lifetime=DEFAULT_LIFETIME, subject_decoder=None, issuer=None, denylist_store=None, allow_inmemory_denylist=False, session_fingerprint_getter=None, session_fingerprint_claim='sfp') dataclass

Configuration for :class:JWTStrategy.

RedisApiKeyNonceStore(*, redis, key_prefix=DEFAULT_API_KEY_NONCE_PREFIX)

Redis-backed API-key signing nonce store.

Store the Redis client and key namespace.

Source code in litestar_auth/authentication/strategy/_api_key_nonce_store.py
def __init__(
    self,
    *,
    redis: RedisApiKeyNonceStoreClient,
    key_prefix: str = DEFAULT_API_KEY_NONCE_PREFIX,
) -> None:
    """Store the Redis client and key namespace."""
    _require_redis_asyncio(feature_name="RedisApiKeyNonceStore")
    self._redis = redis
    self._key_prefix = key_prefix

is_shared_across_workers property

Redis state is shared across workers using the same server.

mark_used(*, key_id, nonce, ttl_seconds) async

Atomically record a nonce with SET NX PX.

Returns:

Type Description
ApiKeyNonceStoreResult

Stored/replay outcome for the nonce insert.

Source code in litestar_auth/authentication/strategy/_api_key_nonce_store.py
async def mark_used(self, *, key_id: str, nonce: str, ttl_seconds: int) -> ApiKeyNonceStoreResult:
    """Atomically record a nonce with ``SET NX PX``.

    Returns:
        Stored/replay outcome for the nonce insert.
    """
    result = await self._redis.set(self._key(key_id, nonce), "1", nx=True, px=max(ttl_seconds, 1) * 1000)
    if result is True:
        return ApiKeyNonceStoreResult(stored=True)
    return ApiKeyNonceStoreResult(stored=False, rejected_as_replay=True)

RedisApiKeyNonceStoreClient

Bases: RedisConditionalSetClient, Protocol

Minimal Redis client for API-key signing nonce storage.

RedisTokenStrategy(*, config=None, **options)

RedisTokenStrategy(*, config: RedisTokenStrategyConfig[ID])
RedisTokenStrategy(**options: Unpack[RedisTokenStrategyOptions[ID]])

Bases: Strategy[UP, ID]

Stateful strategy that stores opaque tokens in Redis with TTL.

Initialize the strategy.

Parameters:

Name Type Description Default
config RedisTokenStrategyConfig[ID] | None

Redis strategy configuration.

None
**options Unpack[RedisTokenStrategyOptions[ID]]

Individual Redis strategy settings. Do not combine with config.

{}

Raises:

Type Description
ValueError

If config and keyword options are combined.

ConfigurationError

When token_hash_secret fails minimum-length requirements.

Source code in litestar_auth/authentication/strategy/redis.py
def __init__(
    self,
    *,
    config: RedisTokenStrategyConfig[ID] | None = None,
    **options: Unpack[RedisTokenStrategyOptions[ID]],
) -> None:
    """Initialize the strategy.

    Args:
        config: Redis strategy configuration.
        **options: Individual Redis strategy settings. Do not combine with
            ``config``.

    Raises:
        ValueError: If ``config`` and keyword options are combined.
        ConfigurationError: When ``token_hash_secret`` fails minimum-length requirements.
    """
    if config is not None and options:
        msg = "Pass either RedisTokenStrategyConfig or keyword options, not both."
        raise ValueError(msg)
    settings = RedisTokenStrategyConfig(**options) if config is None else config
    _load_redis_asyncio()
    try:
        validate_production_secret(settings.token_hash_secret, label="RedisTokenStrategy token_hash_secret")
    except ConfigurationError as exc:
        raise ConfigurationError(str(exc)) from exc
    validate_token_bytes(settings.token_bytes, label="RedisTokenStrategy")

    self.redis = settings.redis
    self._token_hash_secret = settings.token_hash_secret.encode()
    self.lifetime = settings.lifetime
    self.token_bytes = settings.token_bytes
    self.key_prefix = settings.key_prefix
    self.subject_decoder = settings.subject_decoder

destroy_token(token, user) async

Delete a persisted Redis token.

Source code in litestar_auth/authentication/strategy/redis.py
@override
async def destroy_token(self, token: str, user: UP) -> None:
    """Delete a persisted Redis token."""
    token_key = self._key(token)
    user_id = str(user.id)
    index_key = self._user_index_key(user_id)
    await self.redis.delete(token_key)
    await self.redis.srem(index_key, token_key)

has_recent_totp_verification(user, session_id) async

Return whether a Redis-backed session has a live TOTP step-up marker.

Source code in litestar_auth/authentication/strategy/redis.py
async def has_recent_totp_verification(self, user: UP, session_id: str) -> bool:
    """Return whether a Redis-backed session has a live TOTP step-up marker."""
    return await self.redis.get(self._totp_stepup_key(str(user.id), session_id)) is not None

invalidate_all_tokens(user) async

Delete all Redis-backed tokens associated with the given user.

This bumps a per-user invalidation epoch before deleting indexed token and step-up marker keys, so out-of-index tokens are rejected on their next read without requiring a keyspace scan.

Source code in litestar_auth/authentication/strategy/redis.py
async def invalidate_all_tokens(self, user: UP) -> None:
    """Delete all Redis-backed tokens associated with the given user.

    This bumps a per-user invalidation epoch before deleting indexed token
    and step-up marker keys, so out-of-index tokens are rejected on their
    next read without requiring a keyspace scan.
    """
    user_id = str(user.id)
    await self.redis.eval(
        _REDIS_INVALIDATE_USER_TOKENS_SCRIPT,
        3,
        self._user_epoch_key(user_id),
        self._user_index_key(user_id),
        self._totp_stepup_index_key(user_id),
    )

issue_totp_stepup(user, session_id, *, ttl_seconds) async

Store a short-lived TOTP step-up marker for a Redis-backed session.

Source code in litestar_auth/authentication/strategy/redis.py
async def issue_totp_stepup(self, user: UP, session_id: str, *, ttl_seconds: int) -> None:
    """Store a short-lived TOTP step-up marker for a Redis-backed session."""
    user_id = str(user.id)
    key = self._totp_stepup_key(user_id, session_id)
    index_key = self._totp_stepup_index_key(user_id)
    if ttl_seconds <= 0:
        await self.redis.delete(key)
        await self.redis.srem(index_key, key)
        return
    await self.redis.set(key, "1", ex=ttl_seconds)
    await self.redis.sadd(index_key, key)
    await self.redis.expire(index_key, ttl_seconds)

read_token(token, user_manager) async

Resolve a user from a Redis-backed token.

Returns:

Type Description
UP | None

The resolved user when the token exists and decodes successfully,

UP | None

otherwise None.

Source code in litestar_auth/authentication/strategy/redis.py
@override
async def read_token(
    self,
    token: str | None,
    user_manager: UserManagerProtocol[UP, ID],
) -> UP | None:
    """Resolve a user from a Redis-backed token.

    Returns:
        The resolved user when the token exists and decodes successfully,
        otherwise ``None``.
    """
    if token is None:
        return None

    stored_user_id = await self.redis.get(self._key(token))
    if stored_user_id is None:
        return None

    token_epoch, user_id_text = self._decode_token_payload(stored_user_id)
    if token_epoch != await self._current_user_epoch(user_id_text):
        return None

    try:
        user_id = self.subject_decoder(user_id_text) if self.subject_decoder is not None else user_id_text
    except (TypeError, ValueError):
        return None

    return await user_manager.get(cast("ID", user_id))

write_token(user) async

Persist a new opaque token in Redis and return it.

Returns:

Type Description
str

Newly created opaque token string.

Source code in litestar_auth/authentication/strategy/redis.py
@override
async def write_token(self, user: UP) -> str:
    """Persist a new opaque token in Redis and return it.

    Returns:
        Newly created opaque token string.
    """
    token, token_key = self._mint_token_key()
    user_id = str(user.id)
    epoch = await self._current_user_epoch(user_id)
    await self.redis.set(
        token_key,
        self._encode_token_payload(epoch=epoch, user_id=user_id),
        ex=self._ttl_seconds,
    )
    index_key = self._user_index_key(user_id)
    await self.redis.sadd(index_key, token_key)
    await self.redis.expire(index_key, self._ttl_seconds)
    return token

RedisTokenStrategyConfig(redis, token_hash_secret, lifetime=DEFAULT_LIFETIME, token_bytes=DEFAULT_TOKEN_BYTES, key_prefix=DEFAULT_KEY_PREFIX, subject_decoder=None) dataclass

Configuration for :class:RedisTokenStrategy.

RefreshableStrategy

Bases: Protocol

Protocol for strategies that support refresh-token rotation.

Note

Refresh tokens are intentionally modeled as a separate lifecycle artifact from access tokens. In particular, Strategy.destroy_token() only targets the access token used for request authentication; refresh-token invalidation (if any) is managed by the refresh strategy itself.

rotate_refresh_token(refresh_token, user_manager) async

Consume a refresh token and return the user plus a rotated replacement.

Source code in litestar_auth/authentication/strategy/base.py
async def rotate_refresh_token(
    self,
    refresh_token: str,
    user_manager: UserManagerProtocol[UP, ID],
) -> tuple[UP, str] | None:
    """Consume a refresh token and return the user plus a rotated replacement."""

write_refresh_token(user) async

Issue a refresh token for the provided user.

Source code in litestar_auth/authentication/strategy/base.py
async def write_refresh_token(self, user: UP) -> str:
    """Issue a refresh token for the provided user."""

Strategy

Bases: ABC

Abstract base class for token storage and validation strategies.

destroy_token(token, user) abstractmethod async

Invalidate a token for the provided user.

Source code in litestar_auth/authentication/strategy/base.py
@abstractmethod
async def destroy_token(self, token: str, user: UP) -> None:
    """Invalidate a token for the provided user."""

read_token(token, user_manager) abstractmethod async

Resolve a user from a token.

Source code in litestar_auth/authentication/strategy/base.py
@abstractmethod
async def read_token(self, token: str | None, user_manager: UserManagerProtocol[UP, ID]) -> UP | None:
    """Resolve a user from a token."""

write_token(user) abstractmethod async

Issue a token for the provided user.

Source code in litestar_auth/authentication/strategy/base.py
@abstractmethod
async def write_token(self, user: UP) -> str:
    """Issue a token for the provided user."""

UserManagerProtocol

Bases: Protocol

Protocol for user manager lookups used by token strategies.

get(user_id) async

Return the user for the given identifier.

Source code in litestar_auth/authentication/strategy/base.py
async def get(self, user_id: ID) -> UP | None:
    """Return the user for the given identifier."""