Skip to content

Redis contrib

Use Configuration for the canonical Redis-backed auth story: the preferred one-client preset flow, the AUTH_RATE_LIMIT_* helper exports, namespace families, migration behavior, and TOTP replay-store guidance still live there.

litestar_auth.contrib.redis is the higher-level entrypoint when one async Redis client should back both auth rate limiting and TOTP replay protection:

  • RedisAuthPreset builds AuthRateLimitConfig plus RedisUsedTotpCodeStore from one shared client and per-group rate-limit tiers. Import slot helpers such as AUTH_RATE_LIMIT_VERIFICATION_SLOTS from litestar_auth.ratelimit when calling build_rate_limit_config(...). The shared client only needs the combined RedisRateLimiter + RedisUsedTotpCodeStore operations: eval(...), delete(...), and set(name, value, nx=True, px=ttl_ms).
  • RedisTokenStrategy and RedisUsedTotpCodeStore remain the direct low-level convenience imports.
  • AuthRateLimitConfig.from_shared_backend() and direct RedisRateLimiter(...) construction remain the fallback escape hatches for applications that need separate backends or fully bespoke wiring.

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 preset plus the current low-level Redis-backed auth convenience imports.

RedisAuthPreset(redis, rate_limit_tier=_default_rate_limit_tier(), group_rate_limit_tiers=dict(), totp_used_tokens_key_prefix=None) dataclass

Shared-client Redis preset for auth rate limiting and TOTP replay protection.

Keep the low-level RedisRateLimiter, RedisUsedTotpCodeStore, 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 both the auth rate-limit config and the TOTP replay store.

Parameters:

Name Type Description Default
redis RedisSharedAuthClient

Async Redis client compatible with redis.asyncio.Redis and satisfying the combined RedisRateLimiter plus RedisUsedTotpCodeStore contract: eval(...), delete(...), and set(name, value, nx=True, px=ttl_ms).

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".

dict()
totp_used_tokens_key_prefix str | None

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

None

build_rate_limit_config(*, enabled=None, disabled=(), group_backends=None, scope_overrides=None, namespace_style='route', namespace_overrides=None, endpoint_overrides=None, trusted_proxy=False, identity_fields=None, trusted_headers=None)

Build AuthRateLimitConfig from the preset's shared Redis client.

Parameters:

Name Type Description Default
enabled Iterable[AuthRateLimitEndpointSlot] | None

Optional auth slot names to build.

None
disabled Iterable[AuthRateLimitEndpointSlot]

Auth slot names to leave unset.

()
group_backends Mapping[AuthRateLimitEndpointGroup, RateLimiterBackend] | None

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

None
scope_overrides Mapping[AuthRateLimitEndpointSlot, RateLimitScope] | None

Optional per-slot scope overrides.

None
namespace_style AuthRateLimitNamespaceStyle

Namespace family for generated limiters.

'route'
namespace_overrides Mapping[AuthRateLimitEndpointSlot, str] | None

Optional per-slot namespace overrides.

