Closes the two gaps 010-identity-auth explicitly deferred (password reset, login rate-limiting), plus a shared password-strength validator both the reset-consume endpoint and admin account creation now depend on. - Password reset: single-use, paired-Redis-key tokens (never in Postgres), identical response regardless of account existence, stubbed delivery via a structured log line (no email infrastructure exists yet). - Password strength: one validatePasswordStrength() call site, wired into both POST /admin/users and the reset-consume flow. - Login rate-limiting: checkRateLimit keyed by submitted email, checked before any credential verification. Also fixes tests/helpers/auth.ts's shared loginAs() helper, which reused two fixed accounts across the whole integration suite via upsert — now rate-limited per email, that collided across ~30 files sharing one budget. Each call now gets a unique email; no call sites needed to change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
152 lines
6.9 KiB
Markdown
152 lines
6.9 KiB
Markdown
---
|
|
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)
|
|
|
|
- [x] 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
|
|
|
|
- [x] 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
|
|
|
|
- [x] T003 [US2] Add `identity/auth/mapper/password-policy.ts`'s
|
|
`validatePasswordStrength(password): void`, throwing `ValidationError` (depends on T001)
|
|
- [x] T004 [US2] Export it from `identity/auth`'s public `index.ts` (depends on T003)
|
|
- [x] T005 [US2] Call it from `identity/agents/service/users.service.ts`'s `UsersService.create`,
|
|
before hashing (depends on T004)
|
|
- [x] 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
|
|
|
|
- [x] 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
|
|
|
|
- [x] T008 [US1] Add `identity/auth/mapper/reset-token.ts` — `generateResetToken()` (raw token +
|
|
its SHA-256 hash) (depends on T001)
|
|
- [x] 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)
|
|
- [x] 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)
|
|
- [x] 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)
|
|
- [x] 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)
|
|
- [x] 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
|
|
|
|
- [x] 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`
|
|
- [x] 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
|
|
|
|
- [x] 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)
|
|
- [x] 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
|
|
|
|
- [x] T018 [P] Update `specs/013-auth-hardening/checklists/requirements.md` Notes with any
|
|
implementation-time findings
|
|
- [x] T019 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck`
|
|
- [x] 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
|