Skip to content

Storage

The server-side store for sessions, CSRF tokens, and one-time tokens. Pick a backend with get_session_storage(...), or implement AbstractSessionStorage for your own.

crudauth.storage.AbstractSessionStorage

AbstractSessionStorage(
    prefix: str = DEFAULT_STORAGE_PREFIX,
    expiration: int = DEFAULT_SESSION_TTL_SECONDS,
)

Bases: ABC, Generic[T]

Async key/value store for serializable Pydantic models with TTLs.

Concrete backends serialize T to JSON, key it under {prefix}{id} and honor per-key expiration.

Optional capabilities (duck-typed - implement if your backend can, callers check with hasattr):

  • async get_user_sessions(user_id) -> list[str] - index sessions by user; unlocks multi-device limits and "sign out everywhere".
  • async scan_keys(match: str | None = None) -> list[str] - enumerate keys by glob; unlocks the periodic idle-session cleanup sweep. A backend without it simply gets no proactive sweep (per-key TTLs still expire entries).
Example
storage = get_session_storage("redis", prefix="session:", redis_url=...)
sid = await storage.create(SessionData(user_id=1), expiration=1800)
data = await storage.get(sid, SessionData)

create abstractmethod async

create(
    data: T,
    session_id: str | None = None,
    expiration: int | None = None,
) -> str

Store data under session_id (generated if omitted) with a TTL.

Parameters:

Name Type Description Default
data T

The Pydantic model to serialize.

required
session_id str | None

Key to store under; a fresh UUID if None.

None
expiration int | None

TTL in seconds; the storage default if None.

None

Returns:

Type Description
str

The session id the value was stored under.

get abstractmethod async

get(session_id: str, model_class: type[T]) -> T | None

Load and deserialize a value into model_class, or None if absent/expired.

update abstractmethod async

update(
    session_id: str,
    data: T,
    reset_expiration: bool = True,
    expiration: int | None = None,
) -> bool

Overwrite an existing value.

Parameters:

Name Type Description Default
session_id str

Key to update.

required
data T

New value to serialize.

required
reset_expiration bool

If True, refresh the TTL to expiration.

True
expiration int | None

TTL in seconds when resetting; storage default if None.

None

Returns:

Type Description
bool

True if the key existed and was updated, False otherwise.

delete abstractmethod async

delete(session_id: str, user_id: Any = None) -> bool

Delete a session. user_id, when known by the caller, lets indexed backends skip re-reading the record to update their per-user index.

extend abstractmethod async

extend(
    session_id: str, expiration: int | None = None
) -> bool

Extend a key's TTL.

Note

expiration=None falls back to the storage default ([expiration][crudauth.storage.base.AbstractSessionStorage.expiration]), NOT the caller's session window - callers that slide a specific window (e.g. the CSRF-token TTL) must pass an explicit expiration.

set_if_absent async

set_if_absent(
    session_id: str, data: T, expiration: int | None = None
) -> bool

Atomically store data under session_id only if it is absent.

Returns True if this call created the entry (the caller "won"), False if it already existed. This is the primitive behind a correct one-time-use token guard: a plain exists then create is a check-then-set race on a networked backend.

Note

This default implementation is only atomic on a backend whose exists/create never yield to the event loop (the in-memory backend). Networked backends MUST override it with a native atomic operation (Redis SET NX); see RedisSessionStorage.

get_and_delete async

get_and_delete(
    session_id: str, model_class: type[T]
) -> T | None

Atomically read and delete an entry, returning it (or None).

Lets a single-use value (e.g. OAuth state) be consumed exactly once even under concurrent callbacks. As with set_if_absent, the default is only atomic on the in-memory backend; networked backends override it.

get_user_sessions async

get_user_sessions(user_id: Any) -> list[str]

Optional: session ids belonging to user_id.

Implement when the backend can index by user - unlocks multi-device limits and "sign out everywhere". Meaningful only for user_id-bearing models. Raises NotImplementedError when unsupported.

scan_keys async

scan_keys(match: str | None = None) -> list[str]

Optional: enumerate stored keys by glob.

Unlocks the periodic idle-session cleanup sweep. A backend without it gets no proactive sweep (per-key TTLs still expire entries). Raises NotImplementedError when unsupported.

delete_pattern async

delete_pattern(pattern: str) -> int

