Docs menu: vestibule/state_store

vestibule/state_store

Single-use storage for in-flight OAuth flow state (CSRF `state` and PKCE `code_verifier`). Entries are deleted on first read to prevent replay.

vestibule/state_store

Single-use storage for in-flight OAuth flow state (CSRF state and PKCE code_verifier). Entries are deleted on first read to prevent replay.

Every entry is bound to the provider that started the flow, and reading it back requires naming the same provider. Without that binding a session minted for provider A would satisfy provider B’s callback, letting an attacker-controlled provider redirect the browser to another provider’s callback with the still-valid state (an OAuth mix-up attack that ends in login CSRF or account-linking takeover).

This is the shared store used by transport packages (vestibule_wisp, vestibule_mist, etc.). Applications that load more than one transport share a single ETS owner process and may share a store.

The owner process is started lazily and is not supervised. If it dies, every in-flight session is lost (those logins fail and users retry), but the store heals itself: the next operation on an existing handle respawns the owner and recreates the table with the same capacity, so a crash never leaves authentication broken until the VM restarts.

Capacity and expiry

Starting a flow is an unauthenticated operation, so the store bounds what a client can pin in memory: every store has a maximum number of live entries (create_with_capacity, default 100 000), and a store that is full refuses new flows with StoreFull rather than growing. Expired entries are rejected on read, reclaimed on demand when the store is at capacity, and swept periodically by the owner process; inserts themselves are O(1). Rate-limit the request endpoint upstream if you need a stronger guarantee than the capacity cap provides.

Types

StateStore

The state store table.

The concrete storage implementation is intentionally opaque so the public API can evolve without exposing the underlying table representation.

pub type StateStore

StateStoreError

Errors returned by checked state store operations.

The reason fields carry the raw failure reason reported by the underlying ETS owner process, to aid debugging failures that do not map to a more specific variant.

pub type StateStoreError {
OwnerUnavailable
OperationTimedOut
TableAlreadyExists
TableCreateFailed(reason: String)
TableNotFound
InsertFailed(reason: String)
CleanupFailed(reason: String)
StoreFull
InvalidCapacity
}

Constructors

StoreFull

The store holds max_entries live sessions and no expired ones could be reclaimed. New flows are refused until sessions are consumed or expire.

InvalidCapacity

create_with_capacity was given a max_entries of zero or less.

Functions

consume

Consume a CSRF state, code verifier, and optional nonce by session ID.

Returns Error(Nil) if not found, expired, already consumed, or stored for a different provider. The entry is removed in every case, so a cross-provider attempt burns the session rather than leaving it usable.

pub fn consume(
StateStore,
String,
provider: String
) -> Result(#(String, String, option.Option(String)), Nil)

create

Create the state store. Call once per VM at application startup; the returned table handle is needed by store and consume.

pub fn create() -> Result(StateStore, StateStoreError)

create_named

Create a named state store with the default capacity. Returns Error(TableAlreadyExists) if the table already exists, or another StateStoreError if the owner process or ETS operation fails.

pub fn create_named(String) -> Result(StateStore, StateStoreError)

create_with_capacity

Create a named state store that holds at most max_entries live sessions. Once full, store fails with StoreFull until sessions are consumed or expire. Returns Error(InvalidCapacity) when max_entries is not positive.

pub fn create_with_capacity(
name: String,
max_entries: Int
) -> Result(StateStore, StateStoreError)

peek

Look up a CSRF state, code verifier, and optional nonce by session ID without consuming it.

Expired sessions are treated as missing and removed from the store. A session stored for a different provider is treated as missing but left in place, so a wrong-provider probe cannot burn a legitimate in-flight login.

pub fn peek(
StateStore,
String,
provider: String
) -> Result(#(String, String, option.Option(String)), Nil)

store

Store a CSRF state value, PKCE code verifier, and optional OIDC nonce for a flow started with provider, returning a session ID.

provider is the strategy’s provider name; consume and peek must be called with the same value.

pub fn store(
StateStore,
provider: String,
state: String,
code_verifier: String,
nonce: option.Option(String)
) -> Result(String, StateStoreError)

store_with_ttl

Store a CSRF state value, PKCE verifier, and optional OIDC nonce for a flow started with provider, with a TTL, returning a session ID.

pub fn store_with_ttl(
StateStore,
provider: String,
state: String,
code_verifier: String,
nonce: option.Option(String),
ttl_seconds: Int
) -> Result(String, StateStoreError)

sweep_expired

Remove every expired session from the store now, returning how many were removed. The owner process does this on a timer and on demand when the store is at capacity, so calling it is optional.

pub fn sweep_expired(StateStore) -> Result(Int, StateStoreError)