The SignedCookie primitive encodes a payload as
base64url(payload) + "." + base64url(hmac_sha256(key, base64url(payload)))
— the same shape JSON Web Signature uses, so the format is
familiar to anyone who has read RFC 7515. The MAC is computed
over the base64url-encoded payload (not the raw bytes) so the
verifier can split on . without first decoding.
Session[T] wraps an arbitrary Copyable payload
and (de)serialises it through a user-supplied SessionCodec[T].
SessionStore is the abstraction over storage; flare ships:
CookieSessionStore — payload travels in the signed cookie
itself (stateless).
InMemorySessionStore — payload kept server-side, the
cookie carries only an opaque (caller-supplied) session id.
BackedSessionStore[B: SessionBackend] — the id is a CSPRNG token
(new_session_id), the value lives in a pluggable SessionBackend
with a TTL, and destroy revokes it. MemorySessionBackend is the
reference backend; a shared/Redis/SQL backend implements the same four
methods (get / set / delete / sweep).
Sessions expire server-side by default: every store takes a ttl_s
(default :data:DEFAULT_SESSION_TTL_S, one day; 0 opts out). The
cookie stores read the wall clock themselves and also offer encode_at /
load_at / insert_at forms that take the time as an argument, the
way BackedSessionStore takes now_s.
The two cookie stores share the SessionStore read trait
(cookie_name + load) so handler code can be generic over them.
Keys are rotated by passing previous_keys; valid signatures under any
of the supplied keys are accepted (with the current key used to sign
new cookies).
Loading a session is done via store.load(req) (a
BackedSessionStore adds now_s for TTL). It is deliberately not a
config-free Extractor: an extractor only receives the Request and
cannot hold the runtime signing key / backend, so the store instance is
the injection point.
Threat model
- The signing key is the trust anchor; rotate at the cadence your
org demands. Library-level enforcement:
Session.new rejects
keys shorter than 16 bytes.
- HMAC-SHA256 verification is constant-time (
CRYPTO_memcmp);
no early-exit timing leak.
- Forged cookies (any byte tampered) raise on
decode; callers
should treat the raise as "treat user as anonymous".
Structs
StringSessionCodec
struct StringSessionCodec
The default codec: payload is just a UTF-8 string.
Pair with flare.json-encoded text payloads (or any other
string serialisation) when richer types are needed without
writing a new codec.
Implements Copyable, Defaultable, Movable, SessionCodec
encode
def encode(payload: String) -> List[UInt8]
decode
def decode(data: List[UInt8]) -> String
Session
struct Session
A typed session payload, carried in either a signed cookie or an in-memory store.
Construction: Session.empty() / Session(value).
Field value is a String to keep the public surface
simple; pair with flare.json if you need to roundtrip a
structured payload (encode JSON in the handler, decode on the
way out).
Implements Copyable, Defaultable, Movable
- Fields
value: String: Opaque session value (typically a JSON blob).present: Bool: True if the session was loaded from a valid cookie / store entry; False for anonymous requests.
__init__
def __init__(out self, value: String)
CookieSessionStore
struct CookieSessionStore
Stateless store: the entire session is encoded into the signed cookie. Suitable for small payloads (< 4 KiB).
Stores no server-side state; payload size is bounded by the
cookie size limit (RFC 6265 paragraph 6.1 recommends >= 4096
bytes per cookie).
The signed payload is "<expiry>|<value>", where <expiry> is
the absolute epoch second after which :meth:load refuses the cookie
(0 = never; only with ttl_s <= 0). The expiry is covered by
the MAC, so a client cannot extend it, and a validly signed payload
without one -- such as a cookie issued before expiry existed -- is
refused: its holder is treated as anonymous and signs in again.
Implements Copyable, Movable, SessionStore
__init__
def __init__(out self, key: List[UInt8], cookie_name: String = "flare_session", ttl_s: Int = Int(86400))
Sign and verify with key.
- Args
key: HMAC key, at least MIN_SESSION_KEY_BYTES bytes.cookie_name: Name of the session cookie.ttl_s: Lifetime of a cookie in seconds, counted from
:meth:encode. 0 (or negative) issues cookies that
never expire.
Raises Error: If key is shorter than MIN_SESSION_KEY_BYTES.
There is no default constructor: a store without a real
key cannot be made.
add_previous_key
def add_previous_key(mut self, key: List[UInt8])
Accept key as a valid signing key for inbound cookies.
Used during key rotation: set self._key to the new key,
keep the old key here so existing browsers' cookies still
verify until their natural expiry.
Raises Error: If key is shorter than MIN_SESSION_KEY_BYTES.
cookie_name
def cookie_name(self) -> String
load
def load(self, req: Request) -> Session
Look up the session cookie on req and return a Session.
Returns Session.empty() for any failure mode (cookie
missing, malformed, MAC fails, expired, no expiry). Never
raises -- handlers can treat the result as authoritative.
load_at
def load_at(self, req: Request, now_s: Int) -> Session
:meth:load with the current epoch second given by the caller.
A cookie whose expiry is <= now_s is refused.
encode
def encode(self, value: String) -> String
Return a signed-cookie value carrying value as payload, expiring ttl_s seconds from now.
encode_at
def encode_at(self, value: String, now_s: Int) -> String
:meth:encode with the current epoch second given by the caller.
InMemorySessionStore
struct InMemorySessionStore
Server-side session table keyed by signed session id.
Entries expire ttl_s seconds after they are inserted (default
:data:DEFAULT_SESSION_TTL_S; 0 = never). :meth:load refuses an
expired entry and :meth:sweep evicts them.
Concurrency note: the implementation is single-worker; for
multi-worker mode the store is per-worker (each worker keeps
its own table). A shared backend (Redis, DB) is a follow-up
behind the same trait.
Implements Copyable, Movable, SessionStore
__init__
def __init__(out self, key: List[UInt8], cookie_name: String = "flare_session", ttl_s: Int = Int(86400))
Sign and verify session ids with key.
- Args
key: HMAC key, at least MIN_SESSION_KEY_BYTES bytes.cookie_name: Name of the session cookie.ttl_s: Lifetime of an entry in seconds, counted from
:meth:insert. 0 (or negative) = never expires.
Raises Error: If key is shorter than MIN_SESSION_KEY_BYTES.
cookie_name
def cookie_name(self) -> String
insert
def insert(mut self, id: String, value: String)
Replace any existing entry for id with value; it expires ttl_s seconds from now.
insert_at
def insert_at(mut self, id: String, value: String, now_s: Int)
:meth:insert with the current epoch second given by the caller. Replacing an entry restarts its lifetime.
remove
def remove(mut self, id: String) -> Bool
Drop the entry for id. Returns True if present.
sweep
def sweep(mut self, now_s: Int) -> Int
Evict every entry expired at now_s; return the count.
encode_id
def encode_id(self, id: String) -> String
Wrap id in a signed cookie value.
load
def load(self, req: Request) -> Session
Look up the session for req; empty if the cookie is missing, forged, unknown or expired.
load_at
def load_at(self, req: Request, now_s: Int) -> Session
:meth:load with the current epoch second given by the caller.
MemorySessionBackend
struct MemorySessionBackend
Reference in-process :trait:SessionBackend with TTL expiry.
ponytail: linear-scan parallel lists (id / value / expiry). Fine for
the moderate per-worker session counts this backs; a shared/large
deployment swaps in a hashed or external backend behind the same
trait. Not thread-safe -- one instance per worker (the multi-worker
server copies the store per worker).
Implements Copyable, Defaultable, Movable, SessionBackend
get
def get(mut self, id: String, now_s: Int) -> Optional[String]
set
def set(mut self, id: String, value: String, now_s: Int, ttl_s: Int)
delete
def delete(mut self, id: String) -> Bool
sweep
def sweep(mut self, now_s: Int) -> Int
BackedSessionStore
struct BackedSessionStore[B: SessionBackend]
Server-side session store over a pluggable :trait:SessionBackend.
The cookie carries only a CSPRNG session id (signed, so a client
cannot forge one); the value lives in the backend with a TTL. This
is the store to reach for when sessions must expire, be revocable
(:meth:destroy), or share a backend across workers.
Its load / save / destroy surface is mut self + raises +
now_s (the backend mutates on lazy expiry and needs the clock),
so -- unlike the two cookie stores -- it deliberately does not
implement the minimal :trait:SessionStore read trait.
Implements Copyable, Movable
__init__
def __init__(out self, var backend: B, key: List[UInt8], cookie_name: String = "flare_session", ttl_s: Int = Int(86400))
Sign session ids with key and keep values in backend.
- Args
backend: The storage backend (ownership transferred).key: HMAC key, at least MIN_SESSION_KEY_BYTES bytes.cookie_name: Name of the session cookie.ttl_s: Lifetime of a saved session in seconds (default
:data:DEFAULT_SESSION_TTL_S). 0 (or negative) saves
sessions that never expire.
Raises Error: If key is shorter than MIN_SESSION_KEY_BYTES.
cookie_name
def cookie_name(self) -> String
load
def load(mut self, req: Request, now_s: Int) -> Session
Load the session for req (empty if no valid cookie / the backend entry is gone or expired).
save
def save(mut self, value: String, now_s: Int) -> String
Store value under a fresh CSPRNG id and return the signed session-cookie value to set on the response.
destroy
def destroy(mut self, req: Request) -> Bool
Revoke the session referenced by req's cookie; return True if a backend entry was removed.
sweep
def sweep(mut self, now_s: Int) -> Int
Evict expired backend entries; return the count.