Part of the BFF cookie-auth migration EPIC.
Replace the oidc-client-ts / sessionStorage / Bearer flow with cookie + /auth/me. Drives the FE PRD rewritten in FE Phase A (#156).
Order (resolved Q5): start once backend Phases 2 and 3 are live (refresh, logout, sessions, CSRF, back-channel). Router Phase 1 should also be in dev by then so /api/* calls work cookie-only.
Scope
Files
| File |
Change |
src/app/auth/OidcManager.ts |
Delete. All OIDC client logic now backend-side. |
src/app/effects/authEffects.ts |
Rewrite. On boot: GET /auth/me. On refresh_at: POST /auth/refresh (leader-only). On 401: window.location = '/auth/login?return_to=...'. |
src/app/actions/bootstrapActions.ts |
Stop decoding JWT. Read user.email etc. from /auth/me response. Reverts the unique_name/sub fallback hack from commit 6aa99ce. |
src/app/api/AuthApiService.ts |
New methods: getMe(), refresh(), logout(), listSessions(), revokeSession(id), revokeAll(), getCsrf(). All requests use credentials: 'include'. |
HTTP client (AuthPlugin + friends) |
Set credentials: 'include' on all fetch calls. Remove all Authorization: Bearer … injection. Add CSRF middleware: read token from auth state cache (primed by /auth/me), echo as X-CSRF-Token on POST/PUT/PATCH/DELETE. |
| Login UI / route guard |
Sign-in button: window.location = '/auth/login?return_to=' + encodeURIComponent(location.pathname). Unauth state derived from /auth/me 401. |
NEW src/app/auth/AuthTabCoordinator.ts |
BroadcastChannel('insight-auth') leader election. Only leader fires refresh. Followers receive {expires_at, refresh_at} broadcast and update timers. Fallback to localStorage 'storage' events. |
src/app/auth/RestMockPlugin.ts (legacy) |
Delete the window.__OIDC_CONFIG__ mock. SPA no longer needs OIDC config — all logic backend-side. |
docker-entrypoint.sh |
Remove OIDC_* env-var injection (OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_SCOPES). SPA chart values can drop the corresponding keys. |
package.json |
Remove oidc-client-ts. Remove any JWT-decode lib if only used for token parsing. |
vite.config.ts |
Verify dev proxy routes /auth/* and /api/* to the api-gateway dev port (http://localhost:8080) so cookies remain first-party in dev. |
Logic detail
- Boot flow: SPA calls
GET /auth/me. On 200 → app renders, schedule refresh timer at refresh_at. On 401 → redirect to /auth/login?return_to=....
- Refresh timer:
setTimeout(refresh_at - now). On fire, leader calls POST /auth/refresh with X-CSRF-Token. On success, broadcast {expires_at, refresh_at} and reschedule. On 401 → broadcast logout, redirect to login.
- Leader election: First tab to join channel claims leadership. On leader unload, next tab takes over (heartbeat via
BroadcastChannel). Followers honor leader's refresh broadcasts and do not fire their own timer.
- API 401 handler: Any
/api/* returning 401 → redirect to /auth/login?return_to=.... NEVER auto-refresh on 401 — refresh is server-scheduled.
- CSRF: Token primed from
/auth/me. Refreshed via GET /auth/csrf if a state-changing request gets 403 with WWW-Authenticate: csrf-required.
- Logout: Button →
POST /auth/logout → response {end_session_url} → window.location = end_session_url.
Out of scope
- Sessions management UI (
/auth/sessions endpoints) — file as a follow-up issue if/when the UX exists. Backend ships it; SPA can consume it later.
- Impersonation flow changes — backend dev-fake-OIDC stub covers dev needs; the existing impersonation feature (persisted in sessionStorage per commit
07ddc69) is orthogonal and stays as-is for now.
Acceptance
- Cold load in incognito → SPA redirects to backend
/auth/login. After IdP flow, lands on the original path.
document.cookie shows only __Host-sid (HttpOnly, can't read from JS — verify via DevTools Network tab cookie inspector).
sessionStorage and localStorage contain NO token data. (Old oidc.user:* keys are gone.)
- Open two tabs → both authenticated; only one tab fires
/auth/refresh per round (verify in Network tab).
- Logout from one tab → other tab redirects to login on next interaction (401 from
/api/*).
- POST to a state-changing API →
X-CSRF-Token header present. Missing → 403 from backend.
- Remove
oidc-client-ts from package.json → npm install + build succeeds; bundle size drops measurably.
- Manual smoke against
cargo run -p api-gateway --features dev-auth + dev-fake-OIDC stub: full login/refresh/logout loop with INSIGHT_DEV_USER_EMAIL user.
- All existing E2E tests pass (or get updated; expect mostly login flow tests to change).
Dependencies
Part of the BFF cookie-auth migration EPIC.
Replace the oidc-client-ts / sessionStorage / Bearer flow with cookie +
/auth/me. Drives the FE PRD rewritten in FE Phase A (#156).Order (resolved Q5): start once backend Phases 2 and 3 are live (refresh, logout, sessions, CSRF, back-channel). Router Phase 1 should also be in dev by then so
/api/*calls work cookie-only.Scope
Files
src/app/auth/OidcManager.tssrc/app/effects/authEffects.tsGET /auth/me. Onrefresh_at:POST /auth/refresh(leader-only). On 401:window.location = '/auth/login?return_to=...'.src/app/actions/bootstrapActions.tsuser.emailetc. from/auth/meresponse. Reverts theunique_name/subfallback hack from commit6aa99ce.src/app/api/AuthApiService.tsgetMe(),refresh(),logout(),listSessions(),revokeSession(id),revokeAll(),getCsrf(). All requests usecredentials: 'include'.AuthPlugin+ friends)credentials: 'include'on allfetchcalls. Remove allAuthorization: Bearer …injection. Add CSRF middleware: read token from auth state cache (primed by/auth/me), echo asX-CSRF-Tokenon POST/PUT/PATCH/DELETE.window.location = '/auth/login?return_to=' + encodeURIComponent(location.pathname). Unauth state derived from/auth/me401.src/app/auth/AuthTabCoordinator.tsBroadcastChannel('insight-auth')leader election. Only leader fires refresh. Followers receive{expires_at, refresh_at}broadcast and update timers. Fallback tolocalStorage'storage' events.src/app/auth/RestMockPlugin.ts(legacy)window.__OIDC_CONFIG__mock. SPA no longer needs OIDC config — all logic backend-side.docker-entrypoint.shOIDC_*env-var injection (OIDC_ISSUER,OIDC_CLIENT_ID,OIDC_SCOPES). SPA chart values can drop the corresponding keys.package.jsonoidc-client-ts. Remove any JWT-decode lib if only used for token parsing.vite.config.ts/auth/*and/api/*to the api-gateway dev port (http://localhost:8080) so cookies remain first-party in dev.Logic detail
GET /auth/me. On 200 → app renders, schedule refresh timer atrefresh_at. On 401 → redirect to/auth/login?return_to=....setTimeout(refresh_at - now). On fire, leader callsPOST /auth/refreshwithX-CSRF-Token. On success, broadcast{expires_at, refresh_at}and reschedule. On 401 → broadcast logout, redirect to login.BroadcastChannel). Followers honor leader's refresh broadcasts and do not fire their own timer./api/*returning 401 → redirect to/auth/login?return_to=.... NEVER auto-refresh on 401 — refresh is server-scheduled./auth/me. Refreshed viaGET /auth/csrfif a state-changing request gets 403 withWWW-Authenticate: csrf-required.POST /auth/logout→ response{end_session_url}→window.location = end_session_url.Out of scope
/auth/sessionsendpoints) — file as a follow-up issue if/when the UX exists. Backend ships it; SPA can consume it later.07ddc69) is orthogonal and stays as-is for now.Acceptance
/auth/login. After IdP flow, lands on the original path.document.cookieshows only__Host-sid(HttpOnly, can't read from JS — verify via DevTools Network tab cookie inspector).sessionStorageandlocalStoragecontain NO token data. (Oldoidc.user:*keys are gone.)/auth/refreshper round (verify in Network tab)./api/*).X-CSRF-Tokenheader present. Missing → 403 from backend.oidc-client-tsfrompackage.json→npm install+ build succeeds; bundle size drops measurably.cargo run -p api-gateway --features dev-auth+ dev-fake-OIDC stub: full login/refresh/logout loop withINSIGHT_DEV_USER_EMAILuser.Dependencies