# Redis

Use this page for Redis-backed auth helpers, shared rate-limit wiring, TOTP enrollment state, replay stores, pending-token deduplication, and Redis import boundaries.

## Redis-backed auth surface

This section is the Redis integration guide for the currently implemented auth surface.
Use it for the shared-backend rate-limit contract, namespace-override patterns for non-default Redis key shapes, TOTP
pending-enrollment state, replay protection, pending-login-token JTI deduplication, and the stable split between
`litestar_auth.ratelimit` and `litestar_auth.contrib.redis`.

### Shared-backend rate limiting

For the usual Redis deployment where one async Redis client should back auth rate limiting, TOTP
pending enrollment, replay protection, and pending-login-token JTI deduplication, start with
`litestar_auth.contrib.redis.RedisAuthPreset` plus explicit verification slots from
`litestar_auth.ratelimit.AuthRateLimitSlot`:

This is the recommended rate-limit backend posture for multi-worker production. Pair the generated
rate-limit config with `LitestarAuthConfig.deployment_worker_count=2` or the actual known worker
count so startup can fail closed if a later configuration change accidentally reintroduces a
process-local auth rate-limit backend. Leave `deployment_worker_count=None` only when the host
topology is genuinely unknown; in that case the plugin preserves warning-only diagnostics because it
cannot reliably infer every ASGI server's process count.

For strict typing, annotate the shared client with
`litestar_auth.contrib.redis.RedisAuthClientProtocol`. The shared-client recipe assumes a
`redis.asyncio.Redis`-compatible runtime client. The shared protocol covers the combined operations
used by the preset's rate-limiter, pending-enrollment, used-code replay, and pending-token denylist helpers:
`eval(...)`, `delete(...)`, `set(name, value, nx=True, px=ttl_ms)`, `set(name, value, ex=ttl_s)`, and `get(...)`.

```python
from litestar_auth import LitestarAuthConfig, TotpConfig
from litestar_auth.contrib.redis import (
    RedisAuthClientProtocol,
    RedisAuthPreset,
    RedisAuthRateLimitConfigOptions,
    RedisAuthRateLimitTier,
)
from litestar_auth.ratelimit import AuthRateLimitSlot

redis_client: RedisAuthClientProtocol
redis_auth = RedisAuthPreset(
    redis=redis_client,
    rate_limit_tier=RedisAuthRateLimitTier(max_attempts=5, window_seconds=60),
    group_rate_limit_tiers={
        "refresh": RedisAuthRateLimitTier(max_attempts=10, window_seconds=300),
        "totp": RedisAuthRateLimitTier(max_attempts=5, window_seconds=300),
    },
)
rate_limit_config = redis_auth.build_rate_limit_config(
    options=RedisAuthRateLimitConfigOptions(
        disabled={AuthRateLimitSlot.VERIFY_TOKEN, AuthRateLimitSlot.REQUEST_VERIFY_TOKEN},
        trusted_proxy=True,
        trusted_proxy_hops=2,
    ),
)
totp_config = TotpConfig(
    totp_pending_secret="replace-with-32+-char-secret",
    totp_enrollment_store=redis_auth.build_totp_enrollment_store(),
    totp_pending_jti_store=redis_auth.build_totp_pending_jti_store(),
    totp_used_tokens_store=redis_auth.build_totp_used_tokens_store(),
)
config = LitestarAuthConfig(
    user_model=User,
    session_maker=session_maker,
    backends=backends,
    rate_limit_config=rate_limit_config,
    totp_config=totp_config,
    deployment_worker_count=2,
)
```

`RedisAuthPreset` is the highest-level shared-client Redis path. Keep the module split explicit:

- `litestar_auth.contrib.redis` owns the higher-level convenience entrypoints such as `RedisAuthPreset`,
  `RedisAuthRateLimitTier`, `RedisAuthClientProtocol`, `RedisTokenStrategy`,
  `RedisTokenStrategyConfig`,
  `RedisTotpEnrollmentStore`, and `RedisUsedTotpCodeStore`.
