---
phase: 02-authentication
plan: 07
type: execute
wave: 6
depends_on: [01, 03, 04, 05, 06]
files_modified:
  - backend/internal/auth/csrf.go
  - backend/internal/web/ui/csrf_field.templ
  - backend/internal/web/router.go
  - backend/internal/web/handlers_auth.go
  - backend/internal/web/handlers_auth_test.go
  - backend/internal/web/handlers.go
  - backend/templates/auth_login.templ
  - backend/templates/auth_signup.templ
  - backend/templates/auth_form_errors.templ
  - backend/templates/layout.templ
  - backend/templates/index.templ
  - backend/cmd/web/main.go
  - backend/.env.example
  - backend/go.mod
  - backend/go.sum
autonomous: true
requirements: [AUTH-06]
tags: [go, csrf, gorilla, htmx, templ, security-hardening]
must_haves:
  truths:
    - "gorilla/csrf v1.7.3 middleware is mounted on the chi stack AFTER ResolveSession and BEFORE any route group (D-14, D-24)"
    - "csrf.Protect is configured with Secure(env != \"dev\"), SameSite(csrf.SameSiteLaxMode), Path(\"/\"), FieldName(\"_csrf\"), RequestHeader(\"X-CSRF-Token\") (D-14)"
    - "The csrf authentication key is a 32-byte value loaded from env SESSION_SECRET; main.go fails fast if it is missing or wrong length (D-15 \"key MUST come from env\")"
    - "A reusable templ component ui.CSRFField(token) renders <input type=\"hidden\" name=\"_csrf\" value=\"{token}\"/> (D-15)"
    - "Every templ form (signup, login, login fragment, signup fragment, layout logout) embeds @ui.CSRFField(token) as the first child of <form>"
    - "GET handlers that render forms (signup page, login page, fragments, layout) thread csrf.Token(r) into the templ Layout/page calls"
    - "POST /signup, POST /login, POST /logout without a valid _csrf token return 403 Forbidden (gorilla/csrf default response)"
    - "POST /signup, POST /login, POST /logout WITH a valid _csrf token continue to the normal flow (200/303 etc.)"
    - "Handlers read form fields via r.PostFormValue ONLY (never re-reading r.Body) so they coexist with gorilla/csrf's body consumption (Pitfall 1)"
  artifacts:
    - path: backend/internal/auth/csrf.go
      provides: "Mount(env string, secret []byte) func(http.Handler) http.Handler helper that constructs csrf.Protect with the locked options"
      contains: "csrf.Protect"
    - path: backend/internal/web/ui/csrf_field.templ
      provides: "ui.CSRFField(token string) templ component"
      contains: "templ CSRFField"
    - path: backend/internal/web/router.go
      provides: "r.Use(auth.Mount(env, csrfKey)) inserted after ResolveSession and before route groups"
    - path: backend/cmd/web/main.go
      provides: "Loads SESSION_SECRET from env (hex-decode 32 bytes); fails fast on missing/short key; passes []byte secret + env into NewRouter"
    - path: backend/.env.example
      provides: "Documents SESSION_SECRET with `openssl rand -hex 32` instruction"
  key_links:
    - from: backend/internal/web/router.go
      to: backend/internal/auth/csrf.go
      via: "r.Use(auth.Mount(env, csrfKey)) immediately after ResolveSession"
      pattern: "auth\\.Mount|csrf\\.Protect"
    - from: backend/templates/layout.templ
      to: backend/internal/web/ui/csrf_field.templ
      via: "@ui.CSRFField(csrfToken) inside the logout form when user != nil"
      pattern: "ui\\.CSRFField|CSRFField\\("
    - from: backend/internal/web/handlers_auth.go
      to: backend/internal/web/ui/csrf_field.templ
      via: "csrf.Token(r) -> templ page/fragment arg -> @ui.CSRFField(token)"
      pattern: "csrf\\.Token\\(r\\)"
---

<objective>
Mount gorilla/csrf on the chi stack and wire every templ form to embed the hidden _csrf field. Close AUTH-06.

Purpose: AUTH-06 closure. Before this plan, POST /signup, POST /login, POST /logout work without a CSRF check — meaning a malicious cross-origin or stale-tab POST could trigger state changes. After this plan, every state-changing request requires a valid token, and every form ships one by construction. SameSite=Lax provides an interim defense before this lands; gorilla/csrf is the real fix.
Output: csrf-protected POST routes, a reusable @ui.CSRFField templ component, env-driven csrf key, and integration tests proving 403-on-missing / 200-on-valid for all three POST routes.
</objective>

