Skip to content

Redis contrib

Use Configuration for the main Redis-backed auth story: the shared-client preset flow, the AuthRateLimitSlot enum, namespace families, migration behavior, and TOTP store guidance still live there.

litestar_auth.contrib.redis is the higher-level entrypoint when one async Redis client should back auth rate limiting plus the TOTP Redis stores:

  • RedisAuthClientProtocol is the stable typing contract for annotating the shared async Redis client passed to RedisAuthPreset.
  • RedisAuthPreset builds AuthRateLimitConfig, RedisTotpEnrollmentStore, RedisUsedTotpCodeStore, and the pending-token RedisJWTDenylistStore helper from one shared client and per-group rate-limit tiers. Use RedisAuthRateLimitConfigOptions when calling build_rate_limit_config(...) with enabled=... or disabled=.... The shared-client contract covers the combined operations used by the rate-limiter, pending-enrollment, used-code replay, and pending-JTI denylist helpers: eval(...), delete(...), set(name, value, nx=True, px=ttl_ms), set(name, value, ex=ttl_s), and get(...).
  • RedisTokenStrategy, RedisTokenStrategyConfig, RedisTotpEnrollmentStore, and RedisUsedTotpCodeStore remain the direct low-level convenience imports.
  • AuthRateLimitConfig.from_shared_backend(), direct RedisRateLimiter(...) construction, and direct RedisJWTDenylistStore(...) / RedisTotpEnrollmentStore(...) / RedisUsedTotpCodeStore(...) construction remain the fallback low-level path for applications that need separate backends or fully bespoke wiring.
  • RedisApiKeyNonceStore is the shared-store nonce backend for signed API-key replay protection. Use it instead of InMemoryApiKeyNonceStore in multi-worker deployments.

Optional Redis-backed helpers (requires litestar-auth[redis]).

litestar_auth.contrib.redis

Stable public Redis contrib helpers.

This package exposes the documented shared-client Redis typing contract, the shared-client preset, and the current low-level Redis-backed auth convenience imports.

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.

RedisAuthClientProtocol

Bases: RedisSharedAuthClient, Protocol

Public async Redis client contract shared by Redis auth contrib helpers.

RedisAuthPreset(redis, rate_limit_tier=_default_rate_limit_tier(), group_rate_limit_tiers=_empty_group_rate_limit_tiers(), totp_used_tokens_key_prefix=None, totp_pending_jti_key_prefix=None, totp_enrollment_key_prefix=None) dataclass

Shared-client Redis preset for auth rate limiting and TOTP state stores.

Keep the low-level RedisRateLimiter, RedisUsedTotpCodeStore, RedisTotpEnrollmentStore, RedisJWTDenylistStore, and AuthRateLimitConfig.from_shared_backend() APIs for advanced cases that need fully custom backends. This preset is the higher-level path when one async Redis client should back the auth rate-limit config plus the TOTP replay, pending-enrollment, and pending-token replay stores.

Parameters:

Name Type Description Default
redis RedisAuthClientProtocol

Async Redis client compatible with redis.asyncio.Redis and satisfying :class:RedisAuthClientProtocol: eval(...), delete(...), set(name, value, nx=True, px=ttl_ms), set(name, value, ex=ttl_s), and get(...).

required
rate_limit_tier RedisAuthRateLimitTier

Default rate-limit settings used for every supported auth slot unless a group-specific override is configured.

_default_rate_limit_tier()
group_rate_limit_tiers Mapping[AuthRateLimitEndpointGroup, RedisAuthRateLimitTier]

Optional per-group rate-limit settings keyed by AuthRateLimitEndpointGroup names such as "refresh" or "totp".

_empty_group_rate_limit_tiers()
totp_used_tokens_key_prefix str | None

Optional default Redis key prefix for the TOTP replay store. None preserves the current RedisUsedTotpCodeStore default.

None
totp_pending_jti_key_prefix str | None

Optional default Redis key prefix for the pending-login-token JTI denylist store. None preserves the current RedisJWTDenylistStore default.

None
totp_enrollment_key_prefix str | None

Optional default Redis key prefix for the pending-enrollment store. None preserves the current RedisTotpEnrollmentStore default.

None

__post_init__()

Snapshot group-specific rate-limit tiers into a read-only mapping.

