Files
solopm-server/plans/05-auth.md
T

48 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 05 — Auth, Sessions, OAuth & Tokens
Two credential paths, one identity: browser SPA uses **server-side sessions in HttpOnly cookies** (alexedwards/scs, Postgres store); scripts/CLI use **personal access tokens**. Local email/password and GitHub/Gitea OAuth all resolve to the same `users` row via `oauth_accounts`.
## Registration & login
- [ ] `POST /auth/register`: validate email format, username `[a-z0-9-_]{3,32}` lowercase-unique (citext), password ≥ 10 chars; bcrypt cost 12
- [ ] **First registered user gets `is_admin = true`** (instance bootstrap; ids 12 are reserved seeds, so the first human is id 3)
- [ ] `POST /auth/login`: verify bcrypt; on success `scs.RenewToken` (session fixation) then store user id; emit `auth.login_succeeded` / `auth.login_failed` (failed: no user enumeration in the response — generic message)
- [ ] Rate-limit register/login/password endpoints per IP (10/min)
- [ ] `POST /auth/logout`: destroy session
- [ ] Session config: Postgres store (`sessions` table), 30-day lifetime with idle-timeout 7 days, cookie `HttpOnly`, `SameSite=Lax`, `Secure` when BASE_URL is https
## OAuth (GitHub + Gitea)
- [ ] Standard authorization-code flow; `state` parameter stored in session, verified at callback (CSRF)
- [ ] GitHub: fixed endpoints; Gitea: endpoints derived from `GITEA_URL` (self-hosted friendly)
- [ ] Provider buttons hidden in the SPA when the corresponding env vars are unset (`/api/v1/me`-adjacent bootstrap config endpoint or injected at index render)
- [ ] Callback logic, in order:
- [ ] Existing `oauth_accounts(provider, provider_user_id)` row → log that user in
- [ ] Else: fetch the provider's **verified** primary email; if a user with that email exists → **link**: insert oauth_accounts row for that user, log in
- [ ] Else: create a new user (username derived from provider login, de-duplicated with numeric suffix; `password_hash` NULL) + oauth_accounts row, log in
- [ ] A linked user may later set a password via account settings (password change with no current password required only when `password_hash` IS NULL)
- [ ] Never auto-link on an **unverified** provider email (account-takeover vector) — fall through to create-new-user
## Personal access tokens
- [ ] Format `solopm_<40 chars base62>`; store SHA-256 hex in `token_hash`; show plaintext exactly once at creation
- [ ] `prefix` (first 12 chars) stored for display in the token list
- [ ] Bearer auth middleware: constant-time hash lookup; reject revoked/expired; update `last_used_at` (throttled to once/minute per token)
- [ ] Tokens act as the full user (no scopes in v1 — recorded in [17-decisions.md](17-decisions.md))
- [ ] Emit `auth.token_created` / `auth.token_revoked`
## Authorization model
- [ ] `is_admin` (system-wide): manage users, run cull/reindex, see admin endpoints. Admins are **not** implicit members of every project — they see only their own projects in normal UI (admin endpoints are separate)
- [ ] Project `owner`: everything a member can, plus: edit project settings, manage members/labels/webhooks, transfer ownership, delete project
- [ ] Project `member`: full read/write on the project's issues, epics, comments, wiki, attachments, links; manage own watches/favorites
- [ ] Non-member: **404** on all project-scoped routes
- [ ] System user (id 1) and zombie user (id 2): cannot log in (no password, no oauth rows, login explicitly rejects ids 12), never listed in pickers
## User deletion edge cases
- [ ] A user who is the sole owner of any project cannot be deleted (409 `conflict`, message lists the projects) — transfer ownership first
- [ ] Deletion is a soft delete; sessions destroyed and PATs revoked immediately
- [ ] At cull time the zombie user inherits authored records ([12-retention.md](12-retention.md)); audit_log keeps the original user id (immutable)
- [ ] Self-service account deletion follows the same rules as admin deletion