---
phase: 2
slug: authentication
status: draft
nyquist_compliant: true
wave_0_complete: true
created: 2026-05-14
---

# Phase 2 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Framework** | Go `testing` + `go test` (stdlib) |
| **Config file** | `backend/go.mod` (no separate config) |
| **Quick run command** | `cd backend && go test ./internal/auth/...` |
| **Full suite command** | `cd backend && go test ./...` |
| **Estimated runtime** | ~15–30 seconds (argon2 tests use reduced params) |

DB-touching tests require `TEST_DATABASE_URL` pointing at the compose-managed Postgres (see Phase 1 pattern in `backend/internal/db/pool_test.go`). Each test runs in a transaction that rolls back, OR uses a per-test schema name to avoid cross-contamination.

---

## Sampling Rate

- **After every task commit:** Run `go test ./internal/auth/...`
- **After every plan wave:** Run `go test ./...`
- **Before `/gsd-verify-work`:** Full suite must be green
- **Max feedback latency:** 30 seconds

---

## Per-Task Verification Map

Filled by planner as PLAN.md tasks are emitted. Each task with `type: execute` must have an `<automated>` verify command OR an explicit Wave 0 dependency. Tasks creating templ/HTML output may need a small Go test that renders the component and asserts the resulting HTML contains expected markers.

| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| 01-T1 | 01 | 1 | AUTH-01,AUTH-02 | T-2-08b | Schema, sqlc gen, test harness | integration | `cd backend && go build ./... && TEST_DATABASE_URL=$DATABASE_URL go test ./internal/db/... -count=1` | ⬜ | ⬜ pending |
| 02-T1 | 02 | 2 | AUTH-01 | T-2-01 | argon2id PHC Hash/Verify, malformed-PHC + version-mismatch rejection (RED→GREEN→REFACTOR) | unit (tdd) | `cd backend && go test ./internal/auth -run "TestPassword_" -count=1` | ⬜ | ⬜ pending |
| 03-T1 | 03 | 2 | AUTH-02,AUTH-03,AUTH-05 | T-2-04,T-2-05,T-2-06 | Session create/lookup/rotate/extend; cookie attrs; ResolveSession/RequireAuth/RedirectIfAuthed | integration | `cd backend && TEST_DATABASE_URL=$DATABASE_URL go test ./internal/auth -run "TestSession_\|TestCookie_\|TestResolveSession\|TestRequireAuth\|TestRedirectIfAuthed" -count=1` | ⬜ | ⬜ pending |
| 04-T1 | 04 | 3 | AUTH-01,AUTH-03,AUTH-05 | T-2-01,T-2-03 | Signup E2E: validate → argon2 → InsertUser → Store.Create → 303; RedirectIfAuthed wraps GET /signup | integration | `cd backend && templ generate && TEST_DATABASE_URL=$DATABASE_URL go test ./internal/web -run "TestSignup_" -count=1` | ⬜ | ⬜ pending |
| 05-T1 | 05 | 4 | AUTH-02,AUTH-03,AUTH-05,AUTH-07 | T-2-02,T-2-03 | Login E2E: rate-check → Verify → Rotate → cookie → 303; generic error string; rate limiter burst+per-key+janitor with injectable clock | integration | `cd backend && templ generate && TEST_DATABASE_URL=$DATABASE_URL go test ./internal/web ./internal/auth -run "TestLogin_\|TestRateLimit_" -count=1` | ⬜ | ⬜ pending |
| 06-T1 | 06 | 5 | AUTH-04,AUTH-05 | T-2-07,T-2-10 | Logout deletes row + clears cookie; / now protected; Layout shows logout button only when authed | integration | `cd backend && templ generate && TEST_DATABASE_URL=$DATABASE_URL go test ./internal/web ./templates -run "TestLogout_\|TestProtected_\|TestLayout_" -count=1` | ⬜ | ⬜ pending |
| 07-T1 | 07 | 6 | AUTH-06 | T-2-08,T-2-08a,T-2-08b,T-2-08c,T-2-08d | gorilla/csrf mounted after ResolveSession; @ui.CSRFField in every form; SESSION_SECRET from env; 403 on missing token, 200/303 on valid | integration | `cd backend && templ generate && TEST_DATABASE_URL=$DATABASE_URL go test ./internal/web ./internal/auth ./cmd/web -run "TestCSRF_\|TestForms_ContainCSRFField\|TestRouter_CSRFMountedAfterResolveSession\|TestLoadCSRFKey" -count=1` | ⬜ | ⬜ pending |

*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

---

## Wave 0 Requirements

- [ ] `backend/internal/auth/password_test.go` — argon2 hash/verify round-trip with reduced test params
- [ ] `backend/internal/auth/session_test.go` — token generate + SHA-256 lookup, expiry semantics
- [ ] `backend/internal/auth/ratelimit_test.go` — per-key bucket allow/deny with injectable clock
- [ ] `backend/internal/auth/middleware_test.go` — ResolveSession context plumbing, RequireAuth 303 / HX-Redirect
- [ ] Reuse `backend/internal/db/pool_test.go` Postgres harness for users/sessions integration tests

*Existing Go test infrastructure (Phase 1) covers framework setup — no new tooling install needed.*

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Cookie is `HttpOnly` and `Secure` in non-dev environments | AUTH-03 | Browser DevTools required to inspect cookie attributes from a live response | Run `ENV=staging` server, hit `/login`, submit valid creds, open DevTools → Application → Cookies; confirm `HttpOnly=true`, `Secure=true`, `SameSite=Lax` |
| `/login` page visual layout matches Phase 1 design system | AUTH-01 / D-69 | Visual assertion | Open `http://localhost:8080/login`, confirm Button/Card/Badge styling from `internal/web/ui` is applied; confirm `/signup` mirrors |
| HTMX inline form-error swap on bad credentials | AUTH-02 | Requires browser to evaluate `HX-Request` round-trip | Submit `/login` with bad password via HTMX form; only the form fragment swaps with "Invalid email or password" — no full page reload |
| Rate-limit error fragment shows after 6th failed login | AUTH-06 | Requires browser to observe 429 + swap | Submit 6 wrong-password attempts in <60s from same IP; 6th returns 429 with "Too many attempts" inline fragment |

---

## Validation Sign-Off

- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 30s
- [ ] `nyquist_compliant: true` set in frontmatter

**Approval:** approved 2026-05-14
