# CLAUDE.md — Phase 2: Authentication

## Scope of This File

This file instructs you (Claude Code) to implement **Phase 2 only** of the Seedbox Command Center.

**Do not implement phases 3 through 15.** A separate CLAUDE.md will be written for each subsequent phase. Stay in scope.

## Authoritative Specification

`PROJECT.md` §5.6 is the authoritative spec for authentication. Read it before writing any code in this phase. Also read:
- `PROGRESS.md` for Phase 0 + Phase 1 state.
- `DECISIONS.md` for binding architectural decisions.
- The existing `internal/server` to understand how middleware is wired.

If you encounter a genuine ambiguity that `PROJECT.md` does not resolve, document it in `DECISIONS.md` and proceed.

## Phase 2 Mission

WebAuthn passkey authentication. After Phase 2, every `/api/*` route except `/api/auth/*` and `/api/system/health` requires a valid session. The operator registers credentials on first use from a desktop browser; the registered credentials may then be used from any device that supports them, with iOS providing biometric unlock through Face ID or Touch ID.

No passwords. Recovery codes generated at registration are stored by the operator out-of-band.

## Deliverables Checklist

Phase 2 is complete when every item below is true:

- [ ] `internal/auth` package implements WebAuthn registration and assertion using a well-maintained Go WebAuthn library (`github.com/go-webauthn/webauthn` is the default; document if you choose another).
- [ ] SQLite migration `003_phase2_auth.sql` adds the `webauthn_credentials`, `recovery_codes`, and `sessions` tables exactly as in `PROJECT.md` Appendix A.
- [ ] Endpoints (under `/api/auth`):
    - `POST /api/auth/webauthn/register/start` — returns challenge + options; allowed only when no credentials exist (bootstrap) or when a valid session is present (adding a new device).
    - `POST /api/auth/webauthn/register/finish` — completes registration, returns recovery codes ONCE (and never again).
    - `POST /api/auth/webauthn/login/start` — returns assertion challenge.
    - `POST /api/auth/webauthn/login/finish` — completes assertion, issues session cookie.
    - `POST /api/auth/logout` — invalidates the session token in the `sessions` table.
    - `GET /api/auth/me` — returns the active session info (created_at, last_seen_at, credential name).
    - `POST /api/auth/recovery` — accepts a recovery code, marks it used, allows re-enrollment of new credentials. Strict rate limit; full audit.
- [ ] Session middleware: reads `cc_session` HTTP-only Secure SameSite-Strict cookie; validates against the `sessions` table; renews `last_seen_at`; rejects expired sessions with 401. Default session lifetime 30 days with renewal on activity (configurable via `auth.session_lifetime_seconds` in `config/system.yaml`).
- [ ] WebAuthn relying party ID is the Tailscale hostname (`<machine>.<tailnet>.ts.net`), configurable via `auth.rp_id` in `config/system.yaml`. Document why this works given the Tailscale-only network model.
- [ ] Recovery codes: 10 generated at first registration. Stored as bcrypt-hashed values in `recovery_codes`. Shown to the operator exactly once in the registration response; copy-to-clipboard button in the UI.
- [ ] Audit log: every register, login, logout, recovery-code-redemption writes to `audit_log` (table already exists from Phase 0) with actor, IP, user-agent.
- [ ] Frontend:
    - First-visit flow: if no credentials exist, show a "Register this device" screen using `@simplewebauthn/browser`.
    - Subsequent visits: show login screen with a single "Sign in with passkey" button.
    - Settings: a "Devices" panel listing registered credentials with last-used timestamps and a remove button (cannot remove the last credential).
    - Recovery code display modal that requires explicit "I've saved these" confirmation before dismissing.
- [ ] Rate limiting on `/api/auth/login/*` and `/api/auth/recovery` (e.g., 5 attempts / 5 minutes per IP; configurable). Recovery endpoint is more strict.
- [ ] Tests: registration round-trip with a synthesized authenticator, session middleware accepts valid / rejects expired / rejects tampered cookies, recovery code one-time use, rate limiter triggers.
- [ ] `PROGRESS.md` updated. `DECISIONS.md` records: library chosen, RP ID strategy, session lifetime default, recovery code count default.

## Phase 2 Database Scope

Migration `003_phase2_auth.sql` creates:

- `webauthn_credentials`
- `recovery_codes`
- `sessions`

The `audit_log` table already exists (Phase 0); use it.

## Repository Layout

```
internal/
  auth/
    webauthn.go          # registration + assertion ceremonies
    sessions.go          # cookie issue, validation, renewal
    recovery.go          # recovery code hashing, redemption
    middleware.go        # http.Handler that enforces auth
    ratelimit.go         # per-IP token bucket
    *_test.go
```

Files added under existing packages:
- `internal/server/routes_auth.go` — the `/api/auth/*` handlers.
- `internal/server/server.go` — wire the auth middleware to `/api/*` except auth + health.

Frontend additions:
- `web/src/pages/Login.tsx`
- `web/src/pages/Register.tsx`
- `web/src/pages/Settings/Devices.tsx`
- `web/src/components/RecoveryCodesModal.tsx`
- `web/src/lib/auth.ts` — fetch wrappers + state hook.

Do not create `internal/eventbus`, `internal/integrations/qbit`, or any other directory belonging to a later phase.

## Working Rules

**Trust the library.** Use `go-webauthn/webauthn` exactly as documented. The WebAuthn ceremonies are precise; do not invent challenge handling.

**Cookies are HTTP-only, Secure, SameSite=Strict.** Always. No exceptions for development. (`dev_mode` does not change this — the cost of getting cookie attributes wrong is too high.)

**Rate limit aggressively on the recovery endpoint.** Recovery codes are the only path to re-enroll if all credentials are lost; treat the endpoint like a vault door.

**Audit every state change.** Add an audit log entry on every register, login, logout, and recovery redemption. The audit log is operator-facing.

**The first registration is bootstrap.** When `webauthn_credentials` is empty, registration is unauthenticated. Once one credential exists, registration requires an active session. The recovery endpoint is the only other unauthenticated path.

**Update `PROGRESS.md` continuously.** Same Phase 0 cadence.

## What "Phase 2 Complete" Looks Like

After Phase 2 ships, the operator can:

1. Reach `https://<tailscale-hostname>:8443/` from a desktop browser on the tailnet.
2. See a "Register this device" prompt. Complete the WebAuthn ceremony with a USB security key or platform authenticator.
3. Receive ten recovery codes; copy and save them to a password manager.
4. Reach the same URL from their iPhone (already on the tailnet via the Tailscale app), authenticate with Face ID, and use the PWA.
5. Add the iPhone as a second credential from Settings → Devices.
6. Delete the desktop credential. Re-enroll a new desktop credential by tapping "Add device" while logged in on iPhone.
7. If all credentials are lost, visit `/recovery`, enter one of the saved recovery codes, and re-enroll a fresh credential. The used recovery code is marked consumed in `recovery_codes` and cannot be used again.
8. Every step above appears in `/api/system/audit-log` with timestamps and IPs.

## Begin

Read `PROJECT.md §5.6` and the existing `internal/server` middleware wiring. Create `migrations/003_phase2_auth.sql` first, then `internal/auth`, then the handlers, then the frontend. Update `PROGRESS.md` after each subsystem. When the deliverables checklist is fully satisfied, write a final `PROGRESS.md` entry and stop. Do not begin Phase 3.
