CARLOS rastrillo docs

๐Ÿค– auth

amadan.net/rastrillo/rastrillo/auth

Passwordless sign-in by emailed link. It wraps github.com/keymaildev/signin, filling the holes that package deliberately leaves โ€” link storage, mailer, cookies, sessions, CSRF, admission โ€” once here instead of once per app.

Magic links is the guide, and it carries the one trap that costs you a working app.

Addresses with a claimed keymail inbox upgrade to keymail's OAuth ceremony automatically. That is an aside rather than a feature you wire: same handlers, same session, same Subject either way. The guide's keymail section has the details.

New and Config

func New(cfg Config) (*Auth, error)

Build one at boot.

type Config struct {
	DB           *sql.DB
	Origin       string
	InstanceKey  string
	Mailer       mail.Sender
	Authorize    func(address string) bool
	SecondFactor func(w http.ResponseWriter, r *http.Request, sess sessions.Session) (done bool, err error)
	SigninPath   string
	SignedInPath string
}

InstanceKey must not be empty, and New returns ErrEmptyInstanceKey when it is. It seals the pending blob with an HMAC, and an empty input hashes to one fixed, publicly computable value โ€” identical across every deployment that made the same mistake โ€” which would let an attacker forge a pending blob naming their own keymail server.

Origin is the base of emailed links and what decides the cookie attributes. It doubles as the OAuth client_id keymail validates redirects against.

Mailer is a mail.Sender. Leave it nil and you get mail.Logged with a warning on every send: an emailed link is a live credential, so the fallback is development-only and says so.

Authorize is the admission gate: given a verified address, may it have a session? Nil admits every verified address. Membership tables, roles and admin bootstrap are your policy layered on this hook.

SecondFactor is the same seam password has. DefaultSessionTTL is the TTL used when the config does not override it.

Schema

var Schema = migrate.MustFromFS(migrationFS, "auth")

Merge it after sessions.Schema โ€” auth's backfill migration reads the sessions table, and migrate.Merge's argument order is apply order.

The handlers

POST /signin         -> Auth.Begin
GET  /auth/verify    -> Auth.Verify     (the emailed link's landing)
GET  /auth/callback  -> Auth.Callback   (the keymail OAuth return)
POST /signout        -> Auth.Signout

The sign-in page stays yours. These handlers report outcomes by redirecting to SigninPath with a query your page renders: ?sent=1 and ?err=rate|address|expired, plus ?err=keymail and ?force=1 on the keymail path.

Guarding and reading

func (a *Auth) RequireSession(next http.Handler) http.Handler
func (a *Auth) RequireFreshSession(maxAge time.Duration) func(http.Handler) http.Handler
func From(r *http.Request) (Identity, bool)
func (a *Auth) SessionFrom(r *http.Request) (Identity, bool)

RequireSession stashes both the Identity and the underlying sessions.Session, so From and sessions.Current both work downstream.

Do not use sessions.UserID under this plugin. The subject is a verified email address โ€” on both the emailed-link and keymail paths โ€” so it returns (0, false), and the ordinary scoping seam drops that ok and scopes every query in your app to user_id = 0. Read the viewer with From and map the address to your user row's id first.

Identity is an alias for signin.Identity, the same value the upstream ceremony produces, so it cannot drift from it.

Odds and ends

Auth.SessionCookie reports the session cookie's name. Auth.Sweep deletes expired links and sessions. NewToken and HashToken re-export the sessions helpers, so a caller already holding an *Auth need not import both.

A link is consumed in one DELETE ... RETURNING. A split SELECT-then-DELETE would let two concurrent callers both observe the row before either deleted it โ€” even at one writer connection โ€” defeating single use.

An unknown hash, a wrong purpose and an expired row all come back as the same "not ok"; telling them apart would be an oracle. The row is deleted even when expired, because a presented token is spent either way.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.