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>
49 lines
2.5 KiB
Markdown
49 lines
2.5 KiB
Markdown
# Contract: Identity and Authentication
|
|
|
|
## Login
|
|
|
|
- `POST /auth/login` — body `{ email, password }`. `401` on 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." `200` with
|
|
`{ token, user: { id, email, name, role } }` on success.
|
|
|
|
## Self-identity
|
|
|
|
- `GET /auth/me` — gated by `fastify.authenticate`. `401` if the token is missing/invalid/
|
|
expired/revoked. `401` if 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). `200`
|
|
with `{ id, email, name, role }` on success.
|
|
|
|
## Account creation (admin-only)
|
|
|
|
- `POST /admin/users` — gated by `fastify.authenticate` + `requireRole('ADMIN')`. Body
|
|
`{ email, name, role, password }` (`role` one of `ADMIN`/`AGENT`). `403` for a valid non-admin
|
|
session. `409` if `email` is already in use. `201` with the created `{ id, email, name, role
|
|
}` (never the password or its hash) on success.
|
|
|
|
## Logout
|
|
|
|
- `POST /auth/logout` — gated by `fastify.authenticate`. Revokes the calling token's own `jti`
|
|
(Redis denylist, TTL = remaining lifetime) so it's rejected on any further use even before its
|
|
natural expiry. `200` on success.
|
|
|
|
## Guarantees (callable contract)
|
|
|
|
1. **Every route already gated by `fastify.authenticate` across 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).
|
|
2. **A route additionally gated by `requireRole('ADMIN')` rejects a structurally valid session
|
|
whose role isn't `ADMIN`, with a response distinguishable from "no valid session at all"**
|
|
(403 vs 401) (FR-005, SC-002).
|
|
3. **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).
|
|
4. **No password is ever stored, logged, or returned anywhere in plaintext** — only
|
|
`passwordHash` is persisted, and no response body (login, self-identity, account creation)
|
|
ever includes it (FR-003, SC-004).
|
|
5. **An admin-created account can log in immediately with the password it was created with, no
|
|
manual step in between** (FR-008, SC-005).
|
|
6. **A token revoked via logout is rejected on any further use, even before its natural expiry**
|
|
(FR-009).
|