Local user authentication: register, login, logout, sessions (#11) #38
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!38
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "11-local-user-auth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #11.
Substantial PR — the bits all have to land together for the acceptance criteria to even be testable end-to-end (register depends on sessions depends on the middleware lookup, etc.). Full reasoning for the load-bearing decisions lives in ADR-0004 (server-side sessions over JWT) and in code comments.
Surface added
POST /api/auth/registerPOST /api/auth/loginPOST /api/auth/logoutGET /api/auth/meKey decisions
@node-rs/argon2(m=19MiB, t=2, p=1 — OWASP profile). napi-rs prebuilts, so nonode-gyppain in the container.sessionstable. Cookie is opaque; the row is the source of truth. ADR-0004 explains why DB-backed over JWT for Carol's deployment shape.HttpOnly,SameSite=Lax,Securein prod,Path=/.REGISTRATION_POLICYenv var (defaultopen). Onlyopenis wired in this PR;inviteandadmin-approvalreturn 503policy_not_implementedso a misconfigured deployment fails closed, not open.is_admin = 1). TOCTOU window betweencount()andcreate()accepted at this scale; documented inline.email,ip) per 15 min → 429. Keyed by(email, ip)so an attacker swapping IPs can't lock out the real user from their own address.users+local_identities(1:1). A futureoauth_identitiestable (#12) will sit alongside; both FK back tousersso one account owns both identity types.getSession()(lib/auth/session.ts) now does a real DB lookup, replacing #10's cookie-presence stub. Public allowlist unchanged.Tests (80 total — 65 SQLite + 15 Postgres, all passing)
tests/auth/password.test.ts— argon2 round-trip, salt freshness, malformed-hash safety. No DB.tests/auth/services.test.ts— dual-engine viadescribePerEngine: register, login success / wrong-password / unknown-email, rate-limit kicks in, rate-limit is(email, ip)-keyed, session validity / expiry / revocation, isolation between two users, registration policy 503.tests/auth/api.test.ts— single-engine SQLite HTTP-layer tests: status codes, JSON shape,Set-Cookieattributes, the "no plaintext password ever in a response" guard.tests/middleware.test.ts— updated to mock@/lib/auth/sessionso it remains a unit test instead of needing a DB.End-to-end against the production
node .next/standalone/server.jsmatches the test matrix (register → me → logout → me-returns-401, plus rate-limit and password-rejection paths).Test plan
npm run lint/npm run typecheck/npm run build— all greennpm test(SQLite only) — 60 pass, 20 Postgres skipnpm testwith Postgres 16 via podman — 80/80 pass (parity confirmed)