Phase 0 research resolves the library choices (jsonwebtoken + bcryptjs, chosen partly to avoid native-build friction on Windows dev environments), the Redis-backed revocation-denylist shape (reusing 002's own jti-replay-protection pattern exactly), a 4-hour token lifetime, and why fastify.authenticate populating the already-shared reqContext.actorId/actorType retroactively makes every audit trail since 007 accurate for real agent/admin actions instead of always 'unknown'. Also surfaces and scopes a real gap found along the way: User (login identity) and Agent (routing/skills profile) have never been linked. Adds Agent.userId as a nullable FK now (cheap, additive) without building the actual linking workflow, which belongs in 006's own identity/agents admin screens as a later, separate piece of work. Phase 1 adds data-model.md, the login/self-identity/account-creation/ logout contract, and five quickstart scenarios including a specific requirement to re-verify at least one already-shipped admin route per module (002-009), not just this feature's own new endpoints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2.5 KiB
2.5 KiB
Contract: Identity and Authentication
Login
POST /auth/login— body{ email, password }.401on any failure (wrong password, no such account, or a deactivated account) with an identical response body/status in every case (FR-002/SC-003) — never a distinguishable "no such user" vs "wrong password."200with{ token, user: { id, email, name, role } }on success.
Self-identity
GET /auth/me— gated byfastify.authenticate.401if the token is missing/invalid/ expired/revoked.401if the account behind a structurally-valid token no longer exists or is deactivated (re-validated against current state, not the token's own claims alone).200with{ id, email, name, role }on success.
Account creation (admin-only)
POST /admin/users— gated byfastify.authenticate+requireRole('ADMIN'). Body{ email, name, role, password }(roleone ofADMIN/AGENT).403for a valid non-admin session.409ifemailis already in use.201with the created{ id, email, name, role }(never the password or its hash) on success.
Logout
POST /auth/logout— gated byfastify.authenticate. Revokes the calling token's ownjti(Redis denylist, TTL = remaining lifetime) so it's rejected on any further use even before its natural expiry.200on success.
Guarantees (callable contract)
- Every route already gated by
fastify.authenticateacross 002-009 continues to accept a valid session and now genuinely rejects a missing/invalid/expired/revoked one — the gate itself changes from a no-op to a real check; which routes carry the gate is unchanged (FR-006, SC-001). - A route additionally gated by
requireRole('ADMIN')rejects a structurally valid session whose role isn'tADMIN, with a response distinguishable from "no valid session at all" (403 vs 401) (FR-005, SC-002). - A login failure never reveals whether the submitted email corresponds to an existing account — verified by comparing the exact response for a wrong password against a wholly nonexistent email (FR-002, SC-003).
- No password is ever stored, logged, or returned anywhere in plaintext — only
passwordHashis persisted, and no response body (login, self-identity, account creation) ever includes it (FR-003, SC-004). - An admin-created account can log in immediately with the password it was created with, no manual step in between (FR-008, SC-005).
- A token revoked via logout is rejected on any further use, even before its natural expiry (FR-009).