๐ค sessions
amadan.net/rastrillo/rastrillo/sessions
The signed-in-state core: SQLite-backed session rows, __Host- cookies
on https origins, and the request-context surface every identity plugin
signs into.
It does not know how a session is earned. A plugin verifies a credential
and calls SignIn, and that call is the whole plugin contract.
Sessions is the guide.
New and Config
func New(cfg Config) (*Sessions, error)
Build one *Sessions per process and share it.
type Config struct {
DB *sql.DB
Origin string
TTL time.Duration
SigninPath string
Logger *slog.Logger
}
DB is your database, and Schema's migrations must already have been
applied. Origin is your external origin with scheme; it decides the
Secure and __Host- cookie attributes and nothing else, since
sessions never redirects off it. TTL defaults to 30 days,
SigninPath to /signin.
Schema
var Schema = migrate.MustFromFS(migrationFS, "sessions")
The package's migration set. Merge it into your boot set โ migrate โ before anything here runs. Tokens never touch the table: only their SHA-256 hash is stored.
Session
type Session struct {
Subject string
Method string
AuthTime time.Time
At time.Time
}
Subject is your own identifier for who is signed in, kept as a string
so plugins never have to agree on a numeric type. Method names how the
credential was verified, in the plugin's vocabulary โ "password",
"magiclink", "magiclink+passkey". AuthTime is when the credential
was verified, zero if the plugin does not track it. At is when this
row was created.
Signing in and out
func (s *Sessions) SignIn(w http.ResponseWriter, r *http.Request, sess Session) error
func (s *Sessions) SignOut(w http.ResponseWriter, r *http.Request)
SignIn mints a row and sets the cookie, rotating the token, which is
also what makes it satisfy RequireFresh. SignOut deletes the row, so
revocation is real: the cookie is dead even if it survives.
Sessions.CookieName reports the cookie's name, which varies with the
origin's scheme because of the __Host- prefix.
func (s *Sessions) Mint(sess Session) (token string, err error)
func (s *Sessions) Adopt(w http.ResponseWriter, r *http.Request, token string) (Session, bool)
Mint and Adopt split SignIn for a credential that lives somewhere
other than this browser โ the vault's copy of
an instance session. Mint creates a row and returns its token without
touching any cookie: same TTL, same table, same Sweep. Adopt is the
return leg: verify the presented token and, if its row is alive, set
the cookie to it โ minting nothing, revoking nothing. A dead token
refuses without touching any cookie, so a failed restore falls through
to ordinary sign-in.
Middleware
func (s *Sessions) Require(next http.Handler) http.Handler
func (s *Sessions) Middleware(next http.Handler) http.Handler
func (s *Sessions) RequireFresh(maxAge time.Duration) func(http.Handler) http.Handler
Require admits only a signed-in caller: a signed-out GET/HEAD
redirects to SigninPath with a same-site return_to, anything else
403s. Middleware resolves a session when there is one and blocks
nothing. RequireFresh is Require plus step-up โ past maxAge a
GET/HEAD goes to SigninPath?reauth=1, anything else 403s.
Sessions.From resolves the session from a request directly, for code
outside the middleware chain.
Fresh
func Fresh(sess Session, maxAge time.Duration, now time.Time) bool
The predicate RequireFresh applies, for a handler stepping up in code
rather than per route.
Freshness is measured from AuthTime when set and from At otherwise.
A session is only ever minted at credential verification, so At is an
honest lower bound, and the fallback is what stops a plugin that never
sets AuthTime โ the magic link, for one โ from redirect-looping
forever.
Reading the viewer
func Current(r *http.Request) (Session, bool)
func UserID(r *http.Request) (int64, bool)
func WithSession(r *http.Request, sess Session) *http.Request
Current gives you the whole session. UserID parses Subject as an
int64, and its ok holds only for a plugin whose subject is a numeric
user id โ not for the magic-link plugin, whose subject is an email
address. Magic links has what follows from
assuming otherwise.
WithSession stashes a session on a request's context. Plugins use it
after verifying, and it is also what makes a session visible to code
called outside the middleware, which is how a generated store sees the
viewer.
SafeReturn
func SafeReturn(r *http.Request, fallback string) string
Returns return_to when it is a same-site absolute path โ exactly one
leading /, no scheme, no backslash, no control characters โ and the
fallback otherwise. Anything laxer is an open redirect on a sign-in
endpoint.
Control characters are in that list because browsers strip tab, CR and
LF from a URL before parsing it, so /\t/evil.example passes a bare
// check and still navigates scheme-relative off-site.
Tokens
func NewToken() (token, hash string, err error)
func HashToken(token string) string
Mint a token and its stored hash, or hash a presented one. Exported because a plugin storing its own single-use credentials โ a magic link, a recovery code โ should hash them the same way instead of inventing a second scheme.
Sweep
func (s *Sessions) Sweep(now time.Time) error
Deletes expired rows. Expired sessions are already refused on read, so this is hygiene rather than enforcement.
Read this page as markdown โ exact, unstyled, and cheap for an agent to fetch.