Skip to content

Auth, sessions, CSRF

Three decisions worth knowing. The conventions already say CSRF is published once; this page is the how.

The cookie holds the raw token. The database stores only sha256(token). A leaked dump does not hand anybody a working session. SECRET_KEY signs cookies and CSRF; without it the app refuses to start in production.

Default lifetime: SESSION_DAYS (14). Expired sessions count as absent. requireUser redirects to /signin?next=….

Hashed with argon2id via Bun.password (Bun defaults). Sign-in verifies a dummy hash when the user does not exist, so response timing does not leak which emails are registered. Bad user and bad password both return null.

The token is HMAC-SHA256(SECRET_KEY, session_token_digest) — no table, tied to the session, rotates when the session does.

It is published once as the <body>’s hx-headers → X-CSRF-Token. Every htmx request inherits it. Never one per form.

Sign-in is the exception: a seed cookie pleyn_csrf plus a hidden _csrf field on the only non-htmx form. Middleware also checks origin / Sec-Fetch-Site; GET and HEAD are unchecked.

A formatter can break the hx-headers attribute — see Pitfalls. Tests that post without CSRF only exercise middleware; they do not test the handler.