| | @@ -26,11 +26,11 @@ invite codes, `RegistrationPolicy`, organisation management UI, roles beyond own |
| 26 | 26 | - [x] Application: `claim_instance` use case — token-gated, creates org → user → |
| 27 | 27 | owner membership, returns the owner signed in |
| 28 | 28 | - [x] Application: `login` use case — verifies credentials, returns an `Actor` |
| 29 | | −- [ ] Infrastructure: migrations `001`–`003`, SQLite implementations |
| 30 | | −- [ ] Infrastructure: `sessions` table + session storage |
| 31 | | −- [ ] Boot: mint and print a `SetupToken` when unclaimed; register it in app context |
| 32 | | −- [ ] Web: `/setup` claim page; every other route redirects there while unclaimed |
| 33 | | −- [ ] Web: login page, logout, `current_actor(cx)` helper |
| 29 | +- [x] Infrastructure: migrations + SQLite implementations |
| 30 | +- [x] Infrastructure: `sessions` table + session storage |
| 31 | +- [x] Boot: mint and print a `SetupToken` when unclaimed; register it in app context |
| 32 | +- [x] Web: `/setup` claim page; every other route redirects there while unclaimed |
| 33 | +- [x] Web: login page, logout, `current_actor(cx)` helper |
| 34 | 34 | - [ ] `/api/me` — first `/api` route, proves the use case layer has two consumers |
| 35 | 35 | |
| 36 | 36 | ### Done when |
| | @@ -39,21 +39,33 @@ A fresh database prints a setup token at boot; `/setup` with that token creates |
| 39 | 39 | owner and signs them in; logging out and back in works; `/api/me` returns that |
| 40 | 40 | identity. |
| 41 | 41 | |
| 42 | +Everything but `/api/me` is done and verified in a browser. |
| 43 | + |
| 44 | +### Resolved |
| 45 | + |
| 46 | +- **`__Host-` cookies need a secure context — and it bit.** The claim succeeded, the |
| 47 | + server recorded sessions, and every page still rendered signed out, because the |
| 48 | + browser silently discarded a `Secure` cookie served over plain HTTP. Nothing errored |
| 49 | + on either side. Fixed with `InsecureCookieTokenStore` behind |
| 50 | + `STEID_INSECURE_COOKIES`, off by default — see [runbook.md](runbook.md#configuration). |
| 51 | +- **Claim TOCTOU** is now covered by a test that drives a real claim through the SQLite |
| 52 | + repos and asserts `unique(orgs.name)` / `unique(users.email)` refuse the second. |
| 53 | + |
| 42 | 54 | ### Watch for |
| 43 | 55 | |
| 44 | | −- **`__Host-` cookies need a secure context.** Topcoat's session cookie is |
| 45 | | − `__Host-`-prefixed and `Secure`. Browsers treat `http://localhost` as trustworthy so |
| 46 | | − dev over plain HTTP *should* work — verify this as soon as the login page exists, |
| 47 | | − because if it's wrong, login fails silently and looks like a bug in our code. |
| 48 | 56 | - **CSRF.** `SameSite=Lax` blocks cross-site POSTs, which covers the common case. |
| 49 | 57 | Whether forms also want tokens is an open decision, not a default to pick quietly. |
| 50 | | −- **Claim is TOCTOU.** `is_claimed` then write is not atomic; the `UNIQUE` constraints |
| 51 | | − on email and org name are what actually serialise concurrent claims. Integration-test |
| 52 | | − this once SQLite lands. |
| 58 | + Still undecided. |
| 59 | +- **No rate limiting anywhere.** `/login` and `/setup` accept unlimited attempts. The |
| 60 | + setup token has 256 bits so brute force is not the worry; password guessing is. |
| 61 | +- **Form errors are invisible.** A wrong token or password redirects back with no |
| 62 | + message — deliberate, so failures can't be used to probe, but indistinguishable from |
| 63 | + a broken form. Flash messages are the fix and don't exist yet. |
| 64 | +- **Session sweeping is never called.** `sweep_expired` exists and is tested but |
| 65 | + nothing invokes it, so expired rows accumulate. Expiry is enforced on read, so this |
| 66 | + is tidiness rather than a security hole. |
| 53 | 67 | - **Foreign key ordering.** The org must be saved before the user — attempt #2 had to |
| 54 | | − fix this in both `bootstrap_owner` and `register_user`. See |
| 55 | | − [progress.md](progress.md#identity). |
| 56 | | −- **Bootstrap must be idempotent.** It runs on every boot, not just the first. |
| 68 | + fix this in two places. Enforced now: `foreign_keys(true)` plus a test. |
| 57 | 69 | - Never log or `Debug`-print a password. `PasswordHash` is opaque on purpose. |
| 58 | 70 | |
| 59 | 71 | ## Backlog |