<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/02-authentication/02-CONTEXT.md
@.planning/phases/02-authentication/02-RESEARCH.md
@.planning/phases/02-authentication/02-04-PLAN.md
@.planning/phases/02-authentication/02-05-PLAN.md
@.planning/phases/02-authentication/02-06-PLAN.md
@backend/internal/web/router.go
@backend/internal/web/handlers_auth.go
@backend/internal/web/handlers.go
@backend/templates/layout.templ
@backend/templates/auth_login.templ
@backend/templates/auth_signup.templ
@backend/templates/auth_form_errors.templ
@backend/templates/index.templ
@backend/cmd/web/main.go
@backend/.env.example

<interfaces>
New / changed surfaces this plan introduces:

- `backend/internal/auth/csrf.go`:
  - `func Mount(env string, key []byte) func(http.Handler) http.Handler` — wraps `csrf.Protect(key, csrf.Secure(env != "dev"), csrf.SameSite(csrf.SameSiteLaxMode), csrf.Path("/"), csrf.FieldName("_csrf"), csrf.RequestHeader("X-CSRF-Token"))`. Returned middleware is what gets passed to `r.Use(...)`.

- `backend/internal/web/ui/csrf_field.templ`:
  - `templ CSRFField(token string) { <input type="hidden" name="_csrf" value={ token }/> }`
  - Lives alongside the existing Button/Card/Badge components established in Phase 1.

- Every templ form-rendering helper acquires an ADDITIONAL `csrfToken string` arg appended to the existing signature established in Plans 04/05/06. The pre-existing `form` (typed SignupForm/LoginForm — for value preservation, Email roundtrip per Plan 04 D-25) and `errs` (typed SignupErrors/LoginErrors structs) arguments STAY. Final signatures:
  - `templ LoginPage(form LoginForm, errs LoginErrors, csrfToken string)`
  - `templ LoginFormFragment(form LoginForm, errs LoginErrors, csrfToken string)`
  - `templ SignupPage(form SignupForm, errs SignupErrors, csrfToken string)`
  - `templ SignupFormFragment(form SignupForm, errs SignupErrors, csrfToken string)`
  - `templ Layout(title string, user *auth.User, csrfToken string)` — the logout form needs the token even though the page itself may be the Index.
  - `templ Index(user *auth.User, csrfToken string)`

  Do NOT replace the typed `form`/`errs` arguments with `map[string]string` — that would drop form-value preservation (Email roundtrip would be lost) AND swap typed Errors structs for stringly-typed maps. The change in THIS plan is purely additive (appending `csrfToken string`).

- Handlers thread `csrf.Token(r)` into every page/fragment render call alongside the existing form + errs values:
  - `LoginPageHandler`, `SignupPageHandler`, `IndexHandler` and the error-fragment branches of `LoginPostHandler` / `SignupPostHandler`.

- `cmd/web/main.go` loads `SESSION_SECRET` (hex-encoded 32 bytes) → `hex.DecodeString` → []byte; logs a fatal error if missing or len != 32. Pass the []byte and `env` string into `NewRouter`.

