Documentation
¶
Overview ¶
Package session provides TableX's server-side session management: opaque 256-bit IDs (crypto/rand), a pluggable store, idle/absolute timeouts, CSRF token issuance, and login-time re-keying to prevent fixation — Authenticate mints a NEW session (fresh ID + CSRF, payload attached before the object is shared) and atomically replaces the pre-auth one, so the pre-auth ID can never become authenticated. Database credentials and live pools live only inside the session payload (server-side memory) — never in the cookie. See docs/security.md.
Index ¶
- func NewContext(ctx context.Context, s *Session) context.Context
- type Config
- type Detacher
- type Envelope
- type Manager
- func (m *Manager) ActiveSessions() int
- func (m *Manager) Authenticate(w http.ResponseWriter, preAuth *Session, app any, closeFn func()) (*Session, bool)
- func (m *Manager) Destroy(w http.ResponseWriter, s *Session)
- func (m *Manager) Load(r *http.Request) *Session
- func (m *Manager) Shutdown()
- func (m *Manager) Start(w http.ResponseWriter, r *http.Request) *Session
- type SSO
- type Session
- type Store
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Config ¶
type Config struct {
CookieName string
IdleTimeout time.Duration
AbsoluteTimeout time.Duration
Secure bool // set Secure + __Host- prefix (TLS deployments)
// Now, when non-nil, replaces time.Now as the manager's clock — the
// test-only injection point that lets expiry tests advance time
// deterministically instead of racing the wall clock. It MUST be set at
// construction (NewManager launches the reaper goroutine, which reads it);
// assigning it afterwards would be a data race. Implementations must be
// safe for concurrent use.
Now func() time.Time
}
Config tunes the manager.
type Detacher ¶
type Detacher interface {
Detach() []*Session
}
Detacher is an optional Store capability for backends whose membership is shared with other processes — a database-backed store, where a row outlives the process that created it and is visible to every replica.
Such a store has to distinguish two things the in-memory store conflates:
"release the resources I am holding" → Detach, on shutdown "this session is over for everyone" → Delete / Reap
Manager.Shutdown means the first. Routed through Reap with an always-true predicate — correct for a store that IS the only copy — it would delete every session belonging to every replica, so one node restarting would log out the whole cluster. A store that implements Detach gets asked properly; one that does not keeps today's behaviour exactly.
Detach must return every session the process holds, having removed them from its local view, and must leave the shared record untouched.
type Envelope ¶
Envelope is a session's durable identity: everything a Store may keep outside this process, and nothing it may not. There is no field for a credential, and none for the payload — see Adopt.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager creates, loads, rotates and destroys sessions, and runs a background reaper that closes expired sessions' pools.
func NewManager ¶
NewManager builds a Manager over the given store and starts the reaper.
func (*Manager) ActiveSessions ¶
ActiveSessions reports how many sessions the store currently holds. After Shutdown (or a full reap) this is 0, since every reaped session is closed — making it a deterministic proxy for "all pools closed" in tests.
func (*Manager) Authenticate ¶
func (m *Manager) Authenticate(w http.ResponseWriter, preAuth *Session, app any, closeFn func()) (*Session, bool)
Authenticate atomically turns a pre-auth session into an authenticated one on successful login. It constructs a NEW session — fresh ID and CSRF token (anti-fixation: a pre-login ID or token must never survive into the authenticated session), with the payload attached before the object is ever shared — then atomically replaces preAuth in the store (Store.Replace membership-checks the exact pointer, so a pre-auth session that was evicted, reaped or destroyed since it was loaded fails cleanly) and sets the cookie.
This is the ONLY attach path: the pre-auth object and ID never carry a payload, so there is no window in which a fixation-prone ID authenticates requests, no attach→re-key gap a terminal removal could interleave with, and capacity eviction's "pre-auth sessions hold no pools" invariant holds by construction. On ok=false nothing was attached or saved — the caller still owns the payload and must close it (honest backpressure at the session cap instead of an evictable fresh-session retry).
func (*Manager) Destroy ¶
func (m *Manager) Destroy(w http.ResponseWriter, s *Session)
Destroy removes the session, closes its pools and clears the cookie.
The cookie is cleared FIRST and unconditionally. A session that expired or was reaped between the request arriving and logout running leaves s == nil, and returning early there left the browser holding a dead session cookie it would re-send on every subsequent request — logout appeared to do nothing.
func (*Manager) Load ¶
Load returns the live session referenced by the request cookie, or nil if none/expired. It refreshes LastSeen on a hit.
func (*Manager) Shutdown ¶
func (m *Manager) Shutdown()
Shutdown stops the reaper and closes all live sessions (graceful shutdown). It is idempotent: the server's shutdown path may call it more than once (e.g. after a drain timeout forces a second pass), and closing m.stopCh twice would otherwise panic.
type SSO ¶
type SSO struct {
// Pending handshake, meaningful only between the redirect out and the
// callback. Spent as soon as the callback runs.
State string
Nonce string
Verifier string
// Verified identity. Subject is non-empty once the provider has vouched for
// this browser; it is the answer to "which PERSON", and deliberately not to
// "which database account" — the connection still uses the user's own
// credentials, so the audit trail keeps reporting the account.
Subject string
Email string
Name string
}
SSO is the state of an external identity handshake, and then its result.
It lives on the session because the session cookie is the only thing that spans the redirect out to the provider and back — there is nowhere else to keep a per-browser nonce. The handshake fields are opaque to this package; what it guarantees is that they are stored per session and that Authenticate keeps only the VERIFIED half when it mints the post-login session.
type Session ¶
type Session struct {
ID string
CSRF string
Created time.Time
LastSeen time.Time
// contains filtered or unexported fields
}
Session is one authenticated (or pending) session. App holds the application-specific payload (the user's connection manager); closeFn is run exactly once when the session is destroyed or reaped, to release pools.
func Adopt ¶
Adopt rebuilds a Session from an envelope a durable Store has kept outside this process. It exists for those Store implementations and has no other caller.
The result deliberately carries NO payload. A payload holds live database pools and the password the user typed, neither of which can be persisted (internal/storage explains why), so a session adopted by a process that did not log it in is a valid session which is not logged in — precisely how the rest of the application already reads a nil App(). What it does carry is the identity: the same id and the same CSRF token, which is what lets a login posted to one replica be accepted by another.
func FromContext ¶
FromContext retrieves the session stashed by the middleware.
type Store ¶
type Store interface {
Get(id string) (*Session, bool)
Save(s *Session)
Delete(id string)
// Replace atomically swaps old for new under one lock: it verifies old is
// still a store member (EXACT pointer — a same-id re-insert does not
// count), removes it, and inserts new into the authenticated bucket.
// Reports false — inserting nothing — when old is absent (evicted, reaped
// or destroyed since it was loaded). Manager.Authenticate composes it with
// session construction so a login can never resurrect a removed session
// or leave a payload-bearing session in the pre-auth bucket.
Replace(old, new *Session) bool
// Reap removes sessions for which expired(s) is true, returning them so the
// caller can close their resources. Implementations must remove atomically.
Reap(expired func(*Session) bool) []*Session
// Len reports the number of sessions currently held (observability).
Len() int
}
Store persists sessions server-side. The default implementation is an in-memory map; the interface lets a persistent/clustered backend be added later without touching the manager or handlers.