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
create
abstractmethod
async
¶
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
|
expiration
|
int | None
|
TTL in seconds; the storage default if |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The session id the value was stored under. |
get
abstractmethod
async
¶
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
|
expiration
|
int | None
|
TTL in seconds when resetting; storage default if |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
delete
abstractmethod
async
¶
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 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
|
expiration
|
int | None
|
TTL in seconds when resetting; storage default if |
None
|
Returns:
| Type | Description |
|---|---|
T | None
|
The value as written, or |
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
¶
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
¶
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
¶
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
¶
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
¶
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
¶
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.
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 a session. user_id is accepted for the shared contract but
unused here (this backend keeps no separate per-user index).
scan_keys
async
¶
Enumerate keys by glob (e.g. "session:*"), matching the Redis
backend's semantics. None returns all keys.
get_user_sessions
async
¶
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 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
¶
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 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
¶
Atomic create-if-absent via SET key value NX EX ttl (single round trip).
get_and_delete
async
¶
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
¶
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
|
|
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 |
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 |