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. touch may 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, then expires_at, for a digest. expires_at repeats the stored value unless policy.session_renew_seconds is 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_at at 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(_)).

✨ Search Document