- `litestar_auth.ratelimit` owns the lower-level shared-builder surface such as
  `AuthRateLimitConfig.from_shared_backend()`, `RedisRateLimiter`, the typed slot enum, and the group alias.

`RedisAuthPreset.build_rate_limit_config()` accepts `RedisAuthRateLimitConfigOptions`, which carries
the live shared-builder inputs: `enabled`, `disabled`, `group_backends`, and `endpoint_overrides`,
plus the shared proxy and identity settings. Explicit `group_backends` still win over any preset
`group_rate_limit_tiers`. `RedisAuthPreset.group_rate_limit_tiers` is snapshotted into a read-only
mapping at construction time, so later mutations to the caller's source `dict` do not silently
change the preset's runtime budget layout.
`build_totp_enrollment_store()`, `build_totp_used_tokens_store()`, and
`build_totp_pending_jti_store()` follow the same precedence: per-call `key_prefix=` wins over the
preset default, and `None` preserves each low-level store's current built-in prefix.

### Redis opaque-token invalidation

`RedisTokenStrategy` writes each opaque token under a TTL-backed token key and records that key in a
per-user Redis set. Use `RedisTokenStrategyConfig(...)` when the shared Redis client and token
settings should travel as one typed object. `invalidate_all_tokens(user)` atomically bumps a per-user
invalidation epoch and deletes indexed tokens plus indexed TOTP step-up markers in one Redis script.
Token reads compare the token's stored epoch with the current user epoch, so out-of-index orphan
tokens are rejected on their next use without scanning the broader keyspace.

If you are upgrading from a version that created Redis token keys without the per-user index, those
orphaned keys are not deleted by `invalidate_all_tokens(...)`, but the epoch check rejects them after
the user's next all-token invalidation. Rotate or flush pre-index token keys during upgrade if those
sessions need to be invalidated before a user-specific epoch bump occurs.

The shared builder itself exposes typed public identifiers from
`litestar_auth.ratelimit`:

```python
from litestar_auth.ratelimit import AuthRateLimitEndpointGroup, AuthRateLimitSlot

all_slots = tuple(AuthRateLimitSlot)
verification_slots = {AuthRateLimitSlot.VERIFY_TOKEN, AuthRateLimitSlot.REQUEST_VERIFY_TOKEN}
```

- `AuthRateLimitSlot` names the per-endpoint enum keys accepted by `enabled`, `disabled`, and
  `endpoint_overrides`.
- `AuthRateLimitEndpointGroup` names the shared-backend keys accepted by `group_backends`.
- Use `tuple(AuthRateLimitSlot)` for explicit `enabled=...` options that should include every
  supported slot.
- Use `{AuthRateLimitSlot.VERIFY_TOKEN, AuthRateLimitSlot.REQUEST_VERIFY_TOKEN}` for
  `disabled=...` when verification routes stay off.

### Low-level Redis builder path

Keep direct `AuthRateLimitConfig.from_shared_backend()` plus direct `RedisRateLimiter(...)`,
`RedisTotpEnrollmentStore(...)`, `RedisUsedTotpCodeStore(...)`, and
`RedisJWTDenylistStore(...)` construction as the low-level path
when you need separate backends, bespoke key prefixes, or fully manual wiring:

```python
from litestar_auth.ratelimit import (
    AuthRateLimitConfig,
    AuthRateLimitSlot,
    RedisRateLimiter,
    SharedRateLimitConfigOptions,
)
from litestar_auth.totp import RedisTotpEnrollmentStore, RedisUsedTotpCodeStore

shared_backend = RedisRateLimiter(redis=redis_client, max_attempts=5, window_seconds=60)
rate_limit_config = AuthRateLimitConfig.from_shared_backend(
    shared_backend,
    options=SharedRateLimitConfigOptions(
        enabled=tuple(AuthRateLimitSlot),
        disabled={AuthRateLimitSlot.VERIFY_TOKEN, AuthRateLimitSlot.REQUEST_VERIFY_TOKEN},
        trusted_proxy=True,
        trusted_proxy_hops=2,
    ),
)
totp_used_tokens_store = RedisUsedTotpCodeStore(redis=redis_client)
totp_enrollment_store = RedisTotpEnrollmentStore(redis=redis_client)
```

