session

package
v1.0.0-rc.2 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 7 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewContext

func NewContext(ctx context.Context, s *Session) context.Context

NewContext stashes a session in ctx.

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

type Envelope struct {
	ID       string
	CSRF     string
	Created  time.Time
	LastSeen time.Time
}

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

func NewManager(store Store, cfg Config) *Manager

NewManager builds a Manager over the given store and starts the reaper.

func (*Manager) ActiveSessions

func (m *Manager) ActiveSessions() int

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

func (m *Manager) Load(r *http.Request) *Session

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.

func (*Manager) Start

func (m *Manager) Start(w http.ResponseWriter, r *http.Request) *Session

Start returns the existing session or creates a fresh one and sets the cookie.

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.

func (SSO) Verified

func (x SSO) Verified() bool

Verified reports whether an external identity has been established.

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

func Adopt(e Envelope) *Session

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

func FromContext(ctx context.Context) (*Session, bool)

FromContext retrieves the session stashed by the middleware.

func (*Session) App

func (s *Session) App() any

App returns the application payload (may be nil before login).

func (*Session) Envelope

func (s *Session) Envelope() Envelope

Envelope snapshots the session's durable identity.

A durable Store must read the timestamps through this rather than straight off the struct: Load writes LastSeen on every request, so a direct read of a session that has been published races it.

func (*Session) SSO

func (s *Session) SSO() SSO

SSO returns a copy of this session's SSO state.

func (*Session) SetSSO

func (s *Session) SetSSO(x SSO)

SetSSO replaces this session's SSO state.

func (*Session) Token

func (s *Session) Token() string

Token returns the session CSRF token. ID and CSRF are fixed at construction (Authenticate mints a NEW session rather than re-keying in place), so the locked read is uniformity with the mutable time fields, not a correctness requirement.

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.

func NewMemStore

func NewMemStore() Store

NewMemStore returns an empty in-memory store.

Jump to

Keyboard shortcuts

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