diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 01b37c0..c46c49e 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -38,9 +38,13 @@ async function fetchApi(path: string, options: RequestInit = {}): Promise }) if (!res.ok) { if (res.status === 401) { + // Avoid redirect storms: only hand off once per page load + const handoffKey = 'vps_auth_401_handoff' + const already = sessionStorage.getItem(handoffKey) clearToken() const cfg = await ensureAuthConfig() - if (cfg.required || isAuthEnabled()) { + if ((cfg.required || isAuthEnabled()) && !already) { + sessionStorage.setItem(handoffKey, '1') redirectToPortalLogin(`${window.location.origin}/auth/callback`) } } diff --git a/apps/web/src/routes/auth.callback.tsx b/apps/web/src/routes/auth.callback.tsx index 520bccc..3bfff02 100644 --- a/apps/web/src/routes/auth.callback.tsx +++ b/apps/web/src/routes/auth.callback.tsx @@ -2,6 +2,8 @@ import { createFileRoute, redirect } from '@tanstack/react-router' import { ensureAuthConfig, firstAllowedPath, + getClaims, + getToken, parseHashToken, redirectToPortalLogin, setToken, @@ -11,14 +13,19 @@ export const Route = createFileRoute('/auth/callback')({ beforeLoad: async () => { await ensureAuthConfig() const { accessToken } = parseHashToken(window.location.hash) - if (!accessToken) { - redirectToPortalLogin(`${window.location.origin}/auth/callback`) - await new Promise(() => {}) - return + if (accessToken) { + setToken(accessToken) + // Do not replaceState to strip the hash here — that re-triggers beforeLoad + // with an empty hash and sends the user back to the portal (SSO loop). + sessionStorage.removeItem('vps_auth_401_handoff') + throw redirect({ to: firstAllowedPath() }) } - setToken(accessToken) - window.history.replaceState(null, '', '/auth/callback') - throw redirect({ to: firstAllowedPath() }) + // Already stored from a previous parse (e.g. remount) — finish handoff. + if (getToken() && getClaims()) { + throw redirect({ to: firstAllowedPath() }) + } + redirectToPortalLogin(`${window.location.origin}/auth/callback`) + await new Promise(() => {}) }, component: () => null, })