None
endpoint_overrides Mapping[AuthRateLimitEndpointSlot, 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

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(  # noqa: PLR0913
    self,
    *,
    enabled: typing.Iterable[AuthRateLimitEndpointSlot] | None = None,
    disabled: typing.Iterable[AuthRateLimitEndpointSlot] = (),
    group_backends: typing.Mapping[AuthRateLimitEndpointGroup, RateLimiterBackend] | None = None,
    scope_overrides: typing.Mapping[AuthRateLimitEndpointSlot, RateLimitScope] | None = None,
    namespace_style: AuthRateLimitNamespaceStyle = "route",
    namespace_overrides: typing.Mapping[AuthRateLimitEndpointSlot, str] | None = None,
    endpoint_overrides: typing.Mapping[AuthRateLimitEndpointSlot, EndpointRateLimit | None] | None = None,
    trusted_proxy: bool = False,
    identity_fields: tuple[str, ...] | None = None,
    trusted_headers: tuple[str, ...] | None = None,
) -> AuthRateLimitConfig:
    """Build ``AuthRateLimitConfig`` from the preset's shared Redis client.

    Args:
        enabled: Optional auth slot names to build.
        disabled: Auth slot names to leave unset.
        group_backends: Optional explicit backend overrides keyed by auth
            slot group. These win over ``group_rate_limit_tiers``.
        scope_overrides: Optional per-slot scope overrides.
        namespace_style: Namespace family for generated limiters.
        namespace_overrides: Optional per-slot namespace overrides.
        endpoint_overrides: Optional full per-slot replacements or explicit
            ``None`` disablement.
        trusted_proxy: Shared trusted-proxy setting applied to generated
            limiters.
        identity_fields: Optional shared request body identity fields. When
            omitted, ``AuthRateLimitConfig.from_shared_backend()`` keeps its
            current default.
        trusted_headers: Optional shared trusted proxy header names. When
            omitted, ``AuthRateLimitConfig.from_shared_backend()`` keeps its
            current default.

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

    if identity_fields is None:
        if trusted_headers is None:
            return AuthRateLimitConfig.from_shared_backend(
                shared_backend,
                enabled=enabled,
                disabled=disabled,
                group_backends=resolved_group_backends,
                scope_overrides=scope_overrides,
                namespace_style=namespace_style,
                namespace_overrides=namespace_overrides,
                endpoint_overrides=endpoint_overrides,
                trusted_proxy=trusted_proxy,
            )
        return AuthRateLimitConfig.from_shared_backend(
            shared_backend,
            enabled=enabled,
            disabled=disabled,
            group_backends=resolved_group_backends,
            scope_overrides=scope_overrides,
            namespace_style=namespace_style,
            namespace_overrides=namespace_overrides,
            endpoint_overrides=endpoint_overrides,
            trusted_proxy=trusted_proxy,
            trusted_headers=trusted_headers,
        )

    if trusted_headers is None:
        return AuthRateLimitConfig.from_shared_backend(
            shared_backend,
            enabled=enabled,
            disabled=disabled,
            group_backends=resolved_group_backends,
            scope_overrides=scope_overrides,
            namespace_style=namespace_style,
            namespace_overrides=namespace_overrides,
            endpoint_overrides=endpoint_overrides,
            trusted_proxy=trusted_proxy,
            identity_fields=identity_fields,
        )

    return AuthRateLimitConfig.from_shared_backend(
        shared_backend,
        enabled=enabled,
        disabled=disabled,
        group_backends=resolved_group_backends,
        scope_overrides=scope_overrides,
        namespace_style=namespace_style,
        namespace_overrides=namespace_overrides,
        endpoint_overrides=endpoint_overrides,
        trusted_proxy=trusted_proxy,
        identity_fields=identity_fields,
        trusted_headers=trusted_headers,
    )

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)

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(*, redis, token_hash_secret, lifetime=DEFAULT_LIFETIME, token_bytes=DEFAULT_TOKEN_BYTES, key_prefix=DEFAULT_KEY_PREFIX, subject_decoder=None, max_scan_keys=DEFAULT_MAX_SCAN_KEYS)

Bases: Strategy[UP, ID]

Stateful strategy that stores opaque tokens in Redis with TTL.

Initialize the strategy.

Parameters:

Name Type Description Default
redis RedisClientProtocol

Async Redis client compatible with redis.asyncio.Redis.

required
token_hash_secret str

High-entropy secret used for keyed token hashing (HMAC-SHA256).

required
lifetime timedelta

Token TTL applied to persisted keys.

DEFAULT_LIFETIME
token_bytes int

Number of random bytes used for token generation.

DEFAULT_TOKEN_BYTES
key_prefix str

Prefix used to namespace token keys in Redis.

DEFAULT_KEY_PREFIX
subject_decoder Callable[[str], ID] | None

Optional callable that converts the stored user id string into the identifier type expected by the user manager.

None
max_scan_keys int

Safety cap on keys examined during scan-based fallback invalidation. Prevents runaway iteration on large keyspaces.

DEFAULT_MAX_SCAN_KEYS

Raises:

Type Description
ConfigurationError

When token_hash_secret fails minimum-length requirements.

Source code in litestar_auth/authentication/strategy/redis.py
def __init__(  # noqa: PLR0913
    self,
    *,
    redis: RedisClientProtocol,
    token_hash_secret: str,
    lifetime: timedelta = DEFAULT_LIFETIME,
    token_bytes: int = DEFAULT_TOKEN_BYTES,
    key_prefix: str = DEFAULT_KEY_PREFIX,
    subject_decoder: Callable[[str], ID] | None = None,
    max_scan_keys: int = DEFAULT_MAX_SCAN_KEYS,
) -> None:
    """Initialize the strategy.

    Args:
        redis: Async Redis client compatible with ``redis.asyncio.Redis``.
        token_hash_secret: High-entropy secret used for keyed token hashing (HMAC-SHA256).
        lifetime: Token TTL applied to persisted keys.
        token_bytes: Number of random bytes used for token generation.
        key_prefix: Prefix used to namespace token keys in Redis.
        subject_decoder: Optional callable that converts the stored user id
            string into the identifier type expected by the user manager.
        max_scan_keys: Safety cap on keys examined during scan-based fallback
            invalidation.  Prevents runaway iteration on large keyspaces.

    Raises:
        ConfigurationError: When ``token_hash_secret`` fails minimum-length requirements.
    """
    _load_redis_asyncio()
    try:
        validate_secret_length(token_hash_secret, label="RedisTokenStrategy token_hash_secret")
    except ConfigurationError as exc:
        raise ConfigurationError(str(exc)) from exc

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

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)

invalidate_all_tokens(user) async

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

This uses a per-user index to delete only the keys associated with the user, avoiding keyspace scans under the global prefix.

Backward compatibility

If the per-user index is missing, we fall back to a best-effort scan over keys matching the configured prefix and remove those whose stored subject matches user.id.

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 uses a per-user index to delete only the keys associated with the
    user, avoiding keyspace scans under the global prefix.

    Backward compatibility:
        If the per-user index is missing, we fall back to a best-effort scan
        over keys matching the configured prefix and remove those whose
        stored subject matches ``user.id``.
    """
    user_id = str(user.id)
    if await self._invalidate_via_index(user_id):
        return
    await self._invalidate_via_scan(user_id)

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

    user_id_text = self._decode_user_id(stored_user_id)

    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 = secrets.token_urlsafe(self.token_bytes)
    token_key = self._key(token)
    user_id = str(user.id)
    await self.redis.setex(token_key, self._ttl_seconds, user_id)
    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

RedisUsedTotpCodeStore(*, redis, key_prefix=DEFAULT_TOTP_USED_KEY_PREFIX)

Redis-backed replay store for TOTP codes; safe for multi-worker and multi-pod deployments.

For the higher-level shared-client Redis preset that can also derive AuthRateLimitConfig, see litestar_auth.contrib.redis.RedisAuthPreset.

Store the Redis client and key prefix.

Parameters:

Name Type Description Default
redis RedisUsedTotpCodeStoreClient

Async Redis client supporting set(name, value, nx=True, px=ttl_ms) (e.g. redis.asyncio.Redis).

required
key_prefix str

Prefix for replay keys; keys are {key_prefix}{user_id}:{counter}.

DEFAULT_TOTP_USED_KEY_PREFIX
Source code in litestar_auth/totp.py
def __init__(
    self,
    *,
    redis: RedisUsedTotpCodeStoreClient,
    key_prefix: str = DEFAULT_TOTP_USED_KEY_PREFIX,
) -> None:
    """Store the Redis client and key prefix.

    Args:
        redis: Async Redis client supporting ``set(name, value, nx=True, px=ttl_ms)``
            (e.g. ``redis.asyncio.Redis``).
        key_prefix: Prefix for replay keys; keys are ``{key_prefix}{user_id}:{counter}``.
    """
    _load_redis_asyncio()
    self._redis = redis
    self._key_prefix = key_prefix

mark_used(user_id, counter, ttl_seconds) async

Atomically record a used (user_id, counter) pair via SET key 1 NX PX ttl_ms.

Returns:

Type Description
bool

True when the pair was newly stored, False for a replay.

Source code in litestar_auth/totp.py
async def mark_used(self, user_id: Hashable, counter: int, ttl_seconds: float) -> bool:
    """Atomically record a used (user_id, counter) pair via SET key 1 NX PX ttl_ms.

    Returns:
        ``True`` when the pair was newly stored, ``False`` for a replay.
    """
    key = self._key(user_id, counter)
    ttl_ms = int(ttl_seconds * 1000)
    result = await self._redis.set(key, "1", nx=True, px=ttl_ms)
    return result is True