Source code in litestar_auth/contrib/redis/_surface.py
def __post_init__(self) -> None:
    """Snapshot group-specific rate-limit tiers into a read-only mapping."""
    self.group_rate_limit_tiers = MappingProxyType(dict(self.group_rate_limit_tiers))

build_rate_limit_config(*, options=None)

Build AuthRateLimitConfig from the preset's shared Redis client.

Parameters:

Name Type Description Default
options RedisAuthRateLimitConfigOptions | None

Optional shared builder options. When omitted, every supported auth slot uses the preset's default tier.

None

Returns:

Type Description
AuthRateLimitConfig

The auth rate-limit config built from the preset's shared client and

AuthRateLimitConfig

tier settings.

Source code in litestar_auth/contrib/redis/_surface.py
def build_rate_limit_config(
    self,
    *,
    options: RedisAuthRateLimitConfigOptions | None = None,
) -> AuthRateLimitConfig:
    """Build ``AuthRateLimitConfig`` from the preset's shared Redis client.

    Args:
        options: Optional shared builder options. When omitted, every
            supported auth slot uses the preset's default tier.

    Returns:
        The auth rate-limit config built from the preset's shared client and
        tier settings.
    """
    resolved_options = options or RedisAuthRateLimitConfigOptions()
    derived_group_backends: dict[AuthRateLimitEndpointGroup, RateLimiterBackend] = {
        group: self._build_rate_limit_backend(tier) for group, tier in self.group_rate_limit_tiers.items()
    }
    if resolved_options.group_backends is not None:
        derived_group_backends.update(resolved_options.group_backends)
    shared_backend: RateLimiterBackend = self._build_rate_limit_backend(self.rate_limit_tier)
    resolved_group_backends = derived_group_backends or None
    default_shared_options = SharedRateLimitConfigOptions()

    shared_options = SharedRateLimitConfigOptions(
        enabled=resolved_options.enabled,
        disabled=resolved_options.disabled,
        group_backends=resolved_group_backends,
        endpoint_overrides=resolved_options.endpoint_overrides,
        trusted_proxy=resolved_options.trusted_proxy,
        identity_fields=(
            resolved_options.identity_fields
            if resolved_options.identity_fields is not None
            else default_shared_options.identity_fields
        ),
        trusted_headers=(
            resolved_options.trusted_headers
            if resolved_options.trusted_headers is not None
            else default_shared_options.trusted_headers
        ),
        trusted_proxy_hops=resolved_options.trusted_proxy_hops,
    )
    return AuthRateLimitConfig.from_shared_backend(shared_backend, options=shared_options)

build_totp_enrollment_store(*, key_prefix=None)

Build RedisTotpEnrollmentStore from the preset's shared Redis client.

Parameters:

Name Type Description Default
key_prefix str | None

Optional per-call Redis key prefix override. When omitted, the preset uses totp_enrollment_key_prefix and finally falls back to the current RedisTotpEnrollmentStore default.

None

Returns:

Type Description
RedisTotpEnrollmentStore

Redis-backed pending-enrollment store sharing the preset's client.

Source code in litestar_auth/contrib/redis/_surface.py
def build_totp_enrollment_store(self, *, key_prefix: str | None = None) -> RedisTotpEnrollmentStore:
    """Build ``RedisTotpEnrollmentStore`` from the preset's shared Redis client.

    Args:
        key_prefix: Optional per-call Redis key prefix override. When
            omitted, the preset uses ``totp_enrollment_key_prefix`` and
            finally falls back to the current ``RedisTotpEnrollmentStore``
            default.

    Returns:
        Redis-backed pending-enrollment store sharing the preset's client.
    """
    resolved_key_prefix = self.totp_enrollment_key_prefix if key_prefix is None else key_prefix
    if resolved_key_prefix is None:
        resolved_key_prefix = DEFAULT_TOTP_ENROLLMENT_KEY_PREFIX
    return RedisTotpEnrollmentStore(redis=self.redis, key_prefix=resolved_key_prefix)

build_totp_pending_jti_store(*, key_prefix=None)

Build RedisJWTDenylistStore from the preset's shared Redis client.

Parameters:

Name Type Description Default
key_prefix str | None

Optional per-call Redis key prefix override. When omitted, the preset uses totp_pending_jti_key_prefix and finally falls back to the current RedisJWTDenylistStore default.

None

Returns:

