diff --git a/specs/013-auth-hardening/tasks.md b/specs/013-auth-hardening/tasks.md new file mode 100644 index 0000000..073b3b0 --- /dev/null +++ b/specs/013-auth-hardening/tasks.md @@ -0,0 +1,151 @@ +--- +description: "Task list for 013-auth-hardening" +--- + +# Tasks: Authentication Hardening + +**Input**: Design documents from `specs/013-auth-hardening/` + +**Prerequisites**: [plan.md](./plan.md), [spec.md](./spec.md), [research.md](./research.md), +[data-model.md](./data-model.md), +[contracts/auth-hardening-contract.md](./contracts/auth-hardening-contract.md), +[quickstart.md](./quickstart.md) + +**Organization**: Tasks are grouped by user story (US1 = P1 password reset, US2 = P1 password +policy, US3 = P1 login rate-limiting). US2 is a dependency US1's own consume endpoint needs, so +build it first despite the nominal priority tie; US3 is independent of both. + +## Format: `[ID] [P?] [Story] Description` + +All file paths are relative to `supporthub-api/` (repo root). + +--- + +## Phase 1: Foundational (Blocking Prerequisites) + +- [ ] T001 Add `PASSWORD_MIN_LENGTH` (default `10`), + `PASSWORD_RESET_TOKEN_LIFETIME_MINUTES` (default `30`), + `LOGIN_RATE_LIMIT_MAX_ATTEMPTS` (default `5`), and `LOGIN_RATE_LIMIT_WINDOW_SECONDS` + (default `300`) to `src/config/env.ts`, exposed via `src/config/auth.ts`'s existing + `authConfig` object + +**Checkpoint**: Config in place. Both user stories can now be built. + +--- + +## Phase 2: User Story 2 - Password strength is enforced wherever a password is set (Priority: P1) + +**Goal**: One shared validator, called from both the (not-yet-built) reset-consume endpoint and +the existing admin account-creation endpoint. + +**Independent Test**: Quickstart Scenario 2. + +### Tests for User Story 2 + +- [ ] T002 [P] [US2] Unit test for `validatePasswordStrength` (too-short rejected with the + actual minimum named; policy-meeting password passes) in + `tests/unit/identity/password-policy.test.ts` + +### Implementation for User Story 2 + +- [ ] T003 [US2] Add `identity/auth/mapper/password-policy.ts`'s + `validatePasswordStrength(password): void`, throwing `ValidationError` (depends on T001) +- [ ] T004 [US2] Export it from `identity/auth`'s public `index.ts` (depends on T003) +- [ ] T005 [US2] Call it from `identity/agents/service/users.service.ts`'s `UsersService.create`, + before hashing (depends on T004) +- [ ] T006 [US2] Run Quickstart Scenario 2 step 1 locally and confirm it passes; re-run + 010-identity-auth's own existing `POST /admin/users` tests to confirm no regression + +**Checkpoint**: No password shorter than the policy can ever be set via the admin endpoint. + +--- + +## Phase 3: User Story 1 - A user resets a forgotten password (Priority: P1) + +**Goal**: The full request → stub-delivery → consume → login-with-new-password flow. + +**Independent Test**: Quickstart Scenario 1. + +### Tests for User Story 1 + +- [ ] T007 [US1] Integration test covering Quickstart Scenario 1 (request issues a token via + the log stub; a nonexistent email gets an identical response; consume succeeds once and + fails the second time; login works with the new password and fails with the old) in + `tests/integration/password-reset-flow.test.ts` (depends on T006) + +### Implementation for User Story 1 + +- [ ] T008 [US1] Add `identity/auth/mapper/reset-token.ts` — `generateResetToken()` (raw token + + its SHA-256 hash) (depends on T001) +- [ ] T009 [US1] Add `identity/auth/repository/reset-token.repository.ts` — `issue(userId, + tokenHash, ttlSeconds)` (deletes any prior token for this user first, per data-model.md's + paired-key shape), `resolve(tokenHash)` (returns `userId` or null), `consume(tokenHash, + userId)` (deletes both keys) (depends on T008) +- [ ] T010 [US1] Add `AuthService.requestPasswordReset(email)`: always returns the same public + result; internally, if the email resolves to an active account, issues a token and logs + the stub delivery event (structured log, research.md) (depends on T009) +- [ ] T011 [US1] Add `AuthService.resetPassword(token, newPassword)`: validates password + strength first (depends on T004), then resolves/consumes the token, 400s with a specific + reason if the token is missing/expired/used, hashes and stores the new password (depends + on T009, T004) +- [ ] T012 [US1] Add `POST /auth/password-reset/request` and `POST /auth/password-reset/consume` + (both ungated — no session exists yet) in `identity/auth/controller/` + `routes/` + + `schema/`, registered from `src/api/routes.ts` (already registers `authRoutes` as a + whole, so no new registration call needed — depends on T010, T011) +- [ ] T013 [US1] Run Quickstart Scenario 1 locally and confirm all 5 steps pass + +**Checkpoint**: A locked-out user has a real, working self-service fix. + +--- + +## Phase 4: User Story 3 - Login attempts are rate-limited (Priority: P1) + +**Goal**: `POST /auth/login` throttles repeated attempts per submitted email, checked before any +credential verification. + +**Independent Test**: Quickstart Scenario 3. + +### Tests for User Story 3 + +- [ ] T014 [P] [US3] Unit test confirming the rate-limit check is invoked before + `repo.findByEmail`/`verifyPassword` in `AuthService.login` (a fake repo/mapper that would + throw if called after an already-exceeded limit) in + `tests/unit/identity/login-rate-limit-ordering.test.ts` +- [ ] T015 [US3] Integration test covering Quickstart Scenario 3 (N attempts get 401, the N+1th + — even with the correct password — gets 429, a different email is unaffected) in + `tests/integration/login-rate-limit.test.ts` (depends on T001) + +### Implementation for User Story 3 + +- [ ] T016 [US3] In `AuthService.login`, call the existing + `checkRateLimit(`login:${email}`, authConfig.loginRateLimitMaxAttempts, + authConfig.loginRateLimitWindowSeconds)` (from `@/infrastructure/cache`) as the very first + step, throwing `RateLimitError` if exceeded (depends on T001) +- [ ] T017 [US3] Run Quickstart Scenario 3 locally and confirm all 3 steps pass + +**Checkpoint**: All three user stories work independently and together — this feature's full +scope. + +--- + +## Phase 5: Polish & Cross-Cutting Concerns + +- [ ] T018 [P] Update `specs/013-auth-hardening/checklists/requirements.md` Notes with any + implementation-time findings +- [ ] T019 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck` +- [ ] T020 Full regression: `npm run test:unit` then the full integration suite against real + Docker-provisioned Postgres/Redis, confirming nothing outside this feature regressed + (particularly 010-identity-auth's own login/admin-account tests, now touched by this + feature's changes) + +--- + +## Dependencies & Execution Order + +- **Foundational (Phase 1)**: No dependencies — BLOCKS everything +- **User Story 2 (Phase 2)**: Depends on Foundational — BLOCKS User Story 1 (its consume + endpoint needs the shared validator) +- **User Story 1 (Phase 3)**: Depends on User Story 2 +- **User Story 3 (Phase 4)**: Depends only on Foundational — independent of US1/US2, could be + built in parallel with either +- **Polish (Phase 5)**: Depends on all three