`RedisRateLimiter.is_shared_across_workers` satisfies the plugin's declared multi-worker startup
contract. A process-local backend such as `InMemoryRateLimiter` does not; with
`deployment_worker_count > 1`, startup raises `ConfigurationError` and names the affected
auth rate-limit slots.

The shared builder uses the defaults below as its supported slot contract:

| `AuthRateLimitSlot` value | `AuthRateLimitEndpointGroup` value | Default scope | Default namespace token |
| ------------------------- | ---------------------------------- | ------------- | ----------------------- |
| `AuthRateLimitSlot.LOGIN` | `login` | `ip_email` | `login` |
| `AuthRateLimitSlot.REFRESH` | `refresh` | `ip` | `refresh` |
| `AuthRateLimitSlot.REGISTER` | `register` | `ip` | `register` |
| `AuthRateLimitSlot.FORGOT_PASSWORD` | `password_reset` | `ip_email` | `forgot-password` |
| `AuthRateLimitSlot.RESET_PASSWORD` | `password_reset` | `ip` | `reset-password` |
| `AuthRateLimitSlot.CHANGE_PASSWORD` | `login` | `ip_email` | `change-password` |
| `AuthRateLimitSlot.TOTP_ENABLE` | `totp` | `ip` | `totp-enable` |
| `AuthRateLimitSlot.TOTP_CONFIRM_ENABLE` | `totp` | `ip` | `totp-confirm-enable` |
| `AuthRateLimitSlot.TOTP_VERIFY` | `totp` | `ip` | `totp-verify` |
| `AuthRateLimitSlot.TOTP_DISABLE` | `totp` | `ip` | `totp-disable` |
| `AuthRateLimitSlot.TOTP_REGENERATE_RECOVERY_CODES` | `totp` | `ip` | `totp-regenerate-recovery-codes` |
| `AuthRateLimitSlot.VERIFY_TOKEN` | `verification` | `ip` | `verify-token` |
| `AuthRateLimitSlot.REQUEST_VERIFY_TOKEN` | `verification` | `ip_email` | `request-verify-token` |

Accepted `AuthRateLimitEndpointGroup` values are exactly `login`, `refresh`, `register`, `password_reset`, `totp`, and `verification`.

Builder precedence is:

1. `endpoint_overrides` wins per slot and can replace the limiter or set it to `None`.
2. Otherwise, only slots enabled by `enabled` (defaults to every supported slot) and not listed in `disabled` are materialized.
3. Generated limiters start from `backend`, then `group_backends` can swap the backend for the slot's group before the builder materializes the final per-slot limiter.

Generated limiters start from the package default scope and namespace values listed above. Use
`{AuthRateLimitSlot.VERIFY_TOKEN, AuthRateLimitSlot.REQUEST_VERIFY_TOKEN}` to leave unused verification slots unset,
and keep direct `EndpointRateLimit(...)` assembly only for advanced per-endpoint
exceptions.

Example: explicit underscore namespace overrides with split group budgets. This keeps login,
register, and password-reset style routes on one backend, splits out refresh and TOTP budgets, and
leaves verification slots unset. The preset is just a higher-level wrapper around the current shared-builder surface, so the same
slot and override rules still apply.

```python
from litestar_auth.contrib.redis import RedisAuthPreset, RedisAuthRateLimitConfigOptions, RedisAuthRateLimitTier
from litestar_auth.ratelimit import AuthRateLimitSlot

redis_auth = RedisAuthPreset(
    redis=redis_client,
    rate_limit_tier=RedisAuthRateLimitTier(max_attempts=5, window_seconds=60),
    group_rate_limit_tiers={
        "refresh": RedisAuthRateLimitTier(max_attempts=10, window_seconds=300),
        "totp": RedisAuthRateLimitTier(max_attempts=5, window_seconds=300),
    },
)

rate_limit_config = redis_auth.build_rate_limit_config(
    options=RedisAuthRateLimitConfigOptions(
        disabled={AuthRateLimitSlot.VERIFY_TOKEN, AuthRateLimitSlot.REQUEST_VERIFY_TOKEN},
        trusted_proxy=True,
        trusted_proxy_hops=2,
    ),
)
```

