Files
support_backend/specs/013-auth-hardening/tasks.md
T
saqib mirandClaude Sonnet 5 79bc2ef25b feat(013-auth-hardening): password reset, password strength policy, login rate-limiting
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>
2026-09-07 21:22:49 +05:30

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