From bb61d21c83bdaf1c21117b53a0a8b8043bdd36f4 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sun, 7 Jun 2026 15:57:17 -0400 Subject: [PATCH] fix(pwa): gate events query on auth to stop OIDC state-cookie churn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Internal Server Error after returning from Authelia: processOAuthCallback threw OAUTH_INVALID_RESPONSE ("unexpected state parameter") because the OIDC state cookie no longer matched the state returned to /callback. Root cause: eventsQuery (fetchEvents, redirect:'follow', retry:2) ran concurrently with fetchMe on load. While unauthenticated, every /api/* request hits the OIDC guard, which 302-redirects to Authelia AND sets a fresh state cookie. fetchEvents could not follow the cross-origin redirect, so React Query retried it up to 3x over ~3s — each retry overwriting the state cookie mid-login, racing the single /api/login navigation that owns the real flow. Fix: enabled: meQuery.isSuccess. Only fetchMe (redirect:'manual', retry:false) touches a guarded endpoint while unauthenticated, so the top-level /api/login navigation owns the state cookie uncontested. Realizes the documented design intent that only fetchMe drives the login redirect. --- apps/pwa/src/components/CalendarShell.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/pwa/src/components/CalendarShell.tsx b/apps/pwa/src/components/CalendarShell.tsx index 3f4ef37..8e1d6d1 100644 --- a/apps/pwa/src/components/CalendarShell.tsx +++ b/apps/pwa/src/components/CalendarShell.tsx @@ -93,10 +93,22 @@ export function CalendarShell() { staleTime: 5 * 60 * 1000, }) - // Fetch windowed occurrences — key includes start/end so navigation refetches + // Fetch windowed occurrences — key includes start/end so navigation refetches. + // + // enabled: meQuery.isSuccess is load-bearing for the OIDC login flow, not just + // an optimization. When unauthenticated, every /api/* request hits the OIDC + // guard, which 302-redirects to Authelia AND sets a fresh state cookie. If this + // query ran concurrently with fetchMe (and retried), each /api/events redirect + // would overwrite the OIDC state cookie mid-login — so the state returned to + // /callback no longer matched the cookie, producing OAUTH_INVALID_RESPONSE + // ("unexpected state parameter") and an Internal Server Error after Authelia. + // Gating on a successful /api/me means only fetchMe (redirect:'manual', + // retry:false) touches a guarded endpoint while unauthenticated, so the single + // top-level /api/login navigation owns the state cookie uncontested. const eventsQuery = useQuery({ queryKey: ['events', start, end], queryFn: () => fetchEvents(start, end), + enabled: meQuery.isSuccess, retry: 2, staleTime: 5 * 60 * 1000, })