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>
6.9 KiB
description
| description |
|---|
| Task list for 013-auth-hardening |
Tasks: Authentication Hardening
Input: Design documents from specs/013-auth-hardening/
Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/auth-hardening-contract.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(default10),PASSWORD_RESET_TOKEN_LIFETIME_MINUTES(default30),LOGIN_RATE_LIMIT_MAX_ATTEMPTS(default5), andLOGIN_RATE_LIMIT_WINDOW_SECONDS(default300) tosrc/config/env.ts, exposed viasrc/config/auth.ts's existingauthConfigobject
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) intests/unit/identity/password-policy.test.ts
Implementation for User Story 2
- T003 [US2] Add
identity/auth/mapper/password-policy.ts'svalidatePasswordStrength(password): void, throwingValidationError(depends on T001) - T004 [US2] Export it from
identity/auth's publicindex.ts(depends on T003) - T005 [US2] Call it from
identity/agents/service/users.service.ts'sUsersService.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/userstests 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)(returnsuserIdor 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/requestandPOST /auth/password-reset/consume(both ungated — no session exists yet) inidentity/auth/controller/+routes/+schema/, registered fromsrc/api/routes.ts(already registersauthRoutesas 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/verifyPasswordinAuthService.login(a fake repo/mapper that would throw if called after an already-exceeded limit) intests/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 existingcheckRateLimit(login:${email}, authConfig.loginRateLimitMaxAttempts, authConfig.loginRateLimitWindowSeconds)(from@/infrastructure/cache) as the very first step, throwingRateLimitErrorif 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.mdNotes with any implementation-time findings - T019 Run
npx tsx scripts/check-architecture.tsandnpm run lint/npm run typecheck - T020 Full regression:
npm run test:unitthen 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