- `NewRouter` signature acquires `csrfKey []byte` and `env string` params (if not already present from earlier plans) and calls `r.Use(auth.Mount(env, csrfKey))` immediately after `r.Use(auth.ResolveSession(store))`. Note: `ResolveSession` takes a single `*Store` argument (per Plan 03 — the cookie name comes from the `SessionCookieName` constant; do NOT call `ResolveSession(store, cookieName)`).
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Mount gorilla/csrf, add ui.CSRFField, wire every form, env-driven key</name>
  <read_first>
    backend/internal/web/router.go
    backend/internal/web/handlers_auth.go
    backend/internal/web/handlers.go
    backend/templates/layout.templ
    backend/templates/auth_login.templ
    backend/templates/auth_signup.templ
    backend/templates/auth_form_errors.templ
    backend/templates/index.templ
    backend/cmd/web/main.go
    backend/.env.example
    backend/internal/web/ui/ (existing Button/Card/Badge components — match the directory layout and templ file style)
    .planning/phases/02-authentication/02-RESEARCH.md (Pattern 7 — gorilla/csrf integration; Pitfall 1 — body consumption; Pitfall 2 — token absent on GET-rendered forms; Pitfall 7 — middleware order)
    .planning/phases/02-authentication/02-CONTEXT.md (D-14, D-15, D-24)
    .planning/phases/02-authentication/02-06-PLAN.md (Layout signature change to *auth.User; this plan extends it with csrfToken)
  </read_first>
  <behavior>
    Tests added to backend/internal/web/handlers_auth_test.go (extending Plans 04/05/06 suite):

    - TestCSRF_LoginMissingToken: POST /login with valid email+password but NO `_csrf` field → 403. No session row created.
    - TestCSRF_LoginValidToken: GET /login first to receive the csrf cookie; parse the cookie + render-extracted token; POST /login with `_csrf` set → 200/303 (normal flow). Pre-seeded user with known argon2 password.
    - TestCSRF_SignupMissingToken: POST /signup without `_csrf` → 403. SELECT COUNT(*) FROM users WHERE email = $1 == 0.
    - TestCSRF_SignupValidToken: GET /signup → POST /signup with valid token → 303 to /, user row inserted.
    - TestCSRF_LogoutMissingToken: pre-seed session; POST /logout WITH session cookie but WITHOUT _csrf → 403. SELECT COUNT(*) FROM sessions WHERE id = $1 == 1 (NOT deleted).
    - TestCSRF_LogoutValidToken: full GET / → POST /logout with token → 303, session row deleted.
    - TestCSRF_HeaderFallback: POST /login with `X-CSRF-Token` header (no form field) → token accepted (verifies `csrf.RequestHeader("X-CSRF-Token")` wiring for future HTMX hx-headers usage).
    - TestForms_ContainCSRFField (templ smoke): render LoginPage(LoginForm{}, LoginErrors{}, "abc"), SignupPage(SignupForm{}, SignupErrors{}, "abc"), Layout("X", &auth.User{Email:"x@y"}, "abc") to a buffer and assert each contains `name="_csrf"` AND `value="abc"`. Login fragment and signup fragment also covered.
    - TestRouter_CSRFMountedAfterResolveSession: a unit-level inspection — assert that NewRouter wires `auth.ResolveSession` before `auth.Mount` (D-24). Implementation: render a request that proves order, OR (simpler) read router.go source via the test using os.ReadFile and assert the substring `ResolveSession` appears before `auth.Mount` / `csrf.Protect`. Choose the source-file scan — deterministic and cheap.
    - TestMain_FailsFastOnMissingSecret: this is a build-time / startup-time assertion — added as a `TestLoadCSRFKey_*` set in a new `backend/cmd/web/main_test.go` (or `backend/internal/web/csrf_key_test.go` if main.go is too small to support a separate test target). Cases: missing env → error; len != 32 → error; valid hex 64-char → returns []byte{32}.
  </behavior>
  <action>
    1. `go get github.com/gorilla/csrf@v1.7.3` (run inside backend/). Confirm go.sum updated.

    2. Create `backend/internal/auth/csrf.go`:
       - Package `auth`. Import `github.com/gorilla/csrf` and `net/http`.
       - Export `func Mount(env string, key []byte) func(http.Handler) http.Handler` that returns
         `csrf.Protect(key, csrf.Secure(env != "dev"), csrf.SameSite(csrf.SameSiteLaxMode), csrf.Path("/"), csrf.FieldName("_csrf"), csrf.RequestHeader("X-CSRF-Token"))`.
       - Export `func LoadKeyFromEnv() ([]byte, error)` that reads `SESSION_SECRET`, hex-decodes, validates `len == 32`. Return sentinel error `ErrCSRFKeyInvalid` for empty or wrong-length input. `cmd/web/main.go` calls this and `log.Fatal`s on error.

    3. Create `backend/internal/web/ui/csrf_field.templ`:
       - `package ui`
       - `templ CSRFField(token string) { <input type="hidden" name="_csrf" value={ token }/> }`
       - Run `templ generate` after writing.

    4. Update `backend/templates/layout.templ`:
       - Signature: `templ Layout(title string, user *auth.User, csrfToken string)`.
       - Inside the logout `<form method="POST" action="/logout">`, FIRST child is `@ui.CSRFField(csrfToken)`. The button stays as the second child.
       - Import path: align with how Button/Card/Badge are imported in existing templates.

    5. Update `backend/templates/auth_login.templ`:
       - Append `csrfToken string` as the third argument; KEEP the existing `form LoginForm` and `errs LoginErrors` arguments from Plan 05 in place. Final: `templ LoginPage(form LoginForm, errs LoginErrors, csrfToken string)` and `templ LoginFormFragment(form LoginForm, errs LoginErrors, csrfToken string)`.
       - Each `<form>` has `@ui.CSRFField(csrfToken)` as the first child.
       - `LoginPage` wraps `@Layout("Sign in", nil, csrfToken)`.

    6. Update `backend/templates/auth_signup.templ`:
       - Append `csrfToken string` as the third argument; KEEP the existing `form SignupForm` and `errs SignupErrors` arguments from Plan 04 in place. Final: `templ SignupPage(form SignupForm, errs SignupErrors, csrfToken string)` and `templ SignupFormFragment(form SignupForm, errs SignupErrors, csrfToken string)`.
       - Same pattern: `@ui.CSRFField(csrfToken)` first child of every form; `@Layout("Sign up", nil, csrfToken)`.

    7. Update `backend/templates/auth_form_errors.templ` if it owns a `<form>` shell: thread `csrfToken string` through and embed `@ui.CSRFField(csrfToken)`. If it only renders error markup (FieldError/GeneralError per Plan 04), no change needed.

    8. Update `backend/templates/index.templ`:
       - `Index(user *auth.User, csrfToken string)` → wraps `@Layout("Xtablo", user, csrfToken)`.

    9. Update `backend/internal/web/handlers.go`:
       - `IndexHandler()` reads `auth.Authed(r.Context())` for user AND `csrf.Token(r)` for the token, then `templates.Index(user, csrf.Token(r)).Render(...)`.

    10. Update `backend/internal/web/handlers_auth.go` — every page/fragment render call now passes `csrf.Token(r)` ALONGSIDE the existing form + errs values (do not drop typed form/errs; do not collapse to map[string]string):
       - `SignupPageHandler`: `templates.SignupPage(SignupForm{}, SignupErrors{}, csrf.Token(r)).Render(...)`.
       - `SignupPostHandler`: on validation error / duplicate-email branch, render `SignupPage(form, errs, csrf.Token(r))` or `SignupFormFragment(form, errs, csrf.Token(r))`. Success branch (303 redirect) does not render templ.
       - `LoginPageHandler`: same pattern with `LoginPage(LoginForm{}, LoginErrors{}, csrf.Token(r))`.
       - `LoginPostHandler`: same pattern with `LoginPage(form, errs, csrf.Token(r))` / `LoginFormFragment(form, errs, csrf.Token(r))` on error branches.
       - `LogoutHandler`: no templ render; unaffected except its form must now arrive with `_csrf` (handled in layout.templ).
       - **Pitfall 1:** Audit every handler in this file. They MUST read form values via `r.PostFormValue(...)` only — never `io.ReadAll(r.Body)` or `json.NewDecoder(r.Body)`. Existing Plans 04/05 already use `r.PostFormValue` per their action sections; this is a regression guard.

    11. Update `backend/internal/web/router.go`:
       - After `r.Use(auth.ResolveSession(deps.Store))` (single-arg form, per Plan 03 — cookie name is read from the `SessionCookieName` constant inside the package; do NOT pass a second `cookieName` argument) and BEFORE any route group: `r.Use(auth.Mount(env, csrfKey))`.
       - The protected and public route groups stay as in Plan 06; csrf.Protect runs across both.
       - Add a comment above each `r.Use` referencing D-24 locked order.

    12. Update `backend/cmd/web/main.go`:
       - Load `SESSION_SECRET` via `auth.LoadKeyFromEnv()`. On error, `log.Fatalf("invalid SESSION_SECRET: %v ; generate with `openssl rand -hex 32`", err)`.
       - Pass `[]byte` key and `env` string into `NewRouter`.
       - Update `NewRouter` signature accordingly (if not already accepting these params from Plan 04/06 — read router.go first, extend additively).

    13. Update `backend/.env.example`:
       - Add `SESSION_SECRET=` line with a comment: `# 32 random bytes hex-encoded — generate with: openssl rand -hex 32`.
       - Add a placeholder or leave blank value; clearly NOT a real key.

    14. Run `cd backend && templ generate && go build ./...` and fix any signature drift in call sites (Phase 1 + Plans 04..06 tests may reference old `Layout` / `Index` signatures). All updates must be ADDITIVE to existing templ args — do not delete the form/errs parameters introduced by Plans 04/05.

    Anti-pattern guards:
    - `csrf.Mount` must be AFTER `auth.ResolveSession` (D-24, Pitfall 7).
    - Every `<form method="POST">` in `.templ` files MUST contain `@ui.CSRFField(token)` as a literal first child (Pitfall 2). Acceptance grep enforces.
    - Handlers MUST NOT touch `r.Body` directly (Pitfall 1).
    - csrf key MUST come from env, not a compile-time constant (D-15).
    - Cookie name for csrf is gorilla's default `_gorilla_csrf` — do NOT override; D-12 only governs the session cookie name.
    - Do NOT replace the typed `SignupForm`/`SignupErrors`/`LoginForm`/`LoginErrors` arguments with `map[string]string` — Email roundtrip and typed error handling from Plans 04/05 must be preserved.
    - `ResolveSession` is single-arg (`*Store`); a two-arg form `ResolveSession(store, cookieName)` does not exist in Plan 03.
  </action>
  <verify>
    <automated>cd backend &amp;&amp; templ generate &amp;&amp; go build ./... &amp;&amp; TEST_DATABASE_URL="$DATABASE_URL" go test ./internal/web/ ./internal/auth/ ./templates/ ./cmd/web/ -count=1 -run "TestCSRF|TestForms_ContainCSRFField|TestRouter_CSRFMountedAfterResolveSession|TestLoadCSRFKey|TestSignup|TestLogin|TestLogout|TestProtected|TestLayout" 2>&amp;1 | tee /tmp/p07.log | tail -80 &amp;&amp; ! grep -E "^FAIL|^--- FAIL" /tmp/p07.log</automated>
  </verify>
  <acceptance_criteria>
    - `go.mod` contains `github.com/gorilla/csrf v1.7.3`.
    - `backend/internal/auth/csrf.go` exists; `grep -c "func Mount" backend/internal/auth/csrf.go == 1`; `grep -c "csrf.Protect" backend/internal/auth/csrf.go == 1`; `grep -c "csrf.Secure" backend/internal/auth/csrf.go == 1`; `grep -c "csrf.SameSiteLaxMode" backend/internal/auth/csrf.go == 1`; `grep -c "LoadKeyFromEnv" backend/internal/auth/csrf.go == 1`.
    - `backend/internal/web/ui/csrf_field.templ` exists; `grep -c "templ CSRFField" backend/internal/web/ui/csrf_field.templ == 1`; `grep -c 'name="_csrf"' backend/internal/web/ui/csrf_field.templ == 1`.
    - Every templ file with a `<form method="POST">` contains `@ui.CSRFField(`. Concretely, for each f in {layout.templ, auth_login.templ, auth_signup.templ}: `grep -v '^//' $f | awk '/<form/,/<\/form>/' | grep -c 'ui.CSRFField\|CSRFField(' >= 1`.
    - Typed form/errs args preserved in templ signatures: `grep -c "templ SignupPage(form SignupForm, errs SignupErrors, csrfToken string)" backend/templates/auth_signup.templ == 1`; same for `SignupFormFragment`, `LoginPage`, `LoginFormFragment`. No occurrence of `map[string]string` in the four form templ files: `grep -c "map\[string\]string" backend/templates/auth_signup.templ backend/templates/auth_login.templ` returns 0.
    - `backend/internal/web/router.go` has BOTH `auth.ResolveSession` and `auth.Mount` (or `csrf.Protect`) — and ResolveSession appears on an earlier line: `awk '/auth\.ResolveSession/{r=NR} /auth\.Mount|csrf\.Protect/{c=NR} END{exit !(r>0 && c>0 && r<c)}' backend/internal/web/router.go` exits 0.
    - `auth.ResolveSession` call in router.go is single-arg (no `, cookieName` second arg): `grep -E "auth\.ResolveSession\([^,)]*,\s*[a-zA-Z]" backend/internal/web/router.go | wc -l` returns 0.
    - `cmd/web/main.go` calls `auth.LoadKeyFromEnv` and `log.Fatal` (or equivalent) on error: `grep -c "LoadKeyFromEnv" backend/cmd/web/main.go == 1`; `grep -c "SESSION_SECRET" backend/cmd/web/main.go >= 1`.
    - `backend/.env.example` mentions `SESSION_SECRET`: `grep -c "SESSION_SECRET" backend/.env.example >= 1`.
    - `cd backend && templ generate` exits 0.
    - `cd backend && go build ./...` exits 0.
    - `cd backend && TEST_DATABASE_URL=$DATABASE_URL go test ./internal/web/ ./internal/auth/ ./cmd/web/ -run "TestCSRF|TestForms_ContainCSRFField|TestRouter_CSRFMountedAfterResolveSession|TestLoadCSRFKey" -count=1` exits 0 with all named tests PASS.
    - Existing Plan 04/05/06 tests (TestSignup_*, TestLogin_*, TestLogout_*, TestProtected_*, TestLayout_*) still PASS — they must be updated to fetch a valid `_csrf` token (from GET response cookie + form-rendered hidden input) and include it on every POST. The plan executor MUST update those tests in-place rather than disable them.
    - Grep gate forbidding bare body reads in auth handlers: `grep -E "io.ReadAll\\(r\\.Body|json\\.NewDecoder\\(r\\.Body" backend/internal/web/handlers_auth.go | grep -v '^#' | wc -l` returns 0.
  </acceptance_criteria>
  <done>AUTH-06 closed. Every state-changing route in Phase 2 (POST /signup, POST /login, POST /logout) requires a valid CSRF token; every form ships one by construction via @ui.CSRFField.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Cross-origin attacker → user's authenticated browser → POST | gorilla/csrf double-submit + SameSite=Lax cuts both same-site and cross-site CSRF |
