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>
8.8 KiB
Implementation Plan: Identity and Authentication
Branch: 010-identity-auth | Date: 2026-09-07 | Spec: spec.md
Input: Feature specification from specs/010-identity-auth/spec.md
Summary
Finishes the original, never-wired scaffold: identity/auth's email-only login stub becomes a
real bcryptjs-verified, JWT-issuing login; fastify.authenticate (currently a complete no-op)
becomes a real signature/expiry/revocation check populating request.user and the already-
shared request.reqContext.actorId/actorType fields every module since 007 already reads; a
new requireRole(...roles) preHandler factory adds role-based gating on top. User gains
passwordHash and active columns. Logout revokes a token's jti via the same Redis-denylist
shape 002's replay protection already established.
Technical Context
Language/Version: TypeScript 5.4 / Node.js 20+.
Primary Dependencies: jsonwebtoken (new — JWT sign/verify), bcryptjs (new — password
hashing, pure JS to avoid native-build friction on Windows dev environments). Reuses existing
ioredis for the revocation denylist.
Storage: PostgreSQL via Prisma (User.passwordHash, User.active). Redis for the
revocation denylist (auth:revoked:<jti>, TTL = remaining token lifetime) — same shape as
002's hasSeenJti/markJtiSeen.
Testing: Vitest — unit tests for password verification's identical-response-on-failure
behavior and the requireRole preHandler's role-matching logic; integration tests against real
Postgres/Redis for the full login → gated-route → logout flow, and specifically re-verifying at
least one already-shipped admin route per module (002-009) now genuinely rejects an invalid
session.
Target Platform: Same Fastify modular monolith. Modifies src/plugins/auth.plugin.ts,
populates src/modules/identity/auth/, adds POST /admin/users (a new small surface, placed
alongside identity/agents's own admin routes since account management is an identity concern,
not identity/auth's own — identity/auth owns login/logout/self-identity, not account CRUD).
Project Type: Backend service — single project.
Performance Goals: Token verification (signature + expiry + Redis denylist check) must stay a single Redis round trip, not a Postgres query, on every gated request — only the self-identity endpoint (User Story 3) re-fetches from Postgres, by design (research.md).
Constraints: MUST NOT reveal account existence via login's failure response (FR-002); MUST NOT ever store or return a plaintext password (FR-003); MUST NOT change which existing routes are gated, only make the gate real (FR-006); MUST re-validate against current account state on the self-identity endpoint specifically, not on every request (data-model.md).
Scale/Scope: One modified plugin (auth.plugin.ts), one populated module
(identity/auth), one new small admin-account-creation surface, two new dependencies, two new
User columns, one seed-script update. Explicitly excludes: password reset, MFA, login-specific
rate-limiting, and retroactively adding requireRole('ADMIN') to every existing admin route
beyond a representative sample (research.md — tracked as this feature's own Polish-phase
mechanical task, not a redesign of any other feature's access model).
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
| Principle / Section | Check | Result |
|---|---|---|
| I. SaaS Is the Sole Identity & Access Authority | This feature authenticates SupportHub's own staff (User/Agent), explicitly never CUSTOMER-role accounts (research.md/spec.md Assumptions) — customer identity remains exclusively SaaS-delegated via 002's own trust boundary, untouched by this feature. Matches the constitution's own carve-out: "SupportHub is the sole authority only for its own domain: ... support org structure." |
PASS |
| II. Configuration Over Hardcoding | Token lifetime and any future role list are read from a config value (research.md's 4-hour default), never a magic number duplicated at each call site. | PASS |
| III. Layered Architecture With Enforced Module Boundaries | identity/auth keeps its standard shape; requireRole is exported from identity/auth's own public index.ts for other modules' routes to compose with, the same way fastify.authenticate itself is already a cross-cutting plugin-level primitive, not a module import. |
PASS |
| IV. AI Recommends, Deterministic Policy Decides | Not applicable — no AI involvement in this feature. | PASS — N/A |
| V. Evidence-Based Verification | Not applicable — no resolution/verification concept in this feature. | PASS — N/A |
| VI. Durable Audit & History | This feature is what finally makes 007-009's own audit fields (AssignmentHistory.actor, EscalationEvent.triggeredBy, etc.) accurate for real agent/admin actions instead of always falling back to 'unknown' (research.md) — directly strengthens, not just satisfies, this principle. |
PASS |
| VII. Concurrency-Safe, Durable Job Handling | Token verification and revocation are stateless/Redis-TTL-based, not an in-memory timer; two concurrent login attempts for the same account are independently evaluated with no shared mutable state (spec.md Edge Cases). | PASS |
| VIII. Problem and Ticket Are Separate, Related Entities | Not applicable — this feature doesn't touch tickets or problems. | PASS — N/A |
| Technology & Platform Constraints | Two new, narrowly-scoped dependencies (jsonwebtoken, bcryptjs), both justified in research.md; reuses existing Redis infrastructure, no new infrastructure category introduced. |
PASS |
No violations requiring Complexity Tracking justification.
Post-Design Constitution Re-check
All gates above remain PASS after Phase 1 design. Principle VI is worth restating post-design:
this feature has no user-facing "audit" screen of its own, but its real effect is retroactively
correcting the audit trail of every feature since 007 that could only ever record 'unknown'
as the acting agent/admin — a materially more accurate audit history the moment this ships.
Project Structure
Documentation (this feature)
specs/010-identity-auth/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── contracts/ # Phase 1 output
└── tasks.md # Phase 2 output (/speckit-tasks — not created here)
Source Code (repository root)
supporthub-api/
├── prisma/
│ ├── schema.prisma # MODIFIED — User.passwordHash, User.active
│ └── seed/roles.seed.ts # MODIFIED — seeded accounts get real password hashes
├── src/
│ ├── plugins/
│ │ └── auth.plugin.ts # REPLACED stub — real JWT verify + revocation
│ │ check, populates request.user + reqContext
│ ├── infrastructure/
│ │ └── cache/ # MODIFIED — revocation denylist helpers
│ │ alongside the existing jti-replay helpers
│ └── modules/
│ └── identity/
│ ├── auth/ # REPLACED stub — full real login/logout/
│ │ ├── controller/ routes/ schema/ self-identity, requireRole exported from
│ │ │ repository/ service/ types/ its own public index.ts
│ │ │ mapper/ constants/ index.ts
│ │ └── (no engine/ — no real decision logic beyond password/token checks)
│ └── agents/ # MODIFIED — new POST /admin/users route
│ └── (existing module, account-creation surface added alongside its own
│ existing agent-roster admin routes)
└── tests/
├── unit/identity/ # password-failure-response-parity,
│ requireRole matching logic
└── integration/ # full login/gating/logout flow, spot-checks
across 002-009's own existing admin routes
Structure Decision: Single project. POST /admin/users (account creation) lives under
identity/agents rather than identity/auth, since identity/auth owns authentication
mechanics (login/logout/self-identity) while account/roster management is already that
module's own established concern — mirrors 006's own precedent of identity/agents owning
agent-roster CRUD.
Complexity Tracking
No constitution violations — table intentionally omitted.