Add `endpoint_overrides` only when an existing deployment needs a fully custom per-slot limiter or
an explicit `None` disablement beyond the shared `enabled` / `disabled` selection.

### Redis TOTP enrollment, replay protection, and pending-token deduplication {#redis-totp-replay-protection-and-pending-token-deduplication}

Use `RedisTotpEnrollmentStore` for `TotpConfig.totp_enrollment_store` when pending enrollment
state must be visible across workers or restarts, use `RedisUsedTotpCodeStore` for
`TotpConfig.totp_used_tokens_store` when TOTP codes must not be reusable across workers or
restarts, and use `RedisJWTDenylistStore` for `TotpConfig.totp_pending_jti_store` when pending
login tokens must not be replayed across workers or restarts.
`RedisAuthPreset.build_totp_enrollment_store()`, `RedisAuthPreset.build_totp_used_tokens_store()`,
and `RedisAuthPreset.build_totp_pending_jti_store()` are the shared-client path when the same
Redis client also backs auth rate limiting. The direct low-level store implementations remain
available when you intentionally want bespoke wiring or separate Redis backends.

```python
from litestar_auth import TotpConfig
from litestar_auth.authentication.strategy.jwt import RedisJWTDenylistStore
from litestar_auth.contrib.redis import RedisTotpEnrollmentStore, RedisUsedTotpCodeStore

totp_config = TotpConfig(
    totp_pending_secret="replace-with-32+-char-secret",
    totp_enrollment_store=RedisTotpEnrollmentStore(redis=redis_client),
    totp_pending_jti_store=RedisJWTDenylistStore(redis=redis_client),
    totp_used_tokens_store=RedisUsedTotpCodeStore(redis=redis_client),
)
```

`totp_pending_secret` still signs pending-2FA JWTs for the controller flow; it does not replace
server-side stores. Configure `TotpConfig.totp_enrollment_store` for pending enrollment secrets,
`TotpConfig.totp_pending_jti_store` for pending login-token JTI deduplication, and
`TotpConfig.totp_used_tokens_store` for TOTP-code replay protection.

### redis-py 8 client requirements

`litestar-auth[redis]` pins **redis-py 8.x**. The library never calls deprecated write helpers such
as `setex` or `psetex`; expiring keys use `set(..., ex=ttl_seconds)` and replay/denylist paths use
`set(..., nx=True, px=ttl_ms)` where appropriate.

If you implement a custom client for `RedisAuthClientProtocol`, mirror that surface:

| Operation | Required method | Notes |
|-----------|-----------------|-------|
| Expiring string write | `set(name, value, ex=seconds)` | Do not implement `setex` for this contract |
| Conditional write | `set(name, value, nx=True, px=ms)` | Used by TOTP replay and API-key nonce stores |
| Read | `get(name)` | |
| Delete | `delete(*names)` | |
| Lua | `eval(script, numkeys, *keys_and_args)` | Rate limiter and enrollment consume |
| Sorted sets | implicit via Lua | Rate limiter scripts only |

redis-py 8 defaults to RESP3 on the wire with legacy Python response shapes unless
`legacy_responses=False`. Stay on the default unless you deliberately migrate every Redis consumer
in your application.

See [Migration](../migration.md#redis-py-8-litestar-authredis) for upgrade steps from redis-py 7.x.

### Redis contrib import boundary

`litestar_auth.contrib.redis` is the public Redis convenience boundary. It exposes
`RedisAuthClientProtocol`, `RedisAuthPreset`, `RedisAuthRateLimitTier`,
`RedisTokenStrategy`, `RedisTotpEnrollmentStore`, and `RedisUsedTotpCodeStore`.
The high-level one-client preset lives there, while the typed slot enum, group alias, and low-level
shared-backend builder surface remain on `litestar_auth.ratelimit`.
