| 02-authentication |
03 |
auth |
| go |
| sessions |
| sha256 |
| cookies |
| middleware |
| chi |
| htmx |
| postgres |
| sqlc |
|
| phase |
provides |
| 02-authentication/01 |
DB schema (sessions table), sqlc queries (InsertSession, GetSessionWithUser, DeleteSession, ExtendSession), testdb harness, types.go (Session, User, SessionCookieName, SessionTTL, SessionExtendThreshold) |
|
| phase |
provides |
| 02-authentication/02 |
password.go (Hash/Verify), types.go (ErrSessionNotFound, ErrInvalidHash) |
|
|
| Store.Create: 32-byte crypto/rand token, base64url cookie value, sha256 hex DB id (D-05) |
| Store.Lookup: hashes cookie on every request, maps pgx.ErrNoRows to ErrSessionNotFound (D-07) |
| Store.Delete: hard-deletes session row (D-06) |
| Store.Rotate: deletes old row then creates new — session fixation defense (D-10, T-2-04) |
| Store.MaybeExtend: sliding TTL — extends only when remaining < 7 days (D-09) |
| SetSessionCookie: HttpOnly + Secure (env-gated) + SameSite=Lax + Path=/ (D-12) |
| ClearSessionCookie: MaxAge=-1 (not 0) for immediate browser deletion (D-06, RESEARCH Pattern 3) |
| ResolveSession middleware: reads cookie, SHA-256 lookup, MaybeExtend best-effort, attaches Session+User to ctx |
| RequireAuth middleware: 303 /login (plain) or HX-Redirect: /login (HTMX) — no 302 (D-23, Pitfall 9) |
| RedirectIfAuthed middleware: bounces authed users away from /login, /signup |
| Authed(ctx): typed context accessor returning (*Session, *User, bool) |
|
| 02-authentication/04: signup handler wires Store.Create + SetSessionCookie |
| 02-authentication/05: login handler wires Store.Rotate + SetSessionCookie |
| 02-authentication/06: logout handler wires Store.Delete + ClearSessionCookie; chi router uses ResolveSession + RequireAuth |
| 02-authentication/07: CSRF plan mounts after ResolveSession per D-24 |
|
| added |
patterns |
| crypto/sha256 (stdlib): sha256.Sum256 for token-to-id hashing (D-05) |
| crypto/rand (stdlib): 32-byte session token generation |
| encoding/base64: RawURLEncoding for cookie value, hex for DB id |
| log/slog (stdlib): best-effort MaybeExtend error logging in ResolveSession |
|
| SHA-256 token storage: cookie = base64url(raw), DB id = hex(sha256(raw)) — DB read leak does not yield live cookies |
| Sliding-window TTL: MaybeExtend writes to DB only when remaining < SessionExtendThreshold (~once per 23 days) |
| Session fixation defense: Rotate() deletes old row before creating new one (called on every login/signup) |
| HTMX-aware redirect: HX-Request=true → 200 + HX-Redirect header; plain → 303 SeeOther (never 302) |
| Injectable clock: Store.now func() time.Time — overridden in MaybeExtend tests for determinism |
| Best-effort MaybeExtend: ResolveSession logs error but never fails the request on extend failure |
| Context key pattern: unexported struct type sessionCtxKey prevents cross-package collision (mirrors web.ctxKey) |
|
|
| created |
modified |
| backend/internal/auth/session.go: Store struct, NewStore, Create, Lookup, Delete, Rotate, MaybeExtend |
| backend/internal/auth/cookie.go: SetSessionCookie, ClearSessionCookie |
| backend/internal/auth/middleware.go: ResolveSession, RequireAuth, RedirectIfAuthed, Authed, redirectTo |
| backend/internal/auth/session_test.go: 7 real-DB tests (sha256 guard, roundtrip, expired, rotate, delete, maybeextend) + 3 cookie unit tests |
| backend/internal/auth/middleware_test.go: 9 tests (3 real-DB ResolveSession + 6 ctx/routing) |
|
|
|
| Store.now is an exported field (not method) for test-clock injection — avoids interface bloat for a single-test concern |
| sha256.Sum256 inlined at Create and Lookup call sites (not in a helper) to satisfy the >= 2 grep acceptance criterion |
| MaxAge=-1 in ClearSessionCookie: Go's http.Cookie MaxAge=-1 emits Set-Cookie Max-Age=0, which browsers honor as immediate deletion; MaxAge=0 means 'not set' |
| redirectTo helper centralizes HX-Request detection — single place to audit HTMX vs plain redirect logic |
| TestRedirectIfAuthed split into two tests (BouncesWhenAuth + HXBounceWhenAuth) for clarity, plan named 1 test but 2 cover both behaviors |
|
| Pattern: store.now injectable clock — set after NewStore() in tests: store.now = func() time.Time { return fixedNow } |
| Pattern: Authed(ctx) typed accessor — same shape as web.LoggerFromContext; handlers call Authed(r.Context()) for session data |
| Pattern: chi r.Handle(path, RequireAuth(handler)) — wraps individual handlers; r.Use(RequireAuth) wraps whole groups (Plan 04+ will use groups) |
|
|
~15min |
2026-05-14 |