CARLOS rastrillo docs

๐Ÿค– 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.