Files
support_backend/specs/010-identity-auth/plan.md
T
saqib mirandClaude Sonnet 5 3b4c220a45 plan: design for identity and authentication feature (010)
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>
2026-09-07 11:14:42 +05:30

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.