| Form submission → csrf.Protect → handler | Middleware must run BEFORE handler; ordering is load-bearing (D-24, Pitfall 7) |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-2-08 | Tampering | CSRF on POST /signup, POST /login, POST /logout | mitigate | gorilla/csrf v1.7.3 mounted on the chi stack with `csrf.Secure(env != "dev")` + `csrf.SameSiteLaxMode`; every templ form ships `@ui.CSRFField(token)` as its first child. Verified by TestCSRF_*MissingToken (403) and TestCSRF_*ValidToken (200/303). |
| T-2-08a | Tampering | Hidden CSRF input missing from a future form | mitigate | Acceptance grep gates require `@ui.CSRFField` inside every `<form method="POST">` in templ files. Reusable `ui.CSRFField` component makes the correct path the path of least resistance. |
| T-2-08b | Information disclosure | csrf key stored in repo or compile-time constant | mitigate | `auth.LoadKeyFromEnv()` reads `SESSION_SECRET` from env; main.go fails fast on missing/short key. `.env.example` documents the value without a real key. |
| T-2-08c | Spoofing | Body consumed by csrf middleware → handler sees empty form | mitigate | Handlers exclusively use `r.PostFormValue(...)`; acceptance grep forbids `r.Body` reads in handlers_auth.go. Verified by existing Plan 04/05 tests passing after the csrf mount lands. |
| T-2-08d | Tampering | csrf.Protect mounted before ResolveSession (wrong order) | mitigate | D-24 locked order; router.go acceptance assertion uses `awk` line-number compare to enforce `ResolveSession` appears before `auth.Mount`. |
</threat_model>

