authstate

package
v1.0.217 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 2 Imported by: 0

Documentation

Overview

Package authstate is the bounded store for short-lived interactive-login callback state: the OIDC PKCE verifier + nonce, and the SAML AuthnRequest ID. It is a self-contained stdlib-only leaf per ADR-0002.

Why this is its own engine

The state it holds is created SPECULATIVELY, on an UNAUTHENTICATED request. Culvert mints an entry every time it resolves a captive-portal login URL for a client that has not authenticated yet — that resolution happens on the proxy's no-credentials path (proxy.go) and on the public /auth/select page. So the population of this store is driven by whoever can send the gateway a request, not by whoever can log in.

That makes the EVICTION POLICY a security control, not a housekeeping detail. Both stores previously evicted "one arbitrary entry" — a Go map range that stops after the first key, i.e. a uniformly random live entry. Under a flood from a single unauthenticated source, every insertion past the cap destroyed a random OTHER user's in-flight login state, so real users' callbacks came back "invalid or expired state" and nobody could complete an SSO login. Failure was closed (no bypass), but the whole gateway's authentication was remotely deniable by an anonymous client.

The policy

Entries are attributed to a CLIENT KEY, and eviction always takes the OLDEST entry of the client holding the MOST live entries (ties broken by the oldest entry, then by client key, so the choice is deterministic and testable).

The property that buys: a client can only evict its own state until it is no longer the largest holder. One flooding source therefore evicts ITSELF, and a victim who holds a single entry is untouchable until every other client is down to one entry too — i.e. an attacker needs as many distinct client keys as the cap before it can displace one honest login. That is the difference between "one host with a for-loop" and "a thousand distinct source addresses", on a control whose failure mode is a fleet-wide login outage.

Nothing else about the contract changes: entries still expire on a TTL, the store is still hard-capped, lookups are still exact-match on an unguessable state token, and an evicted or expired entry still fails the callback CLOSED.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Store

type Store[T any] struct {
	// contains filtered or unexported fields
}

Store holds short-lived login-callback state of type T, keyed by an unguessable state token, bounded by a TTL and a hard entry cap.

The zero value is not usable; construct with New or NewWithClock. All methods are safe for concurrent use.

func New

func New[T any](ttl time.Duration, maxEntries int) *Store[T]

New returns a store bounded by ttl and maxEntries, using the wall clock. A non-positive maxEntries is treated as 1 (a store that cannot hold anything is a silent outage; a store of one is at least honest about the bound).

func NewWithClock

func NewWithClock[T any](ttl time.Duration, maxEntries int, now func() time.Time) *Store[T]

NewWithClock is New with an injected clock, so expiry and eviction ordering are deterministic under test.

func (*Store[T]) Clients

func (s *Store[T]) Clients() int

Clients returns the number of distinct client keys currently holding at least one entry. Exported for the fairness tests and for operator-facing occupancy reporting.

func (*Store[T]) Evictions

func (s *Store[T]) Evictions() uint64

Evictions returns the number of entries dropped to make room. A rising counter means login state is being displaced before it could be used — the signal that the cap is too small for the deployment, or that something is flooding the login path.

func (*Store[T]) Len

func (s *Store[T]) Len() int

Len returns the number of entries currently held (expired-but-not-yet-swept entries included; they are indistinguishable from live ones for capacity).

func (*Store[T]) Peek

func (s *Store[T]) Peek(key string) (T, bool)

Peek returns the value for key without consuming it. An expired entry is removed and reported as absent.

func (*Store[T]) Pop

func (s *Store[T]) Pop(key string) (T, bool)

Pop consumes the entry for key. A found-but-expired entry is consumed and reported as absent — single-use semantics hold either way, so a replayed state token can never be reused after its first callback.

func (*Store[T]) Set

func (s *Store[T]) Set(key, client string, val T)

Set stores val under key, attributed to client.

client is an opaque fairness key — it is never compared to anything a caller supplies later and never reaches a lookup, so a wrong or empty value can only make eviction less fair, never admit an entry that should not be admitted. An empty client is a legitimate value: all such entries share one bucket and evict each other.

Re-Setting an existing key replaces it (and re-attributes it), matching the map-assignment semantics this store replaced.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL