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).
  • async remove_from_user_index(user_id, session_id) -> None - drop an index entry whose record is gone. A backend with a separate per-user index implements it so expired sessions don't pile up there.
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.

modify async

modify(
    session_id: str,
    model_class: type[T],
    change: Callable[[T], None],
    reset_expiration: bool = True,
    expiration: int | None = None,
) -> T | None

Apply change to a stored value and write it back without losing a concurrent write.

Two writers that each change a different field of the same value both land: a write made after this call read the value makes it read again and reapply change, so change may run more than once and must only set what it owns.

Parameters:

Name Type Description Default
session_id str

Key to modify.

required
model_class type[T]

Model to deserialize into.

required
change Callable[[T], None]

Mutates the loaded value in place.

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
T | None

The value as written, or None if the key doesn't exist.

Note

This default implementation is only atomic on a backend whose get/update never yield to the event loop (the in-memory backend). Networked backends MUST override it with a native compare-and-set; see RedisSessionStorage.

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.

remove_from_user_index async

remove_from_user_index(
    user_id: Any, session_id: str
) -> None

Optional: drop session_id from user_id's index once its record is gone.

Callers use it when an indexed id points at a missing record (an expired session). The default does nothing, which suits a backend that derives the index from the records themselves.

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 = the longest member's expiration + 1h, only ever extended)
Note

No transaction spans more than one key, so the backend works on Redis Cluster, and a live session is never missing from its owner's index: the index is written before a record, an index entry is only removed with the record it points at, and every activity update puts the session back in the index and extends the index's TTL. A leftover entry points at a record that no longer exists; the session manager removes it the next time it reads the user's sessions. Needs Redis 7.0+ for EXPIRE NX/GT.

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.

initialize async

initialize() -> None

Check the connection, failing loudly on a server older than Redis 7.0.

update async

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

Overwrite a live session; False when it no longer exists.

Note

The write is SET ... XX, so a session deleted concurrently (a logout-all racing a request) stays deleted instead of being re-created.

modify async

modify(
    session_id: str,
    model_class: type[T],
    change: Callable[[T], None],
    reset_expiration: bool = True,
    expiration: int | None = None,
) -> T | None

Compare-and-set update: WATCH the key, apply change, write in MULTI.

Note

A write to the key between the read and EXEC aborts the transaction, and the value is read and changed again. The transaction touches one key, so it stays within one hash slot on Redis Cluster. A key deleted meanwhile stays deleted and None is returned.

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.

Note

The index entry is removed only when a record was actually deleted, so a delete that runs between a new session's index write and its record write can't leave that session live but unindexed.

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.

remove_from_user_index async

remove_from_user_index(
    user_id: Any, session_id: str
) -> None

Drop an index entry whose record expired.

Note

A login writes its index entry before its record, so a prune can land in between and unindex a session that is about to exist. Its first activity update puts it back.

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,
    client: Any = 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 for backend="redis" when no client is given; localhost when omitted.

None
client Any

Existing async Redis client. The caller owns its lifecycle.

None

Returns:

Type Description
AbstractSessionStorage[Any]

An AbstractSessionStorage for the requested backend.

Raises:

Type Description
ValueError

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