fix(oidc): make SSO callback idempotent + evict stale legacy service worker (#510)
OIDC SSO login intermittently ended on a 403 "Invalid or expired OIDC state — possible CSRF attack" even though the login had already succeeded server-side. Root cause: the (now-removed) legacy vanilla-JS frontend registered a `/sw.js` service worker that, with navigation preload enabled, double-fetched the top-level navigation to `/api/auth/oidc/callback`. The OIDC `state` is single-use, so the first callback consumed it and logged the user in while the duplicate (~0.4s later) found the state gone and returned the 403 the browser rendered. Backend — idempotent callback: after a successful web login, remember `state -> exchange_code` in a short-lived (120s) cache. A duplicate callback whose state was already consumed now replays that same redirect instead of 403-ing, returning the cached result directly without re-running the IdP code exchange (the authorization `code` is single-use too). Keyed by the unguessable 32-byte state, so it adds no new attack surface and fixes the 403 for everyone — including browsers still running a stale legacy service worker. Frontend — evict the stale worker: the current SvelteKit app registers no service worker, so fresh clients can't double-fire. But a browser that previously loaded the legacy frontend still has `/sw.js` registered and controlling pages (and `/sw.js` now 404s, so vendor self-cleanup is inconsistent). killLegacyServiceWorker() runs first in the root layout's onMount: it surgically unregisters only `/sw.js` workers, drops only the legacy `oxicloud-cache-*` caches, and reloads once (guarded). Fixes #510. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
import { session } from '$lib/stores/session.svelte';
|
||||
import { ui } from '$lib/stores/ui.svelte';
|
||||
import { hashUrlToPath } from '$lib/utils/hashRedirect';
|
||||
import { killLegacyServiceWorker } from '$lib/utils/killLegacyServiceWorker';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
@@ -33,6 +34,8 @@
|
||||
let ready = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
await killLegacyServiceWorker();
|
||||
|
||||
// The instant HTML boot splash has done its job — the app is mounted, so
|
||||
// the route (login renders immediately; protected routes show their own
|
||||
// loading state) is already in the DOM behind it.
|
||||
|
||||
Reference in New Issue
Block a user