Skip to main content

EP-001 — Authentication & Access Control RTM (Onboarding Subset)

EPIC: EP-001 Auth & Access Scope: This is a PARTIAL RTM, covering only the EP-001 stories that participate in the Tenant + Property Onboarding business flow (BF-001). The remaining EP-001 stories will be reconciled in a separate pilot for the auth business flow. Reconciled: 2026-05-24

In-scope stories​

StoryWhy in scope
US-001-001 LoginTenant admin must log in immediately after signup
US-001-004 MFA Setup (TOTP)ADMIN/SUPER_ADMIN forced to enroll MFA per platform policy (currently kill-switched OFF)
US-001-005 MFA Verification on LoginSame; MFA gate on first admin login
US-001-006 RBACTenant admin's role bootstrap
US-001-007 Password Reset via EmailFirst-time admin sets password via reset link
US-001-010 Brute-Force LockoutOnboarding form must be hardened against signup-flood
US-001-011 User CRUD (Invite/Activate/Deactivate)Admin invites first staff users post-onboarding

Status legend​

StatusMeaning
BuiltCode exists + tests exist + tests pass
Built-UntestedCode exists, no automated test coverage
PartialCode exists for some acceptance criteria, gaps for others
StubCode is a placeholder / TODO / returns mock data
Not StartedNo code

Summary​

MetricCount
In-scope stories7
Catalog claims "Built"6 (US-001-001/004/005/006/007/010); US-001-011 = Partial
Verified-Built4 (US-001-001, US-001-005, US-001-007, US-001-011)
Built-Untested0
Partial3 (US-001-004, US-001-006, US-001-010)
Stub0
Not Started0
Drift findings11 (D-001..D-011; 2 P0 / 8 P1 / 1 P2)
[SME-PROPOSED] refinements20

US-001-001 — User Login with Email/Password​

Catalog status: Built (P0, 3 points) Verified status: Built

Code locations:

  • hmp-auth-service/src/auth/auth.service.ts — login() (Redis-backed brute force, audit log on login/logout per EP-001 implementation report)
  • hmp-auth-service/src/auth/auth.controller.ts:58 — @Public() @Post('login')
  • hmp-auth-service/src/auth/dto/login.dto.ts — LoginDto (email, password, optional mfaCode)
  • hmp-auth-service/CLAUDE.md — auth-service flow doc (login steps, MFA gate at JwtAuthGuard, kill-switch via MFA_ENFORCE_ADMIN=false)
  • hmp-shared-packages/packages/auth/src/guards/jwt-auth.guard.ts — JwtAuthGuard.handleRequest (per MEMORY 2026-05-20: throws MFA_REQUIRED_FOR_ADMIN for ADMIN/SUPER_ADMIN without mfaEnabled=true; honours MFA_ENFORCE_ADMIN='false' kill-switch via strict equality — @hmp/auth@1.6.1)
  • hmp-web-dashboard/src/app/(auth)/login/page.tsx — re-exports ./keynova login page
  • hmp-web-dashboard/src/app/(auth)/login/keynova/ — KeyNova-branded login UI
  • hmp-web-dashboard/src/lib/auth.ts — NextAuth authorize() (parses /auth/me 401 body; catches MFA_REQUIRED_FOR_ADMIN → throws structured MfaSetupRequired:<email> per MEMORY 2026-05-20)
  • hmp-admin-portal/src/lib/api/auth.ts — admin-portal login client (per uc-tenant-self-onboarding.yaml)

