Documentation
¶
Overview ¶
Package session stores typed per-browser state. It knows nothing about login: what proved a session, how strongly, and for how long belongs to whatever owns authentication, which is normally popcornweb/plugin/auth.
An application declares each piece of state once, as a Go type with a Placement, and reads it back by that type:
session.Register[Cart](registry, "cart", session.Private, nil) cart, ok := session.Load[Cart](ctx)
The Placement states what the client may do with the value and where its bytes live. Shared is a plain cookie the front end reads and writes, ReadOnly a signed one it may read, Private is sealed and moves from a cookie to the configured backend at the login rotation, ServerOnly is sealed and always on the server because it must stay revocable, and RequestScope lives in process memory for one request and is never persisted at all. A host may deliberately place anonymous Private state on its server too, as the dev memory backend does.
The browser receives only a random token, issued lazily on the first write, so a visitor who writes nothing receives no cookie and occupies no storage. Handlers never observe the token, the key hash, the placement, or the backend client.
Jar remains for one typed cookie deliberately kept outside the session, such as a sign-in hint that has to survive the logout it describes.
Index ¶
- Constants
- Variables
- func Load[T any](ctx context.Context) (T, bool)
- func ParseSameSite(value string) (http.SameSite, error)
- func Present(ctx context.Context) bool
- func Register[T any](registry *Registry, key string, placement Placement, codec Codec[T], ...) error
- func WithValue[T any](ctx context.Context, value T) context.Context
- type Backend
- type Carrier
- type Codec
- type CookieMode
- type CookieOptions
- type CookieStore
- func (s *CookieStore) BindRequest(ctx context.Context, carrier Carrier) context.Context
- func (s *CookieStore) CookieName() string
- func (s *CookieStore) Delete(ctx context.Context, keyHash string) error
- func (s *CookieStore) Get(ctx context.Context, keyHash string) (RawRecord, error)
- func (s *CookieStore) Put(ctx context.Context, keyHash string, record RawRecord) error
- func (s *CookieStore) Touch(ctx context.Context, keyHash string, lastSeenAt, idleExpiresAt time.Time) error
- type CookieStoreOptions
- type JSONCodec
- type Jar
- func (j *Jar[T]) Clear(w http.ResponseWriter)
- func (j *Jar[T]) ClearFrom(carrier Carrier)
- func (j *Jar[T]) Load(r *http.Request) (T, error)
- func (j *Jar[T]) LoadFrom(carrier Carrier) (T, error)
- func (j *Jar[T]) Middleware() func(http.Handler) http.Handler
- func (j *Jar[T]) Mode() CookieMode
- func (j *Jar[T]) Name() string
- func (j *Jar[T]) Read(ctx context.Context) (T, bool)
- func (j *Jar[T]) Save(w http.ResponseWriter, value T) error
- func (j *Jar[T]) SaveTo(carrier Carrier, value T) error
- func (j *Jar[T]) Value(ctx context.Context) (*JarValue[T], bool)
- type JarOptions
- type JarValue
- type Keyring
- type Manager
- func (m *Manager) Attach(w http.ResponseWriter, r *http.Request) (*http.Request, error)
- func (m *Manager) AttachTo(carrier Carrier) (Resolved, error)
- func (m *Manager) CookieName() string
- func (m *Manager) Destroy(w http.ResponseWriter, r *http.Request) error
- func (m *Manager) DestroyOn(ctx context.Context) error
- func (m *Manager) Middleware(unavailable UnavailableHandler) func(http.Handler) http.Handler
- func (m *Manager) Resolve(carrier Carrier) (Resolved, error)
- func (m *Manager) Rotate(w http.ResponseWriter, r *http.Request) error
- func (m *Manager) RotateOn(ctx context.Context) error
- type MemoryStore
- func (s *MemoryStore) Delete(ctx context.Context, keyHash string) error
- func (s *MemoryStore) Get(ctx context.Context, keyHash string) (RawRecord, error)
- func (s *MemoryStore) Put(ctx context.Context, keyHash string, record RawRecord) error
- func (s *MemoryStore) Touch(ctx context.Context, keyHash string, lastSeenAt, idleExpiresAt time.Time) error
- type Options
- type Placement
- type RawRecord
- type RawStore
- type Record
- type Registry
- type RequestBinder
- type Resolved
- type Slot
- type SlotOption
- type Store
- type UnavailableHandler
- type ValueStore
Constants ¶
const BrowserMax = 400 * 24 * time.Hour
BrowserMax is the longest a browser will keep a cookie.
There is no "never expires" in HTTP: a cookie with no Max-Age dies when the browser closes, and one with a Max-Age is capped. Current browsers cap it at 400 days, so this is what "keep it as long as you can" actually means.
const (
// DefaultCookieName is used when Options.Cookie.Name is empty.
DefaultCookieName = "pw_session"
)
const DefaultDataCookieName = "pw_session_data"
DefaultDataCookieName holds the sealed record of a CookieStore. It is a second cookie beside the token cookie of the Manager, because the token names the record and this one carries it.
const ( // DefaultMaxCookieBytes bounds the encoded value of a Jar or CookieStore // cookie. Browsers commonly accept about 4096 bytes per cookie including // its name and attributes, so the default leaves room for both. DefaultMaxCookieBytes = 3800 )
Variables ¶
var ( // ErrCookieMissing reports that the request carries no such cookie. ErrCookieMissing = errors.New("session: cookie not present") // ErrCookieInvalid reports a value this keyring did not produce: a changed // payload, a foreign encoding, a value moved from another cookie name, or // one written under a retired key. ErrCookieInvalid = errors.New("session: cookie value is not authentic") // ErrCookieTooLarge reports an encoded value beyond the configured budget. // Browsers drop an oversized cookie silently, so it is refused at the // write instead. ErrCookieTooLarge = errors.New("session: cookie value exceeds the size limit") )
var ( ErrNotFound = errors.New("session: record not found") ErrExpired = errors.New("session: record expired") ErrCodec = errors.New("session: codec failure") ErrInvalidOptions = errors.New("session: invalid options") ErrInvalidKey = errors.New("session: invalid key") ErrNoSession = errors.New("session: no session on request") )
Functions ¶
func Load ¶
Load returns the value of the registered slot for T and whether the request carried one.
func ParseSameSite ¶
ParseSameSite reads the configured same-site policy.
It lives here rather than beside one caller because CookieOptions is this package's type: a second parser elsewhere would be a second place for the accepted spellings to drift.
func Present ¶
Present reports whether the request carries a session token at all. It is what a logout endpoint checks before doing work, and it is not an authentication claim: an anonymous browser holding a cart has a session.
func Register ¶
func Register[T any](registry *Registry, key string, placement Placement, codec Codec[T], options ...SlotOption) error
Register declares one piece of per-browser state.
key is the browser cookie name for a cookie-placed slot, and the field name inside the session record for a server-placed one. codec may be nil, which uses JSONCodec[T].
Call it from main, after every package init has run, exactly as RegisterConfig requires: the registry must be complete before the first request decodes anything, and an init-time call cannot see the configuration that places it.
A duplicate Go type and a duplicate key are each an error rather than a silent replacement.
func WithValue ¶
WithValue returns ctx carrying value as the slot for T, exactly as the middleware would have resolved it, without a manager behind it.
It is the seam a test uses to run a handler against a session it did not have to establish. A slot installed this way is read-only: Set and Clear refuse, because nothing here reaches a browser or a store.
Types ¶
type Backend ¶
type Backend struct {
// Store is required.
Store RawStore
// Close releases a client the backend opened. A backend that borrowed a
// resource from its host leaves it nil: what it did not open, it does not
// close.
Close func(context.Context) error
// Prune is the expiry sweep of a backend that accumulates records. A
// backend whose server or browser forgets records on its own leaves it
// nil, and its host schedules nothing.
Prune func(ctx context.Context, now time.Time, limit int) (int64, error)
}
Backend is one opened storage backend together with the responsibilities it brings. A host reads capabilities from this value instead of type-asserting a plugin type, so adding a backend changes no host.
type Carrier ¶
type Carrier interface {
// Cookies returns the cookies the request carried.
Cookies() []*http.Cookie
// SetCookie adds one Set-Cookie to the response.
SetCookie(cookie *http.Cookie)
// Context is the request's context, which is what a store is reached
// through.
Context() context.Context
}
Carrier is everything a session needs from the transport it is served over.
It is three methods because that is all a session actually touches: it reads the cookies that arrived, it sets the cookies that leave, and it needs the request's context to reach a store. Everything else this package does — the token, the sealing, the slot map, the rotation rules, the deadlines — is arithmetic over those.
Having found that out, the interface is what lets a second transport carry a session without a second implementation of any of it. The alternative was a parallel copy of the lifecycle, and a session is the last thing that should exist twice: two implementations of when a token rotates, or of which cookie is cleared on a stale record, are two chances to leave a session valid that should have ended.
The cookie type is net/http's. It is a plain data struct with no behaviour and no transport in it — a name, a value, and the attributes a browser is told — so it describes a cookie rather than implementing one, and a transport that spells cookies differently translates at its own edge.
func HTTPCarrier ¶
func HTTPCarrier(w http.ResponseWriter, r *http.Request) Carrier
HTTPCarrier carries a session over net/http.
type Codec ¶
Codec serializes the typed payload of a record for durable stores. Record timestamps stay in backend fields so renewal never rewrites the payload. Implementations must use a bounded format and must not include payload contents in returned errors.
type CookieMode ¶
type CookieMode int
CookieMode selects how much protection a cookie value carries. A stronger mode never changes the typed API, so a cookie can be promoted from Plain to Sealed without touching the handlers that read it.
const ( // CookiePlain writes the payload without protection. The client can read // it and can replace it, so a handler must treat the decoded value as // request input rather than as application state. CookiePlain CookieMode = iota + 1 // CookieSigned appends a message authentication code. The client can still // read the payload but cannot change it, and a changed value is rejected // instead of being decoded. CookieSigned // CookieSealed encrypts and authenticates the payload. The client can // neither read nor change it. CookieSealed )
func (CookieMode) String ¶
func (m CookieMode) String() string
String names the mode for diagnostics. It never renders a key or a value.
type CookieOptions ¶
type CookieOptions struct {
Name string
Path string
Domain string
Secure bool
HTTPOnly bool
SameSite http.SameSite
}
CookieOptions is the browser cookie policy of a Manager or a Jar. Secure defaults to true; only an explicit loopback development deployment should disable it.
type CookieStore ¶
type CookieStore struct {
// contains filtered or unexported fields
}
CookieStore keeps session records in a sealed browser cookie instead of in a backend. It implements RawStore and RequestBinder, so a Manager built over Typed of it behaves like one built over sessionstore/sqlite: the same Options, the same Create, Rotate, and Delete, and the same Read[T] in handlers. Moving a deployment to a persistent backend later replaces the store and nothing else.
The record is sealed under the hash of the session token, so a record cookie is only readable together with the token cookie it was issued with, and a record captured from one browser cannot be presented with another token.
What a browser holds, a browser keeps. This store cannot revoke a record it has already written: Delete expires the client copy, and a client that kept one can replay it until its sealed expiry passes. A deployment that must be able to end a session immediately, or whose payload outgrows a cookie, uses a server-side store instead.
func NewCookieStore ¶
func NewCookieStore(options CookieStoreOptions) (*CookieStore, error)
NewCookieStore validates options and returns a store. Wrap it with Typed to give a Manager the payload type it stores.
func (*CookieStore) BindRequest ¶
BindRequest implements RequestBinder.
func (*CookieStore) CookieName ¶
func (s *CookieStore) CookieName() string
CookieName reports the record cookie name.
func (*CookieStore) Delete ¶
func (s *CookieStore) Delete(ctx context.Context, keyHash string) error
Delete expires the record cookie. It is idempotent, and it only reaches the browser that made this request: a copy taken earlier stays valid until its sealed expiry.
func (*CookieStore) Get ¶
Get returns the record the request carries under keyHash. A missing or unauthentic cookie is ErrNotFound: it is stale browser state, not a backend failure, and the request continues unauthenticated.
type CookieStoreOptions ¶
type CookieStoreOptions struct {
// Keys seals every record. Its first secret writes; the rest keep records
// written before a rotation readable.
Keys *Keyring
// Cookie is the policy of the record cookie. Name defaults to
// DefaultDataCookieName; the remaining fields normally repeat the policy
// of the session cookie so both expire under the same rules.
Cookie CookieOptions
// MaxBytes bounds the cookie name and encoded record together. It defaults
// to DefaultMaxCookieBytes.
MaxBytes int
Now func() time.Time
Random io.Reader
}
CookieStoreOptions configures a CookieStore. Keys is required.
type JSONCodec ¶
type JSONCodec[T any] struct{}
JSONCodec is the default payload codec for durable stores. Record timestamps stay in backend columns or fields, so only the typed application payload is serialized here.
type Jar ¶
type Jar[T any] struct { // contains filtered or unexported fields }
Jar reads and writes one typed browser cookie under one protection mode.
The typed API is the same in all three modes, so a cookie that starts as CookiePlain during development can be promoted to CookieSigned or CookieSealed later without changing the handlers that use it. A value written under another name, another mode, or another keyring is rejected rather than decoded.
A Jar is safe for concurrent use.
func NewJar ¶
func NewJar[T any](codec Codec[T], options JarOptions) (*Jar[T], error)
NewJar validates options and returns a Jar over codec. A nil codec uses JSONCodec[T].
func (*Jar[T]) Clear ¶
func (j *Jar[T]) Clear(w http.ResponseWriter)
Clear expires the cookie in the browser.
func (*Jar[T]) Load ¶
Load decodes the cookie of r.
It reports ErrCookieMissing when the request carries no such cookie, ErrCookieInvalid for a value this jar did not write, ErrExpired for a protected value past its embedded expiry, and ErrCodec for a payload the codec cannot decode.
func (*Jar[T]) LoadFrom ¶
LoadFrom is Load over a carrier, so a transport that spells cookies differently reads the same jar.
func (*Jar[T]) Middleware ¶
Middleware decodes the jar cookie once per request and publishes the handle returned by Value.
A missing cookie continues with an absent value. A value this jar did not write, or one past its expiry, is cleared from the browser and continues as absent: stale client state is not an error the application has to handle.
func (*Jar[T]) Read ¶
Read returns the decoded value of the current request. It reports false when the request carried no usable value or when the jar's middleware did not run.
func (*Jar[T]) Save ¶
func (j *Jar[T]) Save(w http.ResponseWriter, value T) error
Save encodes value and writes the cookie. It must run before the response body is committed, like any other header write.
type JarOptions ¶
type JarOptions struct {
// Mode selects the protection of the value. It defaults to CookieSealed,
// so a jar declared without a mode does not silently hand application
// state to the client.
Mode CookieMode
// Keys protects signed and sealed values. Its first secret writes; the
// rest keep a rotation readable.
Keys *Keyring
// Cookie is the browser cookie policy. Name has no default.
Cookie CookieOptions
// MaxAge bounds how long the browser keeps the value and, in a protected
// mode, how long this process accepts it. Zero writes a session cookie
// that the browser drops when it closes.
MaxAge time.Duration
// MaxBytes bounds the cookie name and encoded value together. It defaults
// to DefaultMaxCookieBytes.
MaxBytes int
Now func() time.Time
Random io.Reader
}
JarOptions configures a Jar. Name is required; every other field has a safe default. Signed and Sealed additionally require Keys.
type JarValue ¶
type JarValue[T any] struct { // contains filtered or unexported fields }
JarValue is the request-scoped handle to one jar cookie. The middleware decodes the cookie once and installs it, so repeated reads in one request cost nothing and a write is visible to later handlers immediately.
func (*JarValue[T]) Clear ¶
func (v *JarValue[T]) Clear()
Clear expires the cookie and makes the request look like it carried none.
type Keyring ¶
type Keyring struct {
// contains filtered or unexported fields
}
Keyring holds the secret that protects signed and sealed cookies. The first secret writes every new value; the remaining ones only read, which is what makes a key rotation invisible to a browser that still holds a value written under the previous secret.
A secret is 32 or more random bytes and is never logged.
func NewKeyring ¶
NewKeyring returns a keyring over raw secrets. The first secret is the writing key and the rest are accepted for reading during a rotation.
func ParseKeyring ¶
ParseKeyring is NewKeyring over base64 secrets, which is the form a configuration file or environment variable carries. Standard and URL alphabets are both accepted, with or without padding.
Generate one with `openssl rand -base64 32`.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the opaque token that names one browser and applies the lifetime it is handed, without knowing what any slot means.
Handlers read and write state through Load and Value and never call the manager. Rotate and Destroy are called by whatever owns the login, which is normally popcornweb/plugin/auth.
func NewManager ¶
NewManager validates options and returns a Manager over registry.
backend is the configured server store. A nil backend selects the cookie backend, where nothing is kept on the server and promotion has nowhere to go. Registering further slots after this call is refused.
func (*Manager) Attach ¶
Attach resolves the session of r and returns the request carrying it, for a caller that reaches the manager outside its middleware.
Middleware is the normal path and does this for every request. Attach exists for the callers that legitimately have no middleware above them, such as a test seam that establishes a session against a bare request. Attaching twice is a no-op: the request keeps the session it already has.
func (*Manager) AttachTo ¶
AttachTo is Attach over a carrier: it returns the session already attached to the carrier's context, and otherwise resolves one.
The caller publishes the result, through Attach for a transport that derives a context and through StoreOn for one that writes into its request value. Which of those it is, is the only part that differs.
func (*Manager) CookieName ¶
CookieName reports the configured token cookie name.
func (*Manager) Destroy ¶
Destroy ends the whole session: every record is revoked and every cookie the session owns is expired, whatever placement each slot carries.
A cookie a deployment wants to survive this is a Jar cookie and not a registered slot, which is why a sign-in hint lives outside the session.
func (*Manager) DestroyOn ¶
DestroyOn is Destroy over the context the session was attached to, on the same terms as RotateOn.
func (*Manager) Middleware ¶
Middleware resolves the session token into the registered slots.
A missing token is a browser with no session yet, not a failure. A malformed or expired one is cleared and the request continues with no session. A backend failure is answered by unavailable without calling the next handler.
func (*Manager) Resolve ¶
Resolve reads the session a carrier presents, and reports the failure that should be answered rather than continued through.
It is the whole of what Middleware does apart from wiring, and it exists separately so a second transport carries a session without a second copy of any of this. The rules a copy would have had to reproduce are the ones worth not reproducing: when a token rotates, which cookie is cleared on a stale record, and the difference between browser state that is merely stale and a backend that is down.
Stale or unreadable browser state is cleared and the request continues with no session, because a client holding an expired cookie has not done anything wrong. A backend failure returns an error, because continuing would serve the request as anonymous and a reader could not tell that apart from a signed-out visitor.
func (*Manager) Rotate ¶
Rotate revokes the record the request carries and issues a replacement under a new token, keeping every slot value.
It is the fixation defense, so it changes the token and never the state, which is what lets a login keep what the anonymous browser accumulated. It is also the promotion: the replacement is written to the configured server backend, so a Private slot that rode a sealed cookie while the session was anonymous lands on the server and its cookie is expired in the same response.
A deployment running the cookie backend promotes nothing, because the destination is where the value already is.
func (*Manager) RotateOn ¶
RotateOn is Rotate over the context the session was attached to.
The response writer Rotate takes is not read: the cookies of a rotation go to the carrier the session was resolved with, which is fixed when the session is attached rather than when it turns. So the context is the whole input, and a transport whose request value is its context passes that.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is a process-local RawStore. It is intended for development and tests: records are concurrency-safe and expiry-aware, but disappear when the process stops and are never shared with another process.
Environment policy does not belong to the storage package. The built-in pw backend that exposes this store rejects it outside development.
func NewMemoryStore ¶
func NewMemoryStore(now func() time.Time) *MemoryStore
NewMemoryStore returns an empty process-local store.
func (*MemoryStore) Delete ¶
func (s *MemoryStore) Delete(ctx context.Context, keyHash string) error
type Options ¶
type Options struct {
// TTL is the absolute session lifetime. Zero writes a browser-session
// cookie and stamps no absolute deadline.
TTL time.Duration
// IdleTimeout optionally expires a session that stops being used. It must
// not exceed TTL.
IdleTimeout time.Duration
// RenewalInterval bounds how often an active request renews idle expiry.
// It defaults to one tenth of IdleTimeout.
RenewalInterval time.Duration
// Cookie is the policy of the token cookie.
Cookie CookieOptions
// RecordCookie is the policy of the sealed cookie that carries the record
// of an anonymous session. Its name defaults to DefaultDataCookieName and
// its remaining fields default to Cookie, so both expire under one policy.
RecordCookie CookieOptions
// Keys protects every slot that is not Shared. It is required unless every
// registered slot is Shared, which is the only placement protecting
// nothing.
Keys *Keyring
// ServerSideAnonymous places Private records in the configured backend
// before authentication as well as after it. It is used by the development
// memory backend to avoid persisting an unstable sealed-cookie format.
ServerSideAnonymous bool
// Version invalidates records written before an incompatible change.
Version int
// MaxBytes bounds a cookie name and encoded value together. It defaults to
// DefaultMaxCookieBytes.
MaxBytes int
Now func() time.Time
Random io.Reader
}
Options configures a Manager.
Every duration here is supplied by whatever owns the session lifetime, which is normally popcornweb/plugin/auth: an expiry states how long a proof of identity stays good, and the store holding the bytes has no basis to make that statement. A zero TTL is allowed and means the session is bounded by the browser alone.
type Placement ¶
type Placement int
Placement states what the client may do with a registered slot and where its bytes live. It is declared where the type is registered, because what a client may do with a value is a property of the value rather than of the deployment. The deployment is left with one choice, which server backend a server-placed slot uses.
const ( // writes cannot live on the server, so this placement is a cookie by // definition. A decoded value is request input and is validated like a // query parameter. Shared Placement = iota + 1 // ReadOnly is a signed cookie the client reads but cannot change. The // payload stays readable, so it carries no secret. ReadOnly // Private is sealed and unreadable by the client. It rides a sealed cookie // while the session is anonymous and moves to the configured server backend // at the login rotation, so a visitor who never logs in costs the server // nothing. Its anonymous phase is bounded by the browser cookie budget; a // value that can grow past it is declared ServerOnly instead. Private // ServerOnly is sealed and always server-placed, including while the // session is anonymous. The argument is revocation rather than // confidentiality: sealing already hides a value from the client, but a // cookie-placed record cannot be taken back. An anonymous write creates a // server record, which is what this placement asks for. ServerOnly // RequestScope lives in process memory for one request and is never // persisted: no cookie, no record, no backend. A middleware or handler // derives it from an authoritative source and later handlers in the same // request read it; the next request starts empty. It is for a value whose // freshness matters more than its cost to rebuild — the scope set a bearer // token resolves to against the authentication database is the standing // example — so staleness is prevented by reconstruction rather than // chased by invalidation. RequestScope )
type RawRecord ¶
type RawRecord struct {
// Payload is the encoded application payload.
Payload []byte
// The remaining fields carry the same meaning as in Record.
CreatedAt time.Time
AuthenticatedAt time.Time
LastSeenAt time.Time
ExpiresAt time.Time
IdleExpiresAt time.Time
Method string
Version int
}
RawRecord is a Record whose payload is already encoded. It is what a storage backend writes and reads, so a backend never sees the application payload type and never needs a type parameter.
func (RawRecord) Deadline ¶
Deadline is the earliest authoritative expiry of the record. A backend enforces it on read, whatever its own expiry mechanism does.
A zero field means "no such bound" rather than "the epoch", so this is the earliest of those that are set. See Record.deadline for what reading the absolute bound alone used to cost.
type RawStore ¶
type RawStore interface {
Put(ctx context.Context, keyHash string, record RawRecord) error
Get(ctx context.Context, keyHash string) (RawRecord, error)
Touch(ctx context.Context, keyHash string, lastSeenAt, idleExpiresAt time.Time) error
Delete(ctx context.Context, keyHash string) error
}
RawStore is the contract a storage backend implements. It is Store without the type parameter, which is what lets one registry hold every backend: api:session-backend-plugin resolves a backend by name, and the host adds the payload type back with Typed.
Every rule of Store applies here unchanged: honor cancellation, never accept the raw cookie token, replace one key atomically, never revive an expired record, and treat stored expiry as authoritative.
type Record ¶
type Record[T any] struct { // Data is the typed application payload. Data T // CreatedAt is when this record was written. CreatedAt time.Time // AuthenticatedAt is when the current authentication strength was // established. Rotation after login refreshes it. AuthenticatedAt time.Time // LastSeenAt is the last renewal timestamp. LastSeenAt time.Time // ExpiresAt is the authoritative absolute expiry. ExpiresAt time.Time // IdleExpiresAt is the optional inactivity expiry. It is never later than // ExpiresAt. IdleExpiresAt time.Time // Method records how the session was authenticated, such as oidc. Method string // Version invalidates records after an incompatible schema or policy // change. Version int }
Record is the stored session state. Stores persist it under a key hash and must treat every field as immutable once written.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds every piece of per-browser state an application declared.
A slot is declared once, as a Go type with a Placement, and read back by that type. The type is the key, so a misspelled name is a compile error rather than a missing value, and a package reads its own state without importing the package that owns the layout. Two packages wanting one slot share the type, which makes the sharing visible in the import graph.
Registration happens at startup, before any request decodes anything. A Registry is safe for concurrent use once a Manager has been built over it; registering after that point is refused.
type RequestBinder ¶
RequestBinder is the optional contract of a Store whose records live in the browser instead of a backend, such as CookieStore. The Manager calls BindRequest before every store call it makes on behalf of a request, so the store can reach the cookie it reads and the response it writes.
A backend store implements nothing here and is unaffected, which is what keeps one Manager, one Options, and one handler working over a cookie, an RDB, or a Redis store.
type Resolved ¶
type Resolved struct {
// contains filtered or unexported fields
}
Resolved is a session resolved against one request, ready to be published wherever that transport publishes request state.
func (Resolved) StoreOn ¶
func (r Resolved) StoreOn(store ValueStore)
StoreOn records this session on a request value that carries its own state.
It exists because the key is this package's and should stay that way: a transport publishing the session would otherwise need the key exported, and an exported key is one any code can write, which for a session means any code can present one.
type Slot ¶
type Slot[T any] struct { // contains filtered or unexported fields }
Slot is the request-scoped handle to one registered piece of state.
func Value ¶
Value returns the request-scoped handle for T. It reports false when the session middleware did not run or T is not registered.
func (*Slot[T]) Set ¶
Set writes value and makes it what later handlers in this request read.
A cookie-placed slot writes Set-Cookie immediately and therefore precedes response commitment. A record-placed slot is flushed once, before the response is committed. The first Set on any slot issues the session token, so a visitor who writes nothing receives no cookie and occupies no storage.
type SlotOption ¶
type SlotOption func(*slot) error
SlotOption states how long one registered slot lives, and what ends it.
The two axes are independent, with one constraint: a value may always die before its session, but only a value the browser holds can outlive it, because a session-placed value is destroyed with the record that holds it.
func ExpiresAfter ¶
func ExpiresAfter(d time.Duration) SlotOption
ExpiresAfter bounds a slot to d, whatever its placement.
It is how a value dies before the session that carries it does: a CSRF secret rotated on a schedule, a step-up admission good for seconds, a cached decision good for minutes. A record-placed slot carries its own deadline inside the record and is dropped on read once it passes; a cookie-placed one carries it as the cookie lifetime.
The slot still dies with the session. Use OutlivesSession for a value that should not.
func OutlivesSession ¶
func OutlivesSession(d time.Duration) SlotOption
OutlivesSession keeps a slot for d and exempts it from the destruction of the session, so a sign-out leaves it alone.
It is what a display language or a density preference wants: state that belongs to the browser rather than to whoever is signed in. Pass BrowserMax to keep it as long as a browser will.
It is refused for session.Private and session.ServerOnly. Those live in the session record, and a record cannot outlive its own destruction; the refusal is a registration error rather than a surprise at logout.
func ResetOnRotate ¶
func ResetOnRotate() SlotOption
ResetOnRotate drops the slot at a rotation instead of carrying it forward.
A rotation normally preserves every value, which is what lets a login keep what the anonymous browser accumulated. A few values must not survive it: a CSRF secret is the standing example, because policy:session-security requires it to change with the session, so a token minted before a sign-in cannot be presented after one.
It states what a rotation does to the value, where ExpiresAfter states what time does and OutlivesSession states what a destroy does.
type Store ¶
type Store[T any] interface { // Put replaces one key atomically. Put(ctx context.Context, keyHash string, record Record[T]) error // Get returns ErrNotFound for a missing key and ErrExpired for a record // past its absolute or idle expiry. Get(ctx context.Context, keyHash string) (Record[T], error) // Touch renews an existing record. It never revives a missing or expired // one. Touch(ctx context.Context, keyHash string, lastSeenAt, idleExpiresAt time.Time) error // Delete is idempotent. Delete(ctx context.Context, keyHash string) error }
Store persists session records behind one context-aware contract. Stores never accept or return the raw cookie token.
type UnavailableHandler ¶
type UnavailableHandler func(http.ResponseWriter, *http.Request, error)
UnavailableHandler responds to a request whose session backend could not be reached. The lifecycle fails closed here instead of silently downgrading the request to unauthenticated.
type ValueStore ¶
type ValueStore interface {
SetUserValue(key, value any)
}
ValueStore is a request value that carries its own state instead of being replaced by a derived copy, which is how the second transport publishes request state.