๐ค Sessions
sessions owns signed-in state. It does not know how a session is
earned: an identity plugin verifies a credential and calls SignIn, and
that call is the whole plugin contract.
Three plugins ship โ passwords, magic links, and passkeys as a second factor โ and you can write another without changing anything here.
What a session is
A row in SQLite, not a signed cookie. Sign-out and administrative revocation are therefore real: a deleted row is dead even if the cookie lives on. Only the SHA-256 hash of a token is stored; the token itself exists in the cookie and nowhere else.
writer, err := d.G.DB()
if err != nil {
return err
}
sess, err := sessions.New(sessions.Config{
DB: writer,
Origin: origin,
Logger: logger,
})
Origin is your external origin with the scheme โ
https://app.example.com. It decides the Secure and __Host- cookie
attributes and nothing else; sessions never redirects off it. TTL
defaults to 30 days and SigninPath to /signin.
Build one *Sessions per process and share it. Its migrations go into
BootSchema โ see Migrations.
CSRF
r.Use(csrf.Protect(origin))
Mount it app-wide, above the route groups, so a route you add later is protected without anyone remembering.
It refuses cross-origin POST, PUT, PATCH and DELETE, checking
Sec-Fetch-Site first, then Origin, then Referer. There are no
tokens to mint, nothing to thread through a template, and nothing to get
wrong in a form you wrote in a hurry.
Guarding routes
Three of them, and the difference matters.
Require
r.Group(func(r chi.Router) {
r.Use(sess.Require)
r.Get("/", a.listNotes)
})
A signed-out GET or HEAD redirects to SigninPath with a same-site
return_to. Anything else answers 403, since a signed-out POST has no
sensible page to be sent to.
Middleware
Softer: it resolves a session when there is one and blocks nothing. Use it for a page that renders differently when signed in but is public either way.
RequireFresh
r.Use(sess.RequireFresh(15 * time.Minute))
Require plus step-up. The session must exist and its credential must
have been verified within maxAge; past that, a GET or HEAD goes to
SigninPath?reauth=1 so your sign-in page can say "confirm it's you",
and anything else answers 403.
Re-signing in calls SignIn, which rotates the session with a fresh
timestamp and satisfies the gate. So does a passkey assertion โ
Passkeys covers using one for step-up instead of a
full re-sign-in.
Freshness is measured from AuthTime when the plugin records one, and
from the session row's creation time otherwise. A session is only ever
minted at credential verification, so that fallback is an honest lower
bound, and it is what stops a plugin that never sets AuthTime โ the
magic link, for one โ from redirect-looping forever.
sessions.Fresh(sess, maxAge, now) answers the same question directly,
if you want to step up in a handler rather than per route.
Reading the viewer
uid, ok := sessions.UserID(r) // int64
sess, ok := sessions.Current(r) // the whole Session
Session carries Subject, Method, AuthTime and At. 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".
UserID parses Subject as an int64. Past Require its ok holds
only for a plugin whose subject really is a numeric user id, and the
magic-link plugin's is an email address โ see
Magic links for what follows from assuming
otherwise.
Redirecting after sign-in
http.Redirect(w, r, sessions.SafeReturn(r, "/"), http.StatusSeeOther)
Never use a raw return_to. SafeReturn accepts only a same-site
absolute path โ exactly one leading /, no scheme, no backslash, no
control characters โ and gives you the fallback otherwise.
The control-character rule is not there for show. 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.
Housekeeping
sess.Sweep(time.Now())
Deletes expired rows. Expired sessions are already refused on read, so this is hygiene rather than enforcement โ call it from a periodic job or at boot.
Read this page as markdown โ exact, unstyled, and cheap for an agent to fetch.