From a457c1770e4201b78cefbe0c6ad7086f2e9409e4 Mon Sep 17 00:00:00 2001 From: andreawoe Date: Mon, 24 Aug 2026 17:59:42 +0200 Subject: [PATCH] fix(auth): guard OAuth callback against double token exchange --- app/(main)/[locale]/auth/callback/page.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/(main)/[locale]/auth/callback/page.tsx b/app/(main)/[locale]/auth/callback/page.tsx index d994fda8..343817ff 100644 --- a/app/(main)/[locale]/auth/callback/page.tsx +++ b/app/(main)/[locale]/auth/callback/page.tsx @@ -9,6 +9,11 @@ import { Loader2, AlertCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useParams } from "next/navigation"; +// Module-level guard so a Suspense/search-params remount of this client +// component can't exchange the same OAuth code twice — Keycloak rejects a +// reused code with `invalid_grant` ("Code not valid") and the login fails. +const processedAuthCodes = new Set(); + function OAuthCallbackInner() { const router = useRouter(); const params = useParams(); @@ -32,6 +37,11 @@ function OAuthCallbackInner() { return; } + // Prevent a second token exchange for the same code (remount / double + // effect). Without this, the second exchange fails with "Code not valid". + if (processedAuthCodes.has(code)) return; + processedAuthCodes.add(code); + // Step-up re-auth for device pairing: the QR generator sent the user here // via prompt=login. Don't create a login session — just confirm the fresh // auth (sets the short-lived pairing proof cookie) and bounce back to the