<verification>
- TestCSRF_LoginMissingToken / TestCSRF_LoginValidToken pass.
- TestCSRF_SignupMissingToken / TestCSRF_SignupValidToken pass.
- TestCSRF_LogoutMissingToken / TestCSRF_LogoutValidToken pass.
- TestCSRF_HeaderFallback passes (X-CSRF-Token header works for future HTMX hx-headers usage).
- TestForms_ContainCSRFField passes — every form-rendering templ contains the hidden field.
- TestRouter_CSRFMountedAfterResolveSession passes — middleware order is correct.
- TestLoadCSRFKey_Missing / TestLoadCSRFKey_WrongLength / TestLoadCSRFKey_Valid pass.
- All Plan 04/05/06 tests still pass after being updated to acquire and submit a valid `_csrf` token.
- Manual browser walkthrough: sign up → /, click Log out → /login, log back in. Every form submission has a `_csrf` hidden input visible in DevTools.
</verification>

<success_criteria>
- AUTH-06 closed: all state-changing POSTs require a valid CSRF token; missing/invalid → 403.
- The reusable `ui.CSRFField` component is the canonical way to embed CSRF in future templ forms.
- SESSION_SECRET is documented and loaded from env; missing key fails the binary fast at startup.
- Middleware order (`ResolveSession` → `csrf.Protect` → route groups) matches D-24 exactly.
- Typed `SignupForm`/`SignupErrors`/`LoginForm`/`LoginErrors` arguments from Plans 04/05 remain intact (no regression to `map[string]string`).
</success_criteria>

<output>
Create `.planning/phases/02-authentication/02-07-SUMMARY.md` recording:
- Final list of templ files audited and the form-by-form CSRF-field placement
- Confirmation that all Plan 04/05/06 tests were updated (or already were CSRF-aware)
- Any decisions on token surfacing in HTMX hx-headers (left for a future polish pass; currently the form field is the canonical path)
- Snapshot of the final router middleware chain (single-source-of-truth diff base for Phase 3 / future security hardening)
</output>
