36 tasks across 8 phases (5 user stories + setup/foundational/polish).
US1 (real login) and US2 (real route/role gating) are the P1 MVP; the
one task that touches code outside identity/* (T020, adding
requireRole('ADMIN') across 002-009's existing admin routes) is called
out explicitly to run each touched module's own test suite immediately
after, not only in the final regression pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
14 KiB
description
| description |
|---|
| Task list for 010-identity-auth |
Tasks: Identity and Authentication
Input: Design documents from specs/010-identity-auth/
Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/identity-auth-contract.md, quickstart.md
Tests: Included as first-class tasks. Pure logic worth a unit test: the identical-failure-
response behavior (FR-002/SC-003) and the requireRole matching logic. Everything else is
best proven end-to-end against a real Postgres/Redis, including a specific pass re-verifying
existing 002-009 admin routes now actually reject an invalid session.
Organization: Tasks are grouped by user story (US1 = P1 login, US2 = P1 route/role gating, US3 = P2 self-identity, US4 = P2 admin-created accounts, US5 = P3 logout).
Format: [ID] [P?] [Story] Description
All file paths are relative to supporthub-api/ (repo root).
Phase 1: Setup
- T001 [P] Add
jsonwebtokenandbcryptjs(plus@types/jsonwebtoken,@types/bcryptjs) topackage.json - T002 [P] Add
AUTH_JWT_SECRET(required, no default — never a committed secret) andAUTH_TOKEN_LIFETIME_HOURS(z.coerce.number().default(4)) tosrc/config/env.ts, exposed via a newsrc/config/auth.ts(authConfig.jwtSecret,authConfig.tokenLifetimeHours), matchingorchestrationConfig's exact shape - T003 [P] Populate
src/modules/identity/auth/with the full standard shape around its existing files, replacing the email-onlyAuthService.validateCredentials/AuthRepository.findByEmail-only stub content
Phase 2: Foundational (Blocking Prerequisites)
Purpose: Schema for the entities every user story needs.
⚠️ CRITICAL: No user-story stage work can begin until this phase is complete.
- T004 Add
User.passwordHash(String, required) andUser.active(Boolean @default(true)) toprisma/schema.prisma, plusAgent.userId(String? @unique, FK toUser.id— research.md's additive, not-yet-consumed link) (depends on T001-T003) - T005 Run
npm run prisma:generateand create the migration (npm run prisma:migrate) for T004 (depends on T004) - T006 Update
prisma/seed/roles.seed.tsto set a real bcryptjs-hashed password on both seeded accounts (admin@supporthub.internal,agent@supporthub.internal), documenting the plaintext dev password in a comment directly above the hash call (local/dev use only, per spec.md Edge Cases) (depends on T005)
Checkpoint: Schema migrated, demo accounts have real passwords. User stories can now be built.
Phase 3: User Story 1 - An agent or admin logs in and receives a session (Priority: P1) 🎯 MVP (part 1)
Goal: Real password verification and JWT issuance, with an identical failure response regardless of which reason login failed.
Independent Test: Quickstart Scenario 1.
Tests for User Story 1
- T007 [P] [US1] Unit test: given a found user with a matching/non-matching password, and
given no user found at all, the login-failure path produces byte-identical response
shape/status in the non-matching and no-user cases — in
tests/unit/identity/login-failure-parity.test.ts - T008 [US1] Integration test covering Quickstart Scenario 1 (correct login succeeds with a
token + identity; wrong password and nonexistent email produce the same
401) against a real Postgres intests/integration/identity-auth-flow.test.ts(depends on T006)
Implementation for User Story 1
- T009 [US1] Add
hashPassword/verifyPassword(bcryptjs) andsignToken/verifyToken(jsonwebtoken, embeddingsub/email/role/actorType/jti/iat/expper data-model.md) inidentity/auth/mapper/(depends on T002) - T010 [US1] Add
AuthRepository.findActiveByEmail(replacingfindByEmail) inidentity/auth/repository/(depends on T005) - T011 [US1] Add
AuthService.login(email, password): looks up the user, compares against either the found hash or a fixed dummy hash when not found (FR-002's timing/shape parity), returns{ token, user }or throws a single, identicalAuthenticationErrorfor every failure branch — inidentity/auth/service/(depends on T009, T010) - T012 [US1] Replace
POST /auth/login's schema (email+password, replacing the email-only schema) and controller inidentity/auth/schema/+controller/, registered fromsrc/api/routes.ts(depends on T011) - T013 [US1] Run Quickstart Scenario 1 locally and confirm all 3 steps pass
Checkpoint: Login works and never leaks account existence through its failure response.
Phase 4: User Story 2 - Protected routes require a valid session; admin-only routes require the admin role (Priority: P1) 🎯 MVP (part 2)
Goal: fastify.authenticate actually verifies; requireRole enforces role on top of it;
every existing gated route is re-verified.
Independent Test: Quickstart Scenario 2.
Tests for User Story 2
- T014 [P] [US2] Unit test for
requireRole's matching logic (allowed role passes, wrong role throwsAuthorizationError, norequest.userat all throws) intests/unit/identity/require-role.test.ts - T015 [US2] Integration test covering Quickstart Scenario 2 (no header, malformed token,
wrong-role token, correct-role token) against a real Postgres/Redis in
tests/integration/identity-auth-flow.test.ts(depends on T008) - T016 [US2] Integration test spot-checking at least one existing admin route per module
(002's product-integration admin route, 004's knowledge admin route, 006's team-creation
route, 007's manual-assignment route, 008's SLA-policy route, 009's investigation route)
now rejects a missing/invalid session — in
tests/integration/identity-auth-flow.test.ts(depends on T015)
Implementation for User Story 2
- T017 [US2] Add revocation-denylist helpers (
isTokenRevoked,revokeToken) insrc/infrastructure/cache/, alongside the existinghasSeenJti/markJtiSeen(same Redis-key-with-TTL shape, research.md) (depends on T002) - T018 [US2] Replace
auth.plugin.ts'sauthenticatestub: verify the JWT signature and expiry, check T017's revocation denylist, and on success setrequest.user(the fullAuthUser) andrequest.reqContext.actorId/actorType— throwAuthenticationErroron any failure, never pass through as anonymous (depends on T009, T017) - T019 [US2] Add
requireRole(...allowedRoles: string[])preHandler factory (checksrequest.user?.role, throwsAuthorizationErrorif it doesn't match) inidentity/auth/service/(or a dedicatedidentity/auth/guards/file), exported fromidentity/auth's publicindex.ts(depends on T018) - T020 [US2] Add
requireRole('ADMIN')to every existing write/config admin route across 002-009 that doesn't already distinguish agent-vs-admin access (product-integration admin, knowledge admin, teams/hierarchy admin, SLA/escalation-policy admin) — read-only routes and ticket-working routes an agent legitimately uses stayfastify.authenticate- only (research.md's own scoping: this is a mechanical pass applying an existing judgment, not a new design) (depends on T019) - T021 [US2] Run Quickstart Scenario 2 locally and confirm all 4 steps pass
Checkpoint: Every P1 user story is complete — a session is real, and it's actually checked everywhere it's supposed to be. This is the feature's MVP.
Phase 5: User Story 3 - An authenticated user can identify themselves (Priority: P2)
Goal: A self-identity endpoint that re-validates against current account state.
Independent Test: Quickstart Scenario 3.
Tests for User Story 3
- T022 [US3] Integration test covering Quickstart Scenario 3 (identity matches login;
deactivating the account rejects a still-unexpired token's use of this endpoint
specifically) — in
tests/integration/identity-auth-flow.test.ts(depends on T021)
Implementation for User Story 3
- T023 [US3] Add
AuthService.getCurrentUser(userId): re-fetches theUserrow, throwsAuthenticationErrorif it no longer exists oractive: false— inidentity/auth/ service/(depends on T010) - T024 [US3] Add
GET /auth/meroute (gated byfastify.authenticate) inidentity/auth/ controller/+routes/(depends on T023) - T025 [US3] Run Quickstart Scenario 3 locally and confirm both steps pass
Checkpoint: A session can be introspected and is re-validated against live account state.
Phase 6: User Story 4 - An admin creates additional agent/admin accounts (Priority: P2)
Goal: Admin-only account creation, immediately usable to log in.
Independent Test: Quickstart Scenario 4.
Tests for User Story 4
- T026 [US4] Integration test covering Quickstart Scenario 4 (admin creates an account and
it logs in immediately; non-admin rejected; duplicate email rejected) — in
tests/integration/identity-auth-flow.test.ts(depends on T021)
Implementation for User Story 4
- T027 [US4] Add
UsersService.create(email, name, role, password)(resolve-or-409 on duplicate email, hashes the password via T009) inidentity/agents/service/(research.md — account creation lives alongsideidentity/agents's own roster CRUD, notidentity/auth) (depends on T009) - T028 [US4] Add
POST /admin/usersroute (gated byfastify.authenticate+requireRole('ADMIN')) inidentity/agents/controller/+routes/, registered fromsrc/api/routes.ts— response never includes the password or hash (depends on T019, T027) - T029 [US4] Run Quickstart Scenario 4 locally and confirm all 4 steps pass
Checkpoint: New staff accounts can be provisioned without a manual database write.
Phase 7: User Story 5 - A user logs out (Priority: P3)
Goal: Explicit, immediate session revocation.
Independent Test: Quickstart Scenario 5.
Tests for User Story 5
- T030 [US5] Integration test covering Quickstart Scenario 5 (logout succeeds; the same
token is rejected immediately afterward) — in
tests/integration/identity-auth-flow.test.ts(depends on T021)
Implementation for User Story 5
- T031 [US5] Add
AuthService.logout(jti, remainingTtlSeconds): calls T017'srevokeToken— inidentity/auth/service/(depends on T017) - T032 [US5] Add
POST /auth/logoutroute (gated byfastify.authenticate) inidentity/auth/controller/+routes/(depends on T031) - T033 [US5] Run Quickstart Scenario 5 locally and confirm both steps pass
Checkpoint: All five user stories work independently and together — real login, real gating, self-identity, admin-provisioned accounts, and logout form one coherent auth system.
Phase 8: Polish & Cross-Cutting Concerns
- T034 [P] Update
specs/010-identity-auth/checklists/requirements.mdNotes with any implementation-time findings - T035 Run
npx tsx scripts/check-architecture.tsandnpm run lint/npm run typecheck - T036 Full regression:
npm run test:unit(scoped totests/unit) to confirm nothing broke elsewhere, then the full integration suite (including 002-009's own suites, since T020 addsrequireRoleto their existing routes) against real Docker-provisioned Postgres/Redis
Dependencies & Execution Order
Phase Dependencies
- Setup (Phase 1): No dependencies
- Foundational (Phase 2): Depends on Setup — BLOCKS all user stories
- User Story 1 (Phase 3): Depends on Foundational — no dependency on US2-US5
- User Story 2 (Phase 4): Depends on US1 (a real token to verify)
- User Story 3 (Phase 5): Depends on US2 (the gate US3's own route sits behind)
- User Story 4 (Phase 6): Depends on US2 (
requireRole('ADMIN')) - User Story 5 (Phase 7): Depends on US2 (the gate logout's own route sits behind) and
US1's token shape (
jti) - Polish (Phase 8): Depends on all five user stories
Parallel Opportunities
- T001-T003 (independent scaffolding)
- T007 (unit test) alongside T009-T011 (the implementation it tests)
- T014 (unit test) alongside T019 (the implementation it tests)
- T034 in Polish
Sequencing Note
T020 (adding requireRole('ADMIN') across 002-009's existing routes) is the one task in this
feature that touches code outside identity/* — run each touched module's own existing test
suite immediately after, not only in T036's final regression pass, so a role-gating regression
in, say, 007's own suite is caught close to its cause rather than at the very end.
Implementation Strategy
MVP First (User Stories 1-2 Only)
- Setup + Foundational (T001-T006)
- User Story 1 (T007-T013) → login works, no account-existence leak
- User Story 2 (T014-T021) → the gate is real everywhere it already existed
- STOP and VALIDATE: Quickstart Scenarios 1-2 pass, including the cross-module spot-check (T016). This is the feature's MVP — every other user story is a smaller addition on top of a now-real auth system.
Incremental Delivery
- Setup + Foundational → schema migrated, demo accounts have real passwords
- Add User Story 1 → login is real
- Add User Story 2 → the gate is real everywhere (P1-complete, MVP)
- Add User Story 3 → self-identity, re-validated against live account state
- Add User Story 4 → admins can provision new accounts
- Add User Story 5 → explicit logout
- Polish → full regression across every feature this touches