Type Description
RedisJWTDenylistStore

Redis-backed pending-token JTI denylist sharing the preset's

RedisJWTDenylistStore

client.

Source code in litestar_auth/contrib/redis/_surface.py
def build_totp_pending_jti_store(self, *, key_prefix: str | None = None) -> RedisJWTDenylistStore:
    """Build ``RedisJWTDenylistStore`` from the preset's shared Redis client.

    Args:
        key_prefix: Optional per-call Redis key prefix override. When
            omitted, the preset uses ``totp_pending_jti_key_prefix`` and
            finally falls back to the current ``RedisJWTDenylistStore``
            default.

    Returns:
        Redis-backed pending-token JTI denylist sharing the preset's
        client.
    """
    resolved_key_prefix = self.totp_pending_jti_key_prefix if key_prefix is None else key_prefix
    if resolved_key_prefix is None:
        return RedisJWTDenylistStore(redis=self.redis)
    return RedisJWTDenylistStore(redis=self.redis, key_prefix=resolved_key_prefix)

build_totp_used_tokens_store(*, key_prefix=None)

Build RedisUsedTotpCodeStore from the preset's shared Redis client.

Parameters:

Name Type Description Default
key_prefix str | None

Optional per-call Redis key prefix override. When omitted, the preset uses totp_used_tokens_key_prefix and finally falls back to the current RedisUsedTotpCodeStore default.

None

Returns:

Type Description
RedisUsedTotpCodeStore

Redis-backed TOTP replay store sharing the preset's client.

Source code in litestar_auth/contrib/redis/_surface.py
def build_totp_used_tokens_store(self, *, key_prefix: str | None = None) -> RedisUsedTotpCodeStore:
    """Build ``RedisUsedTotpCodeStore`` from the preset's shared Redis client.

    Args:
        key_prefix: Optional per-call Redis key prefix override. When
            omitted, the preset uses ``totp_used_tokens_key_prefix`` and
            finally falls back to the current ``RedisUsedTotpCodeStore``
            default.

    Returns:
        Redis-backed TOTP replay store sharing the preset's client.
    """
    resolved_key_prefix = self.totp_used_tokens_key_prefix if key_prefix is None else key_prefix
    if resolved_key_prefix is None:
        resolved_key_prefix = DEFAULT_TOTP_USED_KEY_PREFIX
    return RedisUsedTotpCodeStore(redis=self.redis, key_prefix=resolved_key_prefix)

RedisAuthRateLimitConfigOptions(enabled=None, disabled=(), group_backends=None, endpoint_overrides=None, trusted_proxy=False, identity_fields=None, trusted_headers=None, trusted_proxy_hops=1) dataclass

Builder options for :meth:RedisAuthPreset.build_rate_limit_config.

Parameters:

Name Type Description Default
enabled Iterable[AuthRateLimitSlot] | None

Optional auth slot enum values to build.

None
disabled Iterable[AuthRateLimitSlot]

Auth slot enum values to leave unset.

()
group_backends Mapping[AuthRateLimitEndpointGroup, RateLimiterBackend] | None

Optional explicit backend overrides keyed by auth slot group. These win over RedisAuthPreset.group_rate_limit_tiers.

None
endpoint_overrides Mapping[AuthRateLimitSlot, EndpointRateLimit | None] | None

Optional full per-slot replacements or explicit None disablement.

None
trusted_proxy bool

Shared trusted-proxy setting applied to generated limiters.

False
identity_fields tuple[str, ...] | None

Optional shared request body identity fields. When omitted, AuthRateLimitConfig.from_shared_backend() keeps its current default.

None
trusted_headers tuple[str, ...] | None

Optional shared trusted proxy header names. When omitted, AuthRateLimitConfig.from_shared_backend() keeps its current default.

None
trusted_proxy_hops int

Shared X-Forwarded-For hop count applied to generated limiters.

1

RedisAuthRateLimitTier(max_attempts, window_seconds, key_prefix=None) dataclass

Rate-limit settings used by :class:RedisAuthPreset.

Parameters:

Name Type Description Default
max_attempts int

Maximum attempts allowed inside the window.

required
window_seconds float

Sliding-window duration in seconds.

required
key_prefix str | None

Optional Redis key prefix passed to RedisRateLimiter. None preserves the current RedisRateLimiter default.

None

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.