From 95104904c420e4f32363f14e43b7f34e288ba265 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Mon, 3 Aug 2026 23:00:15 +0200 Subject: [PATCH] feat(oidc): reduce amount of page when auto_redirect_to_oidc --- frontend/src/lib/api/endpoints/auth.ts | 14 ++++++++++++++ frontend/src/routes/+layout.svelte | 26 +++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/api/endpoints/auth.ts b/frontend/src/lib/api/endpoints/auth.ts index e78b8db7..7d1ea762 100644 --- a/frontend/src/lib/api/endpoints/auth.ts +++ b/frontend/src/lib/api/endpoints/auth.ts @@ -99,6 +99,20 @@ export interface OidcProviders { */ require_verified_email?: boolean; authorize_endpoint?: string; + /** + * Server-computed: true when the `auto_redirect_if_standalone_oidc` + * policy is on AND OIDC is the only working method (see + * `AuthApplicationService::auto_redirect_to_oidc`). When true the root + * layout guard `window.location.replace`s to `authorize_endpoint` + * instead of routing through `/login` — the server-side `/login` + * middleware only fires on full HTTP loads, so SPA client-side + * navigation to `/login` (root guard, dev via Vite) would otherwise + * stall on the login page. Because the flag is gated by the admin's + * policy on the SERVER, using it on the client does NOT override the + * policy toggle — we're just enacting the same decision on paths the + * middleware can't reach. + */ + auto_redirect_to_oidc?: boolean; } /** Public OIDC provider info for the login page. */ diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 597aa4c3..2052001f 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -12,6 +12,7 @@ import { ui } from '$lib/stores/ui.svelte'; import { hashUrlToPath } from '$lib/utils/hashRedirect'; import { killLegacyServiceWorker } from '$lib/utils/killLegacyServiceWorker'; + import { getOidcProviders, type OidcProviders } from '$lib/api/endpoints/auth'; let { children } = $props(); @@ -34,6 +35,14 @@ } let ready = $state(false); + // Providers info fetched at boot so the guard below can enact the + // SSO-only server-side policy (`auto_redirect_if_standalone_oidc`) on + // SPA client-nav paths the middleware in interfaces/web/mod.rs can't + // see. The middleware only fires on full HTTP loads to /login; when + // the layout guard is about to `goto('/login')` we short-circuit to + // the IdP directly if the server tells us to. Null until fetched; + // the guard waits for it before deciding. + let providers = $state(null); onMount(async () => { await killLegacyServiceWorker(); @@ -51,7 +60,9 @@ const mapped = hashUrlToPath(location.hash); if (mapped) await goto(resolve(mapped as Pathname), { replaceState: true }); } - await session.load(); + // Parallel — providers is a public endpoint independent of session state. + const [, prov] = await Promise.all([session.load(), getOidcProviders()]); + providers = prov; ready = true; }); @@ -60,9 +71,18 @@ $effect(() => { if (!ready) return; const path = page.url.pathname; - if (!session.isAuthenticated && !isPublic(path)) { - void goto(resolve(`/login?redirect=${encodeURIComponent(path)}`), { replaceState: true }); + if (session.isAuthenticated || isPublic(path)) return; + + // SSO-only auto-redirect: mirror what the server-side /login + // middleware does for direct HTTP loads. Full-page navigation + // (`window.location`) so we hit the OxiCloud backend fresh — that + // endpoint 307s to the IdP with a fresh state + PKCE challenge. + // `goto()` would keep us in the SPA and never leave. + if (providers?.auto_redirect_to_oidc && providers.authorize_endpoint) { + window.location.replace(providers.authorize_endpoint); + return; } + void goto(resolve(`/login?redirect=${encodeURIComponent(path)}`), { replaceState: true }); });