Optional: delete keys by prefix. Raises NotImplementedError when unsupported. Never point this at the login:* lockout keys - bulk-deleting them would clear an attacker's accumulated failures.

initialize async

initialize() -> None

Open connections / warm up. Default is a no-op.

close async

close() -> None

Release resources. Default is a no-op.

crudauth.storage.MemorySessionStorage

MemorySessionStorage(
    prefix: str = DEFAULT_STORAGE_PREFIX,
    expiration: int = DEFAULT_SESSION_TTL_SECONDS,
)

Bases: AbstractSessionStorage[T]

Dict-backed storage. Not shared across processes - never use in production.

Note

Eviction is lazy (on access to a key) plus an occasional full sweep on write, so abandoned keys don't accumulate unboundedly the way a purely-lazy dict would. It is still single-process and unsuitable for production - use the redis backend there.

delete async

delete(session_id: str, user_id: Any = None) -> bool

Delete a session. user_id is accepted for the shared contract but unused here (this backend keeps no separate per-user index).

scan_keys async

scan_keys(match: str | None = None) -> list[str]

Enumerate keys by glob (e.g. "session:*"), matching the Redis backend's semantics. None returns all keys.

get_user_sessions async

get_user_sessions(user_id: Any) -> list[str]

Scan all sessions and return ids belonging to user_id.

Note

Reads the user_id field straight out of the serialized payload (the storage layer is model-agnostic, so it can't go through the model). The comparison is on the stringified id, matching how the Redis backend keys its per-user index - so a UUID PK (which a JSON round-trip turns into a string) still matches the UUID object the caller passes. Meaningful only for user_id-bearing models; entries without that field are skipped.

delete_pattern async

delete_pattern(pattern: str) -> int

Delete keys whose name starts with pattern (matches {pattern}*), the same prefix semantics as the Redis backend.

crudauth.storage.RedisSessionStorage

RedisSessionStorage(
    prefix: str = DEFAULT_STORAGE_PREFIX,
    expiration: int = DEFAULT_SESSION_TTL_SECONDS,
    redis_url: str | None = None,
    client: Any = None,
    **_: Any,
)

Bases: AbstractSessionStorage[T]

Async Redis backend with a per-user session index for fast enumeration.

Layout
  • {prefix}{session_id} -> serialized model (TTL = expiration)
  • {prefix_root}_users:{user_id} -> SET of session ids (TTL = expiration + 1h)
Note

Pass an existing client= to share one connection pool with other redis-backed components (e.g. the rate-limiter backend); otherwise each constructs its own pool to the same server.

delete async

delete(session_id: str, user_id: Any = None) -> bool

Delete a session and drop it from its owner's index.

Note

When user_id is given (the indexed terminate paths know it), the owner read is skipped entirely. When it's None (e.g. logout with only a cookie), the record is read once to find the owner so the user index stays consistent. The index assumes a user_id-bearing model; for other models nothing is read or indexed.

set_if_absent async

set_if_absent(
    session_id: str, data: T, expiration: int | None = None
) -> bool

Atomic create-if-absent via SET key value NX EX ttl (single round trip).

get_and_delete async

get_and_delete(
    session_id: str, model_class: type[T]
) -> T | None

Atomic read-and-delete via a MULTI/EXEC pipeline (portable; no GETDEL dependency).

Note

Used for single-use values without a per-user index (OAuth state), so it bypasses the user-index bookkeeping in delete.

crudauth.storage.get_session_storage

get_session_storage(
    backend: str = BACKEND_MEMORY,
    *,
    prefix: str = DEFAULT_STORAGE_PREFIX,
    expiration: int = DEFAULT_SESSION_TTL_SECONDS,
    redis_url: str | None = None,
    **kwargs: Any,
) -> AbstractSessionStorage[Any]

Construct a storage backend by name.

Parameters:

Name Type Description Default
backend str

"memory" (default, dev/testing) or "redis" (production).

BACKEND_MEMORY
prefix str

Key namespace prefix.

DEFAULT_STORAGE_PREFIX
expiration int

Default TTL in seconds.

DEFAULT_SESSION_TTL_SECONDS
redis_url str | None

Connection URL, required for backend="redis".

None

Returns:

Type Description
AbstractSessionStorage[Any]

An AbstractSessionStorage for the requested backend.

Raises:

Type Description
ValueError

If backend is not "memory" or "redis".