CARLOS rastrillo docs

๐Ÿค– passkey

amadan.net/rastrillo/rastrillo/passkey

A WebAuthn second factor on two seams: step-up, where an assertion refreshes a stale session instead of a full re-sign-in, and sign-in, where a verified first factor must be completed by an assertion before a session exists.

Passkeys and second factors is the guide.

The trust boundary

A passkey never signs anybody in from nothing. Step-up endpoints demand a valid session โ€” stale is fine, absent is not. The sign-in pair demands a live pending half-session, which only a verified first factor mints. So a stolen credential id on its own opens no door.

New, Config and Schema

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

Merge passkey.Schema into your boot set, and serve webauthn.JS() as a static asset for the browser half.

Credentials are public material: a public key verifies signatures and nothing else. Challenges are single-use rows consumed by DELETE ... RETURNING.

The endpoints

Mount them behind csrf.Protect like every other mutating route:

Route Handler
POST /passkey/register/begin Handlers.RegisterBegin
POST /passkey/register/finish Handlers.RegisterFinish
POST /passkey/stepup/begin Handlers.StepUpBegin
POST /passkey/stepup/finish Handlers.StepUpFinish
POST /passkey/signin/begin Handlers.SignInBegin
POST /passkey/signin/finish Handlers.SignInFinish
POST /passkey/signin/recovery Handlers.SignInRecovery

The begin handlers answer {"challenge": ...}; the finish handlers take webauthn.mjs's register() or authenticate() result.

A successful step-up calls sessions.SignIn, rotating the session with method "passkey" and a fresh AuthTime โ€” exactly what sessions.RequireFresh checks.

The timeouts

A challenge lives two minutes: long enough for an authenticator prompt, short enough that an abandoned one is not a standing invitation.

A pending half-session lives five minutes. Miss that window and you sign in again from the top.

Gate

func (h *Handlers) Gate(w http.ResponseWriter, r *http.Request, sess sessions.Session) (bool, error)

The SecondFactor hook both identity plugins expose. Called where a plugin would mint the session, it trades the immediate sign-in for a pending half-session โ€” a short-lived cookie plus a hashed row naming who must still assert โ€” and redirects to Config.ConfirmPath.

A verified assertion consumes the pending row, clears the cookie, and mints the real session with the original first-factor method plus "+passkey" โ€” "magiclink+passkey", say.

An account with no passkey passes the Gate untouched, returning (false, nil), so you can turn it on for everyone and let enrollment decide who it applies to.

Handlers.Enrolled(subject) reports whether an account has a credential, for a settings page or a conditional prompt.

Recovery codes

func (h *Handlers) RegenerateRecoveryCodes(subject string) ([]string, error)
func (h *Handlers) RecoveryCodesRemaining(subject string) (int, error)

RegenerateRecoveryCodes mints ten single-use codes and replaces any existing set. Show them once, from a page you mount behind sessions.RequireFresh.

SignInRecovery redeems one against the pending half-session where an assertion would have gone. It is a plain form POST reading the field code, with no JavaScript, deliberately: recovery is exactly the moment WebAuthn is not working.

A wrong code does not consume the half-session; it redirects to ConfirmPath?recovery=failed so another can be tried. A correct one burns the code, consumes the pending session, and mints a session whose method is the first factor plus "+recovery" โ€” a marker you can use to nudge re-enrollment.

This is sign-in only. There is no recovery step-up, and RequireFresh stays satisfiable only by an assertion or a full re-sign-in.

There is no attempt counter either. Redeeming needs a live half-session, held for at most five minutes, and ten codes at 2โปโตโฐ apiece put brute force far below any practical odds inside that window.

Sweep

func Sweep(db *sql.DB, now time.Time) error

Deletes expired challenges and pending half-sessions. Both are refused on read once expired, so this is hygiene rather than enforcement.

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