feat(oidc): reduce amount of page when auto_redirect_to_oidc

This commit is contained in:
Edouard Vanbelle
2026-08-03 23:00:15 +02:00
parent d065f99952
commit 95104904c4
2 changed files with 37 additions and 3 deletions
+14
View File
@@ -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. */
+23 -3
View File
@@ -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<OidcProviders | null>(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 });
});
</script>