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
| Story | Why in scope |
|---|---|
| US-001-001 Login | Tenant 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 Login | Same; MFA gate on first admin login |
| US-001-006 RBAC | Tenant admin's role bootstrap |
| US-001-007 Password Reset via Email | First-time admin sets password via reset link |
| US-001-010 Brute-Force Lockout | Onboarding form must be hardened against signup-flood |
| US-001-011 User CRUD (Invite/Activate/Deactivate) | Admin invites first staff users post-onboarding |
Status legend
| Status | Meaning |
|---|---|
Built | Code exists + tests exist + tests pass |
Built-Untested | Code exists, no automated test coverage |
Partial | Code exists for some acceptance criteria, gaps for others |
Stub | Code is a placeholder / TODO / returns mock data |
Not Started | No code |
Summary
| Metric | Count |
|---|---|
| In-scope stories | 7 |
| Catalog claims "Built" | 6 (US-001-001/004/005/006/007/010); US-001-011 = Partial |
| Verified-Built | 4 (US-001-001, US-001-005, US-001-007, US-001-011) |
| Built-Untested | 0 |
| Partial | 3 (US-001-004, US-001-006, US-001-010) |
| Stub | 0 |
| Not Started | 0 |
| Drift findings | 11 (D-001..D-011; 2 P0 / 8 P1 / 1 P2) |
| [SME-PROPOSED] refinements | 20 |
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 viaMFA_ENFORCE_ADMIN=false)hmp-shared-packages/packages/auth/src/guards/jwt-auth.guard.ts—JwtAuthGuard.handleRequest(per MEMORY 2026-05-20: throwsMFA_REQUIRED_FOR_ADMINfor ADMIN/SUPER_ADMIN withoutmfaEnabled=true; honoursMFA_ENFORCE_ADMIN='false'kill-switch via strict equality —@hmp/auth@1.6.1)hmp-web-dashboard/src/app/(auth)/login/page.tsx— re-exports./keynovalogin pagehmp-web-dashboard/src/app/(auth)/login/keynova/— KeyNova-branded login UIhmp-web-dashboard/src/lib/auth.ts— NextAuthauthorize()(parses /auth/me 401 body; catchesMFA_REQUIRED_FOR_ADMIN→ throws structuredMfaSetupRequired:<email>per MEMORY 2026-05-20)hmp-admin-portal/src/lib/api/auth.ts— admin-portal login client (peruc-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 testshmp-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 closurehmp-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.1per 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 + interactionhmp-web-dashboard/e2e/tests/02-auth-flows.spec.ts— login/logout/role-basedhmp-web-dashboard/e2e/persona-admin.spec.ts— admin persona login coveragehmp-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.yamlAP-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 viaMfaSetupRequired:<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/propertiesreturns 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
codefield, 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,mfaLockedUntilcolumns, 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,MfaVerifyDtohmp-web-dashboard/src/app/(dashboard)/settings/security/mfa/page.tsx— QR + verify UI (usesqrcodenpm)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 reporthmp-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=trueis set, an ADMIN withoutmfaEnabled=trueis bounced to/auth/mfa/setupon every non-@MfaExempt()API call." Tested viajwt-auth.guard.spec.tskill-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=truein 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 +tempToken5-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— NextAuthauthorize()(catchesmfaRequired:true/tempTokenand 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/loginissuestempToken;/auth/mfa/validateexchanges it for the real JWT) is shipped per implementation report. Thereviews/EP-001/US-001-005-review.mdverdictFAIL(2/10) pre-dates the fix and is stale. - Story body does not document the
tempToken5-minute TTL or its renewal semantics (can it be re-issued from/auth/loginif the user takes too long? Today: yes, a new login resets the timer). - Story does not document MFA lockout threshold (today: tracked in
mfaFailedAttempts+mfaLockedUntilcolumns; 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.mdso 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
mfaFailedAttemptscolumn), 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]—RolesGuardfrom@hmp/authhmp-shared-packages/packages/common/src/decorators/roles.decorator.ts[CITED-BUT-NOT-FOUND]—@Roles()decorator from@hmp/commonhmp-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')withassertTenantScope()(AP-T01-003)hmp-web-dashboard/src/app/(dashboard)/settings/roles/page.tsx[CITED-BUT-NOT-FOUND]— roles & permissions page (referenced bysmoke-roles.real.spec.ts)- Frontend sidebar/permission-gate wiring — gaps remain per review (review file
US-001-006-review.mdflagged 7/9 with frontend gaps)
Unit + integration tests:
RolesGuard— review file flagged as UNTESTED (GAP-4inreviews/EP-001/US-001-006-review.md); Task 6 verify if guard tests have shipped sincehmp-auth-service/src/auth/auth.service.spec.ts— role assignment coverage as part of createUser/updateUser testshmp-docs/docs/requirements/use-cases/uc-onboarding-admin-role.yamlcompanion specs (AP-T01-003 — ADMIN tenant-scope on onboarding controller)- Sprint-1 review
reviews/EP-001/sprint-1-auth-review.mdandcustom-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 CRUDhmp-web-dashboard/e2e/persona-admin.spec.ts— admin persona role coveragehmp-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.mddocuments 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 (
/rolesCRUD) 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.mdandauth-service/CLAUDE.mdand 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 theroles/permissionsAPI."
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,ResetPasswordDtohmp-auth-service/src/tenants/CLAUDE.md— gotcha: email-only (no tenantId), anti-enumeration generic 200, 72-h TTL invitation tokens reusepassword-reset:<token>Redis key (QA-PMS-6)hmp-web-dashboard/src/app/(auth)/forgot-password/page.tsx— request formhmp-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 specshmp-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 throughPOST /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 byPOST /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 OKwith 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 consumerhmp-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 testshmp-auth-service/src/risk-engine/risk.service.spec.ts— risk service spechmp-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.tslogin 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.mddocuments 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}ANDauth: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.aiaccount 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/usersper authCLAUDE.md)hmp-auth-service/src/users/users.controller.ts:48—GET /users(for cross-serviceAuthServiceClient.getStaff(), FIND-P2-10)hmp-auth-service/src/auth/dto/register.dto.ts—CreateUserDto,UpdateUserDto,BulkCreateUsersDtohmp-auth-service/src/users/dto/list-users.dto.ts—ListUsersQueryDtohmp-auth-service/CLAUDE.md(gotcha 7) — QA-PMS-6 fix:createUser()now mintsuuidv4()token, stores in Redispassword-reset:<token>72-h TTL, emitssetupLinkin Kafkauser-invitationevent; consumable by existingPOST /auth/password/reset/completehmp-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 pathhmp-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 CRUDhmp-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 statusPartialis 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/falseboolean toggle — there is no explicitactivateendpoint, justPATCH /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→Builtto reflect QA-PMS-6 invitation flow shipping. Adjust theCatalog claims "Built": 6count 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."