6.9 KiB
Hardening Phase 0 — Auth gate & session liveness (make the app ask for login)
Make the login gate actually execute at runtime, make token liveness readable despite the JWE access token, and give anonymous visitors a login redirect instead of an infinite splash. This is the direct fix for the reported symptom "there is no ask for logging in." Track: frontend · Depends on: — · Unlocks: Phases 2, 3 Before you start, read _shared/agent-operating-rules.md.
1. Context — where this sits
Fixes H-01, H-02, H-03 from issues.md. Three defects mask each other: the middleware
never runs (so nobody is asked to log in), its token check could never pass anyway (the token is an
encrypted JWE), and the client fallback shows an infinite splash to anonymous visitors. The gate
design (middleware + PUBLIC_PATHS + RoleGuard-as-chrome) is correct and stays.
What already exists (do not rebuild): client/middleware.ts (correct logic), PUBLIC_PATHS
(src/constants/routes.ts:169), RoleGuard/useRoleHydration/AuthAccountError
(refinement-phase-2), persistAuthTokens/clearAuthTokens (src/lib/auth/session.ts), the
single-flight silent refresh (src/lib/api/refresh.ts), and the wire's
accessExpiresAt/refreshExpiresAt (src/services/auth/types.ts:62-66).
2. Required reading (do this first)
- issues.md H-01/H-02/H-03 — the verified evidence; don't re-audit.
client/middleware.ts,client/src/lib/auth/token.ts,client/src/lib/auth/session.ts,client/src/lib/auth/server.ts,client/src/components/auth/RoleGuard.tsx,client/src/services/auth/hooks/useRoleHydration.ts,client/src/services/auth/routing.ts.client/CLAUDE.md→ "Auth Cookies & session state" (the documented design you are repairing) and "Golden rules".- Next.js 16
proxy.tsfile convention (middleware is deprecated/renamed; file must sit at the same level asapp— for this repo that'sclient/src/).
3. Scope — build this
- Make the gate execute — and prove it.
- Pin the workspace root in
client/next.config.mjs:turbopack.root(andoutputFileTracingRoot) → theclient/directory, so a stray lockfile in an ancestor directory (the live failure:C:\Users\Lenovo\pnpm-lock.yaml) can never re-point root detection. - Migrate
client/middleware.ts→client/src/proxy.tsper the Next 16 convention (exportproxy; keep the exact logic + matcher). Update theclient/CLAUDE.mdreferences. - Runtime proof is part of the deliverable: with the dev server running and no cookies,
GET /must 307 to/fa, andGET /fa,/fa/bookings,/fa/nurse,/fa/adminmust each redirect to/fa/login.GET /fa/loginmust 200. If the probe fails, keep working — do not declare done on a code-only fix.
- Pin the workspace root in
- JWE-compatible liveness. The access token can't be decoded client-side, but verify_otp/refresh
already return
accessExpiresAt/refreshExpiresAt:persistAuthTokensadditionally writes a non-sensitive companion cookie (e.g.access_expires_at, name inCOOKIE_NAMES) holding the ISO/epoch expiry; cookiemaxAges for both tokens derive from the served expiries (this also fixes the documented 7d-vs-server drift for the refresh cookie).clearAuthTokensdeletes it.- Replace
isTokenAlive(token)call sites (proxy +getServerAuthState) with a check that reads the expiry cookie when the token doesn't decode as a plain JWT (keep the JWT path as fallback so a future JWS still works). Token presence without a readable, live expiry = not alive. - Result:
getServerAuthStateseedsisAuthenticated=trueafter a hard reload of a logged-in session — verifyuseMe/useSessionRoleSyncthen hydrate roles normally.
- Unauthenticated branch in the client fallback (defense in depth).
useRoleHydrationgains an explicitunauthenticatedstate (auth context says logged out);RoleGuardredirects it to/${locale}/login?next=<current path>instead of splashing forever.- The login flow honors
next: after verify (+ role routing),RoleRouterprefers a safe, same-origin relativenextpath overresolveRoleDestinationwhen present. Never redirect to an absolute/external URL. clientFetch's unrecoverable-401 redirect should also carrynext.
4. Mocks & seams in this phase
None introduced. Do not touch USE_*_MOCK flags here (Phase 2 owns them).
5. Critical rules you must not get wrong
- The middleware/proxy stays UX-only — no signature verification client-side; the API remains the authority. Don't try to decrypt the JWE in the client.
- The expiry cookie is a liveness hint, not a credential — never gate real authorization on it.
PUBLIC_PATHSusesstartsWith— never add'/'or another prefix-of-everything entry.- Don't break the locale flow: the i18n redirect handling and locale header in the current middleware must survive the migration exactly.
- Keep RoleGuard's existing loading/error/mismatch semantics (refinement-phase-2) — you're adding a fourth branch, not rewriting the guard.
- Golden rules: no hardcoded strings (new copy → both
messages/*.json), cookie access only via the cookie manager, constants for names/params.
6. Definition of Done
- The runtime probe in §3.1 passes (paste the curl/status output into your report).
- Logged-in user: hard reload on
/fa/bookingsstays there (no splash-hang, no bounce to login). - Logged-out user: any private deep link → login → completes OTP → lands back on the deep link.
- Logout → immediately bounced to login on the next private navigation.
npm run checkgreen;npm run test:cigreen; RoleGuard + routing tests extended for theunauthenticatedbranch and thenextparam.
7. How to test
cd client && npm run dev(server up perdev/post-phase/refinement/RUNBOOK.md).- No cookies:
curl -I http://localhost:3000/fa/bookings→ 307/308Location: /fa/login?next=…. - Log in (dev OTP via
GET /api/v1/dev/last_otp/{phone}), F5 on the home shell → stays, roles load. - Delete only the
access_tokencookie, navigate → silent refresh recovers; delete both → login ask. - Visit
/fa/nurseas a customer-only session → RoleGuard redirect toast (unchanged behavior).
8. Hand off & document
- Update
client/CLAUDE.md(middleware→proxy path, expiry-cookie lifecycle, the JWE note) — its "middleware.ts" references and cookie table must match reality. - Tick H-01/H-02/H-03 in issues.md with the commit hash.
- Write
dev/shared-working-context/reports/hardening-phase-0-report.md(what was verified at runtime, any surprises). - Note to the human: the stray
C:\Users\Lenovo\pnpm-lock.yamlshould still be deleted manually; the root pin makes the app immune, but other tools may not be.