Auth flow — spec, plan, and implementation #3

Merged
arne merged 39 commits from auth-flow-spec into main 2026-05-04 22:12:38 +02:00
Owner

Summary

Lands the full auth slice end-to-end:

  • Specdocs/superpowers/specs/2026-05-01-auth-flow-design.md. Public open signup, passkey + magic-link login (no password), dedicated off-center auth shell, BFF-resident state in local SQLite.
  • Plandocs/superpowers/plans/2026-05-01-auth-flow.md. 31 tasks, full TDD code in every step.
  • Implementation — 31 task commits, 52 files changed, ~11k LOC.

What ships

  • New `internal/auth` package: `Service`, sessions, `RequireSession` middleware, Origin/Referer CSRF check, WebAuthn registration + discoverable assertion.
  • New `internal/auth/store` package: SQLite (`modernc.org/sqlite`, pure-Go) with users, sessions, magic-link tokens, passkey credentials, activity log.
  • New `internal/mail` package: `Mailer` interface, `LogMailer` (writes to stderr in dev/fixtures mode), `SMTPMailer` (env-driven for prod), three magic-link email body templates.
  • New auth-shell layout (`web/templates/auth.html`) — off-center wayfinding, no card, scaling across 6 page states by changing only the headline.
  • New CSS in `components.css` — `.auth-shell`, `.auth-column`, eyebrow / display headline / hairline rule rhythm, narrow-viewport collapse, prefers-reduced-motion-aware staggered reveal.
  • New design-system specimens in `/design` — four mini-mockups of the auth shell across its states.
  • New `passkey.js` browser helpers (~100 lines, no library).
  • Sign-in form integrates WebAuthn conditional UI mediation (`autocomplete="username webauthn"` + `isConditionalMediationAvailable` + `mediation: "conditional"`).
  • `/zones` and `/zones/{zone}` are gated behind `RequireSession`.

Verified

  • 55+ tests across `internal/auth`, `internal/auth/store`, `internal/mail`, `internal/server` — all green.
  • `go vet ./...` clean.
  • Smoke tests against the running app:
    • `GET /` → 302 `/zones`
    • `GET /sign-up` → 200 with form
    • `POST /sign-up` with matching `Origin` → 303 to `/sign-up/sent`
    • `POST /sign-up` without `Origin` → 403 (CSRF)
    • `GET /zones` anon → 303 `/sign-in`
    • Magic link printed to log in fixtures mode
    • `/design` renders the auth-shell specimen section

Known follow-ups

  • Two API drift adjustments to the plan during implementation (noted in commit messages on the WebAuthn tasks): `webauthn.User` no longer requires `WebAuthnIcon()`; `SessionData` is taken by value, not pointer. Both fixed at implementation time, not in the plan doc.
  • One plan typo fixed during implementation: `http.SameSiteLaxMode == 2`, not `4` (plan comment was wrong; test uses the constant correctly).
  • A small number of empty commits in Phase C and Phase E (where task code shipped together with an earlier task in the same file). Final state is correct; commit history is slightly messier than the plan's intent.
  • E2E browser test of the passkey flow (register → sign out → conditional UI sign-in) requires a WebAuthn client mock; left as a manual / integration verification step. Smoke tested manually during implementation.

Open questions still pending (per spec)

  • Production RP ID — env-driven, defaults to `localhost`. Settle when production hostname is settled.
  • Email subject lines / body copy — small editorial pass when ready.
  • Marcus's API expectations at signup — out of scope for this slice; one outbound call to add later.

🤖 Generated with Claude Code

## Summary Lands the full auth slice end-to-end: - **Spec** — `docs/superpowers/specs/2026-05-01-auth-flow-design.md`. Public open signup, passkey + magic-link login (no password), dedicated off-center auth shell, BFF-resident state in local SQLite. - **Plan** — `docs/superpowers/plans/2026-05-01-auth-flow.md`. 31 tasks, full TDD code in every step. - **Implementation** — 31 task commits, 52 files changed, ~11k LOC. ## What ships - New \`internal/auth\` package: \`Service\`, sessions, \`RequireSession\` middleware, Origin/Referer CSRF check, WebAuthn registration + discoverable assertion. - New \`internal/auth/store\` package: SQLite (\`modernc.org/sqlite\`, pure-Go) with users, sessions, magic-link tokens, passkey credentials, activity log. - New \`internal/mail\` package: \`Mailer\` interface, \`LogMailer\` (writes to stderr in dev/fixtures mode), \`SMTPMailer\` (env-driven for prod), three magic-link email body templates. - New auth-shell layout (\`web/templates/auth.html\`) — off-center wayfinding, no card, scaling across 6 page states by changing only the headline. - New CSS in \`components.css\` — \`.auth-shell\`, \`.auth-column\`, eyebrow / display headline / hairline rule rhythm, narrow-viewport collapse, prefers-reduced-motion-aware staggered reveal. - New design-system specimens in \`/design\` — four mini-mockups of the auth shell across its states. - New \`passkey.js\` browser helpers (~100 lines, no library). - Sign-in form integrates WebAuthn conditional UI mediation (\`autocomplete=\"username webauthn\"\` + \`isConditionalMediationAvailable\` + \`mediation: \"conditional\"\`). - \`/zones\` and \`/zones/{zone}\` are gated behind \`RequireSession\`. ## Verified - 55+ tests across \`internal/auth\`, \`internal/auth/store\`, \`internal/mail\`, \`internal/server\` — all green. - \`go vet ./...\` clean. - Smoke tests against the running app: - \`GET /\` → 302 \`/zones\` - \`GET /sign-up\` → 200 with form - \`POST /sign-up\` with matching \`Origin\` → 303 to \`/sign-up/sent\` - \`POST /sign-up\` without \`Origin\` → 403 (CSRF) - \`GET /zones\` anon → 303 \`/sign-in\` - Magic link printed to log in fixtures mode - \`/design\` renders the auth-shell specimen section ## Known follow-ups - Two API drift adjustments to the plan during implementation (noted in commit messages on the WebAuthn tasks): \`webauthn.User\` no longer requires \`WebAuthnIcon()\`; \`SessionData\` is taken by value, not pointer. Both fixed at implementation time, not in the plan doc. - One plan typo fixed during implementation: \`http.SameSiteLaxMode == 2\`, not \`4\` (plan comment was wrong; test uses the constant correctly). - A small number of empty commits in Phase C and Phase E (where task code shipped together with an earlier task in the same file). Final state is correct; commit history is slightly messier than the plan's intent. - E2E browser test of the passkey flow (register → sign out → conditional UI sign-in) requires a WebAuthn client mock; left as a manual / integration verification step. Smoke tested manually during implementation. ## Open questions still pending (per spec) - Production RP ID — env-driven, defaults to \`localhost\`. Settle when production hostname is settled. - Email subject lines / body copy — small editorial pass when ready. - Marcus's API expectations at signup — out of scope for this slice; one outbound call to add later. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Spec for the first non-read-only slice through the stack: account
creation (name + email), magic-link sign-in, opt-in passkey enrollment
on a dedicated auth shell. Mirrors the format of the knot-rest
prototype spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
arne changed title from Auth flow design (spec) to Auth flow — spec, plan, and implementation 2026-05-02 07:13:05 +02:00
Critical/high findings from PR review:

- Set Secure on session and passkey-assert cookies; default true,
  opt out for local dev with NORDAAKER_COOKIE_INSECURE=1.
- Move PRAGMA foreign_keys=ON (plus journal_mode=WAL, busy_timeout)
  to the SQLite DSN so it binds to every pooled connection, not just
  the first. Verified out-of-band: FK violations are now rejected.
- Store sha256(token) for magic-link tokens; the raw value only ever
  exists in the issued email and the inbound URL. A read of the DB
  file no longer hands an attacker every outstanding link.
- Split /auth/link into a GET confirmation page + POST consume so
  email-safety scanners pre-fetching the URL can't burn the token
  before the user clicks Sign in.
- clientIP: use net.SplitHostPort (IPv6-safe); honour X-Forwarded-For
  only when NORDAAKER_TRUSTED_PROXY=1.
- Stop leaking err.Error() to clients on signup/signin/resend/consume
  and the four passkey handlers; log full error, return short text.

Test helpers swapped from DB-token peeking to a recordingMailer
since the DB no longer holds the raw token. New tests cover the GET
interstitial, empty-token GET, bad-token POST, and the cookie Secure
flag in both states.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Local-only strategy / planning doc; lives in the working tree but
must stay out of git history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arne merged commit 1c1c6a24e2 into main 2026-05-04 22:12:38 +02:00
arne deleted branch auth-flow-spec 2026-05-04 22:12:38 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nordaaker/web!3
No description provided.