howdy/auth/session_store
Where sessions live. By default that is the auth database, and nothing
here is needed. Supply a SessionStore to auth.with_session_store to
keep them somewhere else instead: Redis, Valkey, DynamoDB, anything that
can hold a small record under a key and find a user’s records.
A store never sees a session token. digest is a one-way digest of it,
so what a store holds cannot be used to sign in. Users, credentials and
suspension stay in the database: every request still confirms there that
the session’s user is active.
Types
One session. Store every field and return it unchanged.
pub type Entry {
Entry(
digest: String,
user_id: String,
method: String,
created_at: Int,
last_seen_at: Int,
expires_at: Int,
client: String,
version: Int,
)
}
Constructors
-
Entry( digest: String, user_id: String, method: String, created_at: Int, last_seen_at: Int, expires_at: Int, client: String, version: Int, )Arguments
- digest
-
The key. Unique, 43 URL-safe characters.
- method
-
How the session was authenticated. Opaque text to a store.
- created_at
-
Unix seconds, as are the two below.
- expires_at
-
A store may drop the record at any time from this moment on, so it suits a native expiry such as a Redis TTL.
touchmay move it later. - version
-
Account session generation. Preserve it unchanged; older generations are refused against the database even when store cleanup fails.
The operations a store provides. Each may be called from any process, and
concurrently. Return Error(service.Internal(_)) when the backend fails:
the operation then fails closed rather than guessing. The package checks
expiry and idleness itself, so get and list may return records that
have expired, or not, as suits the backend.
pub type SessionStore {
SessionStore(
insert: fn(Entry) -> Result(Nil, service.Error),
get: fn(String) -> Result(option.Option(Entry), service.Error),
touch: fn(String, Int, Int) -> Result(Nil, service.Error),
list: fn(String) -> Result(List(Entry), service.Error),
delete: fn(String, String) -> Result(Nil, service.Error),
delete_for_user: fn(String, option.Option(String)) -> Result(
Nil,
service.Error,
),
prune: fn(Int) -> Result(Nil, service.Error),
)
}
Constructors
-
SessionStore( insert: fn(Entry) -> Result(Nil, service.Error), get: fn(String) -> Result(option.Option(Entry), service.Error), touch: fn(String, Int, Int) -> Result(Nil, service.Error), list: fn(String) -> Result(List(Entry), service.Error), delete: fn(String, String) -> Result(Nil, service.Error), delete_for_user: fn(String, option.Option(String)) -> Result( Nil, service.Error, ), prune: fn(Int) -> Result(Nil, service.Error), )Arguments
- insert
-
Add a record. Digests do not collide; overwriting is acceptable.
- get
-
The record stored under a digest.
- touch
-
Set
last_seen_at, thenexpires_at, for a digest.expires_atrepeats the stored value unlesspolicy.session_renew_secondsis renewing the session, when it is later; a backend with native expiry resets its TTL from it. Called at most about once a minute per session. An unknown digest is not an error and creates nothing. - list
-
Every record of a user id, in any order.
- delete
-
Remove the record under a digest if it belongs to the user id. A record of another user must survive. Nothing to remove is not an error.
- delete_for_user
-
Remove every record of a user id, except the digest to keep, if any.
- prune
-
Housekeeping: remove records with
expires_atat or before the given time. A backend with native expiry can do nothing.
Values
pub fn check(store: SessionStore) -> Result(Nil, String)
Exercise a store against the contract above, for an adapter’s own test
suite. Returns the first rule it breaks. It writes and removes records
under user ids beginning howdy-check-, so point it at a test backend.
pub fn memory() -> SessionStore
Sessions in this node’s memory: a reference implementation, and useful in
tests and single-node development. Sessions are lost on restart and are
not shared between nodes. The table belongs to a small process of its own,
started here unlinked, so it outlives the caller and neither takes the
other down; it runs until the VM stops. Use memory_supervised instead
when the application has a supervision tree to own it.
pub fn memory_supervised() -> #(
supervision.ChildSpecification(Nil),
SessionStore,
)
memory, with the table’s owner as a child for the application’s
supervisor. The store may be used as soon as the child has started, and
again after the supervisor restarts it, when it starts empty: sessions
die with the owner. Until then, and while the owner is down, every
operation returns Error(service.Internal(_)).