Files
baya-monorepo/dev/post-phase/hardening/hardening-phase-0-auth-gate.md
T
2026-07-17 13:22:04 +03:30

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.ts file convention (middleware is deprecated/renamed; file must sit at the same level as app — for this repo that's client/src/).

3. Scope — build this

  1. Make the gate execute — and prove it.
    • Pin the workspace root in client/next.config.mjs: turbopack.root (and outputFileTracingRoot) → the client/ 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.tsclient/src/proxy.ts per the Next 16 convention (export proxy; keep the exact logic + matcher). Update the client/CLAUDE.md references.
    • Runtime proof is part of the deliverable: with the dev server running and no cookies, GET / must 307 to /fa, and GET /fa, /fa/bookings, /fa/nurse, /fa/admin must each redirect to /fa/login. GET /fa/login must 200. If the probe fails, keep working — do not declare done on a code-only fix.
  2. JWE-compatible liveness. The access token can't be decoded client-side, but verify_otp/refresh already return accessExpiresAt/refreshExpiresAt:
    • persistAuthTokens additionally writes a non-sensitive companion cookie (e.g. access_expires_at, name in COOKIE_NAMES) holding the ISO/epoch expiry; cookie maxAges for both tokens derive from the served expiries (this also fixes the documented 7d-vs-server drift for the refresh cookie). clearAuthTokens deletes 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: getServerAuthState seeds isAuthenticated=true after a hard reload of a logged-in session — verify useMe/useSessionRoleSync then hydrate roles normally.
  3. Unauthenticated branch in the client fallback (defense in depth).
    • useRoleHydration gains an explicit unauthenticated state (auth context says logged out); RoleGuard redirects it to /${locale}/login?next=<current path> instead of splashing forever.
    • The login flow honors next: after verify (+ role routing), RoleRouter prefers a safe, same-origin relative next path over resolveRoleDestination when present. Never redirect to an absolute/external URL.
    • clientFetch's unrecoverable-401 redirect should also carry next.

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_PATHS uses startsWith — 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/bookings stays 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 check green; npm run test:ci green; RoleGuard + routing tests extended for the unauthenticated branch and the next param.

7. How to test

  1. cd client && npm run dev (server up per dev/post-phase/refinement/RUNBOOK.md).
  2. No cookies: curl -I http://localhost:3000/fa/bookings → 307/308 Location: /fa/login?next=….
  3. Log in (dev OTP via GET /api/v1/dev/last_otp/{phone}), F5 on the home shell → stays, roles load.
  4. Delete only the access_token cookie, navigate → silent refresh recovers; delete both → login ask.
  5. Visit /fa/nurse as 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.yaml should still be deleted manually; the root pin makes the app immune, but other tools may not be.