# Hardening phases — auth gate, role enforcement & the last mocks **Created:** 2026-07-16 · **Scope:** whole repo (client + server), after the 16+16 build phases and the 10 refinement phases all completed · **Method:** a 6-dimension multi-agent audit of the live code (anonymous access, role guarding, server authorization, remaining mocks, end-to-end journey walk, session/error UX), every finding **adversarially re-verified** against the code with file:line evidence, plus a live runtime probe of the dev server. This directory is a **runnable chain of 6 hardening phases**. Run them **in order, one at a time**, pointing a fresh agent at one phase file (*"Execute `dev/post-phase/hardening/hardening-phase-0-auth-gate.md` end to end"*) — or drive the whole chain with [`LOOP-PROMPT.md`](LOOP-PROMPT.md). The full verified finding ledger (with per-item checkboxes) is in [`issues.md`](issues.md). > **Why this exists after the refinement chain.** The refinement phases made the two projects run as one > app. But live probing shows the user-facing symptom *"the app never asks me to log in, and roles feel > unenforced"* is real — caused by an auth gate that never executes at runtime, a token-liveness check > that can't read the server's encrypted token, an admin RBAC policy that is structurally dead (every > admin endpoint 403s), and two client money-path mocks left dangling by the phase-4 de-mock. --- ## The headline: why the app never asks for login Three defects **mask each other**, which is why this survived nine refinement phases: 1. **`client/middleware.ts` never executes in the running app** (verified live: a cookie-less `GET /fa/admin` returns 200 with page HTML; a bare `GET /` returns 404 instead of next-intl's locale redirect — under both Turbopack *and* webpack). The gate code itself is correct. Root cause on this machine: a stray `C:\Users\Lenovo\pnpm-lock.yaml` (home directory, not in the repo) makes Next.js infer the **workspace root as the home directory**, so the middleware file is never bound. The repo must defend against this (pin the root in `next.config.mjs`) and prove the gate runs (runtime DoD in Phase 0). Next 16 has also deprecated `middleware.ts` in favor of `proxy.ts`. 2. **`isTokenAlive` can never return `true` for a real token.** The server's access token is an **encrypted JWE** (`JwtService.cs:116-128`, `EncryptingCredentials`, A128KW/A128CBC-HS256); the client helper (`client/src/lib/auth/token.ts:24-38`) base64-decodes segment 1 and JSON-parses it — impossible for a JWE. So had the middleware ever run, it would have **redirect-looped logged-in users to /login**. Same helper seeds server-side auth state (`getServerAuthState`), so `isAuthenticated` seeds `false` on every hard reload. The wire already returns `accessExpiresAt`/`refreshExpiresAt` (`client/src/services/auth/types.ts:65-66`) — the fix is a readable companion expiry cookie. 3. **The client fallback can't rescue an anonymous visitor.** `useMe()` is enabled only when `isAuthenticated`; for a cookie-less request that's `false` forever, so `useRoleHydration()` stays `loading` and every private shell renders an **infinite branded splash** instead of redirecting to login. RoleGuard needs an explicit unauthenticated → redirect-to-login branch (defense in depth). **One manual step no phase can do for you:** delete or move the stray `C:\Users\Lenovo\pnpm-lock.yaml` from your home directory (it is unrelated to this repo). Phase 0 pins the workspace root so the app no longer *depends* on that cleanup, but the stray file will keep confusing other tools too. --- ## What's actually fine (don't re-fix) - The middleware/auth-gate **logic** and `PUBLIC_PATHS` are correct as written — the problem is execution + the JWE check, not the design. - The 4 private shells genuinely all wrap `RoleGuard`; admin **mutations** are consistently gated behind `useAdminCapabilities()`. - Server **tenancy** is enforced correctly in every spot-checked handler (bookings, tickets, patients, care records, bank accounts, centers) — owner-or-staff checks with 404-not-403. - Public endpoints (catalog/geo/search/nurses, webhooks, dev) are intentionally anonymous; `dev/last_otp` correctly 404s outside Development. - The demo seed gives Journey A a real searchable Tehran nurse; base route names match 1:1 client↔server; the silent-refresh mechanism (single-flight, retry-once) is sound. ## The verified problem inventory (17 findings + root cause) Severity-ordered; the full ledger with evidence is [`issues.md`](issues.md). | # | Severity | Problem | Phase | | --- | --- | --- | --- | | H-01 | blocker | Auth gate never executes at runtime (workspace-root misdetection; middleware deprecated) | 0 | | H-02 | blocker | `isTokenAlive` can't read the JWE token → would redirect-loop; seeds `isAuthenticated=false` on reload | 0 | | H-03 | high | Anonymous visitor to a private shell gets an infinite splash, never a login prompt | 0 | | H-04 | blocker | `DynamicPermission` RBAC is dead: no RoleClaim ever seeded/grantable → every admin endpoint 403s for the seeded `super_admin`/`finance` personas | 1 | | H-05 | high | `BookingRoles.Admin` bundles Support/Moderation into clinical-notes + nurse-balance + forced-transition access | 1 | | H-06 | blocker | Refunds mock cross-imports the retired bookings-mock store → real cancellations 404 | 2 | | H-07 | blocker | BNPL mock cross-imports retired mock stores → installment checkout 404s or fabricates a fake success while the real request expires unpaid | 2 | | H-08 | blocker | Verification 100% mocked while catalog/search are real → a real nurse "publishes" services that can never appear in search, no feedback | 2 | | H-09 | high | Nurse earnings screen fabricated although the REQ-025 endpoints are live (flag held hostage by the admin half of the seam) | 2 | | H-10 | medium | Payment outcome hard-codes `bookingId: null` though REQ-017 is delivered → confirmation deep links lost | 2 | | H-11 | high | Logout/login never clear the React Query cache → previous user's data leaks to the next login on the same device | 3 | | H-12 | high | The only error boundary dumps a raw English stack trace, no retry; no `error.tsx`/`global-error.tsx` | 3 | | H-13 | high | All 401/403/5xx/network toasts are hardcoded English on a Persian-default app | 3 | | H-14 | medium | Admin read-only consoles (audit/verification/tickets/roles) render without a capability check — only the nav hides them | 3 | | H-15 | medium | Tier B/C contract REQs still open (refunds 019-021, BNPL 022-024, admin 029-031, partner 032/033/038, verification admin 034, refund admin 035, payout admin 036) | 4 | | H-16 | medium | Partner portal unreachable from login (no `/me` signal), fully mocked, no tenancy gate on its pages | 4+5 | | H-17 | medium | patientRecords family record: client `string` ids vs wire `long` ids → PUT is write-unsafe; edits don't survive reload | 5 | ## The 6 hardening phases | # | Phase | Track | Fixes | Depends on | | --- | --- | --- | --- | --- | | **0** | [Auth gate & session liveness](hardening-phase-0-auth-gate.md) | frontend | H-01 H-02 H-03 | — | | **1** | [Admin RBAC & staff role scopes](hardening-phase-1-admin-rbac.md) | backend | H-04 H-05 (+delivers REQ-031) | — | | **2** | [Money-path mock integrity](hardening-phase-2-mock-integrity.md) | frontend | H-06 H-07 H-08 H-09 H-10 | 0 | | **3** | [Session & error-surface hardening](hardening-phase-3-session-error-ux.md) | frontend | H-11 H-12 H-13 H-14 | 0 | | **4** | [Contract completion batch (Tier B/C)](hardening-phase-4-contract-completion.md) | backend | H-15 H-16(server half) | 1 | | **5** | [Final de-mock & partner reachability](hardening-phase-5-final-demock.md) | frontend | H-16(client half) H-17 + flip the last flags | 2, 4 | ``` frontend: 0 auth gate ──► 2 mock integrity ──► 5 final de-mock └───────► 3 session/error UX ▲ backend: 1 admin RBAC ──► 4 contract batch ──────┘ ``` Phases 0 and 1 are independent — a frontend and a backend agent can run them in parallel (the [shared-working-context protocol](../../shared-working-context/README.md) applies). **Minimum path to "the app asks for login and roles hold":** 0 → 1. **Minimum path to "the money path works end-to-end on real data":** 0 → 2. Everything real, no mocks: all six. ## How the phase files are written Same skeleton as the rest of the repo (the [phase template](../../phases/_shared/phase-template.md)): mission, context, required reading, enumerated scope **with the audit's file:line evidence inlined** (so the executing agent doesn't re-audit), invariants, Definition of Done, how to test, close-out. Before executing any phase, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md). ## Related documents - [issues.md](issues.md) — the verified finding ledger (evidence + checkboxes; the loop's progress state). - [LOOP-PROMPT.md](LOOP-PROMPT.md) — the reusable prompt that drives this chain phase by phase. - [../refinement/README.md](../refinement/README.md) — the prior chain this one follows. - [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) — the REQ ledger Phase 4 closes out. - [../../shared-working-context/reports/mocks-registry.md](../../shared-working-context/reports/mocks-registry.md) — the mock registry Phases 2 & 5 update.