Unit + integration tests:

  • hmp-auth-service/src/auth/auth.service.spec.ts — 7 login tests (valid, invalid, inactive, MFA, etc.)
  • hmp-auth-service/src/auth/auth.controller.spec.ts — controller delegation tests
  • hmp-auth-service/src/auth/auth.sprint1-fixes.spec.ts — Sprint 1 fix coverage (Redis tracking, audit log)
  • hmp-auth-service/src/auth/auth.sprint1-gaps.spec.ts — Sprint 1 gap closure
  • hmp-shared-packages/packages/auth/src/guards/jwt-auth.guard.spec.ts — 40/40 guard tests incl. 10 new MFA-enforcement cases + kill-switch behaviour (@hmp/auth@1.6.1 per MEMORY 2026-05-20)

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/flow-1-tenant-onboarding.spec.ts :: "S2 — admin login after signup" — 5/10 overall (S2 in PASS subset; S2a/S3/S4 fail on stale read model) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)
  • hmp-infrastructure/testing/flow-tests/src/flows/admin-onboarding.flow.spec.ts :: "admin login + property create" — TBD (not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes:

  • hmp-web-dashboard/e2e/real-backend/smoke-login-home.real.spec.ts — auth landing

E2E (Playwright):

  • hmp-web-dashboard/e2e/sprint1-auth.spec.ts — login form render + interaction
  • hmp-web-dashboard/e2e/tests/02-auth-flows.spec.ts — login/logout/role-based
  • hmp-web-dashboard/e2e/persona-admin.spec.ts — admin persona login coverage
  • hmp-web-dashboard/e2e/persona-superadmin.spec.ts — superadmin persona login coverage

Use cases referencing this story:

  • none direct (referenced indirectly via uc-tenant-self-onboarding.yaml AP-T01-004 — admin-portal signup → login handoff)

SME notes:

  • The story body has 4 documented scenarios (success / invalid creds 401 / deactivated 403 / missing tenant 404) but production behaviour now includes at least 4 more 401 sub-codes that are not in the story: MFA_REQUIRED_FOR_ADMIN, EMAIL_VERIFICATION_REQUIRED, ACCOUNT_LOCKED, TENANT_SUSPENDED. Each surfaces a different dashboard banner via MfaSetupRequired:<email>-style cascade. The story should enumerate them so QA can write negative-path tests against a fixed contract rather than reading code.
  • Audit-log behaviour (fire-and-forget per implementation report) is not in the story body — should be added as a non-functional AC.

Drift findings:

D-001 — [P1] [Test-vs-code] flow-1-tenant-onboarding.spec.ts S2a/S3/S4 fail (5/10 PASS overall) because the property list read model is stale after TENANT_CREATED Kafka propagation. The race is between POST /api/properties and read-model-sync.service.ts consuming the event. The login itself (S2) passes, but the next step the user takes (list properties) fails — so US-001-001 is technically "Built" but the BF-001 happy path is brittle. Fix: either (a) read-after-write consistency hint via response header (x-read-consistency: eventual plus polling helper) or (b) synchronous read-model insert on the same DB connection.

  • [SME-PROPOSED] Add AC: "after successful login, GET /api/properties returns the tenant's properties within 500 ms p95 even immediately after a property POST" — surfaces the read-model staleness as a testable contract.

D-002 — [P1] [Story-vs-code] Story body acceptance criteria do not enumerate the 4 additional 401 sub-codes that the front-end already handles (MFA_REQUIRED_FOR_ADMIN, EMAIL_VERIFICATION_REQUIRED, ACCOUNT_LOCKED, TENANT_SUSPENDED). Each is a hard-coded contract between auth-service and the NextAuth authorize() parser — undocumented contract = silent break risk.

  • [SME-PROPOSED] Refine US-001-001 to list every 4xx sub-code returned with a machine-readable code field, and write one negative-path spec per code.

US-001-004 — MFA Setup (TOTP)​

Catalog status: Built (P0, 5 points) Verified status: Partial — backend Built, frontend Partial, operationally OFF in all 4 envs via kill-switch

Code locations:

  • hmp-auth-service/src/auth/auth.service.ts — setupMfa(), verifyMfa(), disableMfa() (backup codes added per implementation report — mfaBackupCodes, mfaFailedAttempts, mfaLockedUntil columns, bcrypt-hashed, 10 codes)
  • hmp-auth-service/src/auth/auth.controller.ts:164 — @MfaExempt() @Post('mfa/setup')
  • hmp-auth-service/src/auth/auth.controller.ts:179 — @MfaExempt() @Post('mfa/verify')
  • hmp-auth-service/src/auth/auth.controller.ts:218 — @MfaExempt() @Post('mfa/disable')
  • hmp-auth-service/src/auth/dto/login.dto.ts — MfaSetupDto, MfaVerifyDto
  • hmp-web-dashboard/src/app/(dashboard)/settings/security/mfa/page.tsx — QR + verify UI (uses qrcode npm)
  • hmp-shared-packages/packages/auth/src/decorators/mfa-exempt.decorator.ts [CITED-BUT-NOT-FOUND] — @MfaExempt() decorator introduced in @hmp/auth@1.6.0 (per MEMORY 2026-05-20 — opt-out from MFA enforcement guard)

Unit + integration tests:

  • hmp-auth-service/src/auth/auth.service.spec.ts — 5 verifyMfa tests; setup/disable updated to include backup-code mocks per implementation report
  • hmp-auth-service/src/auth/auth.sprint1-fixes.spec.ts — sprint 1 MFA coverage

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/mfa-totp.flow.spec.ts :: "full setup → verify → two-step → disable cycle" — SKIPPED in test/qa (SKIP_MFA_FLOW=true) because MFA enforcement is kill-switched OFF in all 4 envs (per MEMORY) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes:

  • none

E2E (Playwright):

  • none specific (MFA enrollment UI not yet covered in dashboard e2e — surface for Task 6 SME)

Use cases referencing this story:

  • none direct

SME notes:

  • Today's reality: MFA is shipped but kill-switched OFF in test/qa/staging/prod (MFA_ENFORCE_ADMIN=false). The story body says "MFA Setup is mandatory for ADMIN/SUPER_ADMIN" but operationally it is optional everywhere. This is the single largest disconnect between story and reality.
  • DPDPA/CERT-In context: the platform's CERT-In undertaking commits to MFA for privileged accounts. Running with the kill-switch ON in prod is a known compliance exception pending SMS/email MFA delivery channels.
  • Backup codes are bcrypt-hashed per implementation report — story body should document the backup-code count (10), regeneration semantics (do prior codes invalidate? story doesn't say), and what happens when all 10 are used (lockout? regen prompt?).

Drift findings:

D-003 — [P0] [Story-vs-reality] Story body says MFA setup is mandatory for ADMIN/SUPER_ADMIN, but the MFA_ENFORCE_ADMIN=false kill-switch is ON in all 4 envs incl. production (per MEMORY 2026-05-20). The acceptance criteria do not acknowledge the kill switch as a documented exception. A new operator reading the story would expect a hard gate that does not exist.

  • [SME-PROPOSED] Refine US-001-004 to explicitly call out the kill-switch as a platform-level operational toggle and to document the revert condition ("revert when SMS-email MFA ships" per MEMORY marker REVERT-WHEN-SMS-EMAIL-MFA-SHIPS).
  • [SME-PROPOSED] Add AC: "When MFA_ENFORCE_ADMIN=true is set, an ADMIN without mfaEnabled=true is bounced to /auth/mfa/setup on every non-@MfaExempt() API call." Tested via jwt-auth.guard.spec.ts kill-switch cases.

D-004 — [P1] [Test-coverage] No E2E (Playwright) coverage of the MFA enrollment UI in the dashboard. Flow test exists but is skipped in test/qa. When the kill-switch flips back ON, this surface will be untested end-to-end.

  • [SME-PROPOSED] Add AC: "E2E Playwright test covers MFA QR-scan → 6-digit-verify → backup-code-download flow against a real backend with MFA_ENFORCE_ADMIN=true in a dedicated MFA test profile."

D-005 — [P2] [Story-incomplete] Story body does not specify backup-code regeneration semantics: (a) how many codes (10 today), (b) whether regenerating new codes invalidates old ones, (c) what happens when all 10 are consumed, (d) audit-log row on regeneration. Code does (a) and (b) per implementation report; (c) and (d) are unverified.

  • [SME-PROPOSED] Document backup-code lifecycle in AC list — count, regeneration invalidates priors, exhausted-codes triggers warning banner + force-regenerate.

US-001-005 — MFA Verification on Login​

Catalog status: Built (P0, 3 points) Verified status: Built (review file FAIL verdict is stale — D-006)

Code locations:

  • hmp-auth-service/src/auth/auth.service.ts — validateMfaLogin() (two-step flow + tempToken 5-min TTL + lockout + backup-code fallback per implementation report)
  • hmp-auth-service/src/auth/auth.controller.ts:194 — @Public() @Post('mfa/validate') (NEW per implementation report)
  • hmp-web-dashboard/src/lib/auth.ts — NextAuth authorize() (catches mfaRequired:true/tempToken and propagates to MFA challenge page; cf. MfaSetupRequired:<email> cascade per MEMORY 2026-05-20)
  • hmp-web-dashboard/src/app/(auth)/login/keynova/ — login page handler chain that surfaces MFA challenge banner

Unit + integration tests:

  • hmp-auth-service/src/auth/auth.service.spec.ts — login + MFA combined paths covered (5 verifyMfa + login-with-MFA cases)
  • hmp-auth-service/src/auth/auth.sprint1-fixes.spec.ts — sprint 1 MFA-verification coverage

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/mfa-totp.flow.spec.ts :: "Scenario 2 — two-step login with TOTP" and "Scenario 3 — login with backup code" — SKIPPED in test/qa (SKIP_MFA_FLOW=true) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes:

  • none

E2E (Playwright):

  • none specific (challenge UI not yet covered)

Use cases referencing this story:

  • none direct

SME notes:

  • The two-step flow (/auth/login issues tempToken; /auth/mfa/validate exchanges it for the real JWT) is shipped per implementation report. The reviews/EP-001/US-001-005-review.md verdict FAIL (2/10) pre-dates the fix and is stale.
  • Story body does not document the tempToken 5-minute TTL or its renewal semantics (can it be re-issued from /auth/login if the user takes too long? Today: yes, a new login resets the timer).
  • Story does not document MFA lockout threshold (today: tracked in mfaFailedAttempts + mfaLockedUntil columns; threshold unknown without grepping code — surface as AC).

Drift findings:

D-006 — [P0] [Review-vs-code] reviews/EP-001/US-001-005-review.md says FAIL, 2/10 but the implementation report (2026-03-07) ships the two-step tempToken flow, the /auth/mfa/validate endpoint, MFA lockout, and backup-code fallback. The review file is stale by ~10 weeks. Risk: a new reviewer reading the review-file alone would re-open closed work.

  • [SME-PROPOSED] Re-run the US-001-005 review with the post-implementation-report state and refresh the review verdict; flag the review-file timestamps in RTM-DASHBOARD.md so staleness is visible.

D-007 — [P1] [Story-incomplete] Story does not specify: (a) tempToken TTL (5 min today), (b) MFA-failed-attempt lockout threshold, (c) what happens during the lockout window (does the user see a countdown? does the next valid TOTP still get rejected?), (d) whether the tempToken is single-use or replayable within its TTL.

  • [SME-PROPOSED] Refine US-001-005 to document the 5-min tempToken TTL, single-use semantics, the lockout threshold (verify against code; likely 5–10 attempts based on mfaFailedAttempts column), and the user-visible "try again in N seconds" UX.

US-001-006 — Role-Based Access Control​

Catalog status: Built (P0, 8 points) — catalog title says "9 Roles" Verified status: Partial — backend Built, frontend Partial (sidebar/permission-gate wiring incomplete per review)

Code locations:

  • hmp-shared-packages/packages/auth/src/guards/roles.guard.ts [CITED-BUT-NOT-FOUND] — RolesGuard from @hmp/auth
  • hmp-shared-packages/packages/common/src/decorators/roles.decorator.ts [CITED-BUT-NOT-FOUND] — @Roles() decorator from @hmp/common
  • hmp-auth-service/CLAUDE.md — role enum documented (per source-map drift: 11 roles SUPER_ADMIN through HOSPITAL_STAFF; catalog text says "9 Roles" — cite both)
  • hmp-docs/docs/requirements/03-epic-catalog/01-epic-catalog.md:21 — catalog row "RBAC (9 Roles)"
  • hmp-auth-service/src/roles/roles.controller.ts — custom-roles-per-tenant: POST/GET/PATCH/DELETE /roles (@Roles('SUPER_ADMIN','ADMIN'))
  • hmp-auth-service/src/onboarding/onboarding.controller.ts:50-188 — @Roles('SUPER_ADMIN','ADMIN') with assertTenantScope() (AP-T01-003)
  • hmp-web-dashboard/src/app/(dashboard)/settings/roles/page.tsx [CITED-BUT-NOT-FOUND] — roles & permissions page (referenced by smoke-roles.real.spec.ts)
  • Frontend sidebar/permission-gate wiring — gaps remain per review (review file US-001-006-review.md flagged 7/9 with frontend gaps)

Unit + integration tests:

  • RolesGuard — review file flagged as UNTESTED (GAP-4 in reviews/EP-001/US-001-006-review.md); Task 6 verify if guard tests have shipped since
  • hmp-auth-service/src/auth/auth.service.spec.ts — role assignment coverage as part of createUser/updateUser tests
  • hmp-docs/docs/requirements/use-cases/uc-onboarding-admin-role.yaml companion specs (AP-T01-003 — ADMIN tenant-scope on onboarding controller)
  • Sprint-1 review reviews/EP-001/sprint-1-auth-review.md and custom-roles.flow.spec.ts (P3-11) cover the custom-role surface

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/custom-roles.flow.spec.ts :: "create / list / get / patch / delete custom role" — TBD (P3-11, not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)
  • hmp-infrastructure/testing/flow-tests/src/flows/rbac-integrity.flow.spec.ts :: "RBAC enforcement on backend" — TBD (not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)
  • hmp-infrastructure/testing/flow-tests/src/flows/tenant-isolation.flow.spec.ts :: "cross-tenant denial checks" — TBD (not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes:

  • hmp-web-dashboard/e2e/real-backend/smoke-roles.real.spec.ts — roles & permissions page loads

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/settings-roles.spec.ts — roles CRUD
  • hmp-web-dashboard/e2e/persona-admin.spec.ts — admin persona role coverage
  • hmp-web-dashboard/e2e/persona-superadmin.spec.ts — superadmin persona role coverage

Use cases referencing this story:

  • docs/requirements/use-cases/uc-onboarding-admin-role.yaml (AP-T01-003 — ADMIN tenant-scope check on onboarding controller)

SME notes:

  • Catalog text says "9 Roles" but auth-service/CLAUDE.md documents 11 built-in roles (SUPER_ADMIN, ADMIN, MANAGER, FRONTDESK_AGENT, HOUSEKEEPING_STAFF, POS_STAFF, ACCOUNTANT, REVENUE_MANAGER, MAINTENANCE_STAFF, PARTNER_USER, HOSPITAL_STAFF). The 2 additions vs original catalog (PARTNER_USER for service-account-style B2B integrations; HOSPITAL_STAFF likely for medical concierge tier — verify) are real and shipped.
  • The story does not enumerate which roles can access which API surfaces — that contract lives only in @Roles(...) decorators scattered across 17 backend services. Surface as a Day-2 ask.
  • Custom-roles-per-tenant surface (/roles CRUD) is documented but the relationship between a custom role and the 11 built-in roles is unclear: can a custom role inherit / extend a built-in one? Today: no — custom roles are independent. Story should call this out.

Drift findings:

D-008 — [P1] [Catalog-vs-code] Catalog title says "9 Roles" but actual enum has 11 (PARTNER_USER + HOSPITAL_STAFF added since the catalog was authored). The catalog is the public contract; running with the catalog 2 roles behind reality means new hires + auditors get the wrong picture.

  • [SME-PROPOSED] Update catalog row text to "RBAC (11 Roles)" and enumerate all 11 in US-001-006 acceptance criteria.
  • [SME-PROPOSED] Add AC: "the role enum SHALL be documented in 01-epic-catalog.md and auth-service/CLAUDE.md and the two SHALL be cross-validated by a test that asserts identity."

D-009 — [P2] [Frontend-gap] Backend RBAC is fully wired and tenant-isolation.flow.spec.ts covers cross-tenant denial, but frontend sidebar / permission-gate conditional rendering is incomplete (review file US-001-006-review.md 7/9). Today the dashboard shows menu items the user can't actually access — they get 403 on click. UX is jarring.

  • [SME-PROPOSED] Add AC: "dashboard sidebar / menu items SHALL be hidden when the current user's role does not include the required permission. Authoritative source: useRolePermissions() hook driven by the roles/permissions API."

US-001-007 — Password Reset via Email​

Catalog status: Built (P0, 3 points) Verified status: Built

Code locations:

  • hmp-auth-service/src/auth/auth.service.ts — requestPasswordReset(), resetPassword() (email-only resolution per AP-T01-001; generic 200 on unknown email; 1800s TTL per implementation report — was 3600s)
  • hmp-auth-service/src/auth/auth.controller.ts:124 — @Public() @Throttle({ default: { limit: 3, ttl: 3600000 } }) @Post('password/reset') (request)
  • hmp-auth-service/src/auth/auth.controller.ts:134 — @Post('password/reset/complete') (complete)
  • hmp-auth-service/src/auth/dto/login.dto.ts — ResetPasswordRequestDto, ResetPasswordDto
  • hmp-auth-service/src/tenants/CLAUDE.md — gotcha: email-only (no tenantId), anti-enumeration generic 200, 72-h TTL invitation tokens reuse password-reset:<token> Redis key (QA-PMS-6)
  • hmp-web-dashboard/src/app/(auth)/forgot-password/page.tsx — request form
  • hmp-web-dashboard/src/app/(auth)/reset-password/page.tsx — complete form

Unit + integration tests:

  • hmp-auth-service/src/auth/auth.service.spec.ts — 11 reset tests (5 request + 6 complete) + AP-T01-001 email-only specs
  • hmp-auth-service/src/auth/auth.sprint1-gaps.spec.ts — TTL 3600→1800 closure coverage

Flow tests:

  • none (no flow test for password reset per source map)

Real-backend smokes:

  • none

E2E (Playwright):

  • hmp-web-dashboard/e2e/sprint1-auth.spec.ts — forgot-password smoke coverage (alongside login)

Use cases referencing this story:

  • docs/requirements/use-cases/uc-forgot-password.yaml (AP-T01-001 — email-only, cross-tenant resolution)

SME notes:

  • Anti-enumeration design (generic 200 on unknown email) is correctly implemented but not in the story body — should be documented as a non-functional security requirement.
  • The Redis key password-reset:<token> is shared with the staff-invitation flow (QA-PMS-6, US-001-011) — same surface, two senders, two TTLs (30 min for forgot-password, 72 h for invite). The shared key is a deliberate design choice that lets both flows funnel through POST /auth/password/reset/complete. Story should call this out because a future refactor that namespaces the keys would silently break the invite flow.
  • DPDPA implication: password-reset emails are PII-bearing system messages; the platform's notification provider routing (SES in prod per MEMORY 2026-05-21) must remain on the producer-cleared list. Story should reference the notification-events contract.

Drift findings: (none — the existing implementation matches the story closely; the SME refinements above are clarifications, not drift.)

  • [SME-PROPOSED] Add AC: "password-reset token TTL = 30 minutes; staff-invitation token TTL = 72 hours; both share the Redis key namespace password-reset:<token> and both are consumed by POST /auth/password/reset/complete. A token-namespace refactor SHALL update both producers in the same PR."
  • [SME-PROPOSED] Add AC: "the request endpoint returns 200 OK with the same response body regardless of whether the email exists. Audit log row written only when the email exists; rate-limit applied per IP not per email (anti-enumeration)."

US-001-010 — Brute-Force Account Lockout​

Catalog status: Built (P0, 3 points) — catalog/review text says "5-attempt" Verified status: Partial — Redis-backed counter shipped; CERT-In events shipped; threshold mismatch between catalog and code

Code locations:

  • hmp-auth-service/src/auth/auth.service.ts — Redis-backed brute-force counter (auth:brute_force:{ip} + auth:account_fail:{email})
  • hmp-auth-service/CLAUDE.md — flow doc says 20 failures trigger lockout (per source-map drift: code threshold is 20, not the 5 stated in original review; cite both)
  • hmp-docs/docs/requirements/03-epic-catalog/01-epic-catalog.md:25 [CITED-BUT-NOT-FOUND] — catalog row text "Brute-Force Lockout (5-attempt)"
  • hmp-auth-service/src/risk-engine/risk.service.ts [CITED-BUT-NOT-FOUND] — brute-force risk engine consumer
  • hmp-auth-service/src/risk-engine/risk-rules.config.ts [CITED-BUT-NOT-FOUND] — risk rules config (threshold lives here)
  • In-memory fallback Map when Redis down (per source map / auth CLAUDE.md)
  • hmp-api-gateway/src/middleware/rate-limit.middleware.ts [CITED-BUT-NOT-FOUND] — gateway rate-limit middleware (referenced in source map)
  • CERT-In security event publication on lockout per implementation report

Unit + integration tests:

  • hmp-auth-service/src/auth/auth.service.spec.ts — brute force tracking tests
  • hmp-auth-service/src/risk-engine/risk.service.spec.ts — risk service spec
  • hmp-auth-service/src/risk-engine/risk-rules.config.spec.ts — rules config spec

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/brute-force-lockout.flow.spec.ts :: "20 invalid logins → lockout" — MODE=compose only (skips on QA) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes:

  • none

E2E (Playwright):

  • none specific (covered indirectly by sprint1-auth.spec.ts login negative paths)

Use cases referencing this story:

  • none direct

SME notes:

  • Threshold mismatch: catalog and original review file say 5 attempts but the flow test asserts 20 invalid logins → lockout and auth-service/CLAUDE.md documents 20. Either change is fine; the doc + code + flow test must agree.
  • Exponential backoff (60s → 5m → 15m → 30m) is referenced in BF-001 F3 but the story body does not enumerate the ladder. Cite as Day-2 doc gap.
  • Two counter keys exist (auth:brute_force:{ip} AND auth:account_fail:{email}) — the story should specify which one drives lockout (or whether either trips it). Today: both feed the same threshold per CLAUDE.md but a privileged attacker rotating IPs would only trip the email counter. Spec-level question: do we want per-IP and per-email lockout to compose, AND, or OR?
  • CERT-In commitment: the platform's 6-hour breach notification requirement means lockout events MUST publish a Kafka event consumed by the security event collector. Implementation report confirms this — story body should reference the contract.

Drift findings:

D-010 — [P1] [Catalog-vs-code] Catalog row + original review text says "5-attempt" lockout; auth-service code + flow test use 20. Operator-facing doc disagrees with operational behaviour. Production tuning may have been deliberate (5 was too aggressive for forgetful guests / staff sharing terminals) but the catalog was never updated.

  • [SME-PROPOSED] Refine US-001-010 to state the actual threshold (20), the exponential-backoff ladder (60s → 5m → 15m → 30m), and the two counter keys (per-IP + per-email — currently OR semantics; capture explicitly).
  • [SME-PROPOSED] Add AC: "lockout SHALL emit a CERT-In security-event Kafka message within 5 seconds of the lockout decision so the 6-hour breach-notification window starts at the wall-clock instant of detection."

D-011 — [P1] [Test-gap] Brute-force flow test is MODE=compose only and skips in QA/test/staging. Production behaviour for lockout has no live-environment integration test today.

  • [SME-PROPOSED] Add AC: "brute-force-lockout flow test SHALL run nightly in qa env with a dedicated bruteforce-test@keynova.ai account that auto-unlocks after each run."

US-001-011 — User CRUD (Invite, Activate, Deactivate)​

Catalog status: Partial (P0, 5 points) Verified status: Built — invite flow shipped via QA-PMS-6 (catalog "Partial" is stale)

Code locations:

  • hmp-auth-service/src/auth/auth.service.ts — createUser(), updateUser(), deleteUser(), getUsers(), getUser() (legacy admin surface under /auth/users per auth CLAUDE.md)
  • hmp-auth-service/src/users/users.controller.ts:48 — GET /users (for cross-service AuthServiceClient.getStaff(), FIND-P2-10)
  • hmp-auth-service/src/auth/dto/register.dto.ts — CreateUserDto, UpdateUserDto, BulkCreateUsersDto
  • hmp-auth-service/src/users/dto/list-users.dto.ts — ListUsersQueryDto
  • hmp-auth-service/CLAUDE.md (gotcha 7) — QA-PMS-6 fix: createUser() now mints uuidv4() token, stores in Redis password-reset:<token> 72-h TTL, emits setupLink in Kafka user-invitation event; consumable by existing POST /auth/password/reset/complete
  • hmp-web-dashboard/src/app/(dashboard)/settings/users/page.tsx — users management page

Unit + integration tests:

  • hmp-auth-service/src/auth/auth.service.spec.ts — 6 createUser tests + new spec cases per QA-PMS-6 fix (invitation token + setupLink)
  • hmp-auth-service/src/users/users.service.spec.ts — listStaff() for cross-service path
  • hmp-auth-service/src/tenants/admin-preverified.spec.ts — 8 tests on C1 SUPER_ADMIN-created admin pre-verified

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/tenant-onboarding-full.flow.spec.ts :: "Scenario 8 — 3 staff users with roles" — TBD (FT-4, not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)

Real-backend smokes:

  • hmp-web-dashboard/e2e/real-backend/smoke-users.real.spec.ts — users list renders with admin row

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/settings-users.spec.ts — users CRUD
  • hmp-web-dashboard/e2e/sprint1-property.spec.ts — users smoke coverage alongside property

Use cases referencing this story:

  • none direct (invitation surfaces via reset-password flow per AP-T01-001 / QA-PMS-6)

SME notes:

  • The review-file gap "invitation email used loginLink not set-password link" is closed by QA-PMS-6 (MEMORY 2026-05-21). New users now get a setup-password link with 72-h TTL via the shared password-reset:<token> Redis key. Catalog status Partial is stale.
  • Deactivation semantics: story says "Deactivate" but does not specify whether (a) active sessions are immediately invalidated, (b) the user's audit-log history is preserved, (c) the user's email is retained or anonymised under DPDPA. Today: sessions are not actively kicked (next 15-min token-refresh fails); audit history preserved; email retained. Each is reasonable but undocumented.
  • "Activate" is paired with "Deactivate" in the story title but the actual flow uses isActive=true/false boolean toggle — there is no explicit activate endpoint, just PATCH /users/:id { isActive: true }. Cite as Day-2 doc clarification.

Drift findings: (none — the QA-PMS-6 fix closes the prior Partial gap; the SME refinements below are clarifications, not drift.)

  • [SME-PROPOSED] Bump catalog status from Partial → Built to reflect QA-PMS-6 invitation flow shipping. Adjust the Catalog claims "Built": 6 count accordingly.
  • [SME-PROPOSED] Add AC: "Deactivating a user invalidates their refresh token immediately (so the next access-token refresh fails) but does NOT actively kick existing access tokens. Active access tokens expire within 15 minutes."
  • [SME-PROPOSED] Add AC: "Deactivation preserves the user's audit-log history under the deactivated user's id (no anonymisation), per DPDPA art. 8(7) controller's right-to-record-of-processing exception."