-
Notifications
You must be signed in to change notification settings - Fork 506
Description
Bug report
- I confirm this is a bug with Supabase, not with my own application.
- I confirm I have searched the Docs, GitHub Discussions, and Discord.
Describe the bug
When running Supabase self-hosted (docker-compose) and using the PKCE OAuth flow (signInWithOAuth
with Google), the code verifier is not persisted, resulting in an empty code_verifier
being sent to /auth/v1/token?grant_type=pkce
. This causes the callback to fail with:
"AuthApiError: invalid request: both auth code and code verifier should be non-empty"
This only happens when running the app in Docker Compose with Kong as a proxy. The same codebase and Login via Google works as expected when running locally without Docker Compose (i.e., with direct access to the Supabase services).
To Reproduce
- Clone a Next.js app using Supabase Auth (with
@supabase/ssr
) - Set up Supabase self-hosted via docker-compose with Kong as the proxy, following the official setup
- Configure OAuth (Google), using
signInWithOAuth({ provider: "google", options: { redirectTo: ... } })
- Complete the OAuth flow in the browser
- Observe the request to
/auth/v1/token?grant_type=pkce
(via Kong), wherecode_verifier
is empty
src/auth/page.tsx:
"use client";
import { supabase } from "@/lib/supabase/browserClient";
...
const handleGoogleLogin = async () => {
// redirect URL here is http://localhost:3000/auth/callback and the user is successfully redirected to the callback (see underneath)
const { error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
});
if (error) {
setError(error.message);
}
};
...
src/auth/callback/page.tsx:
"use client";
import { useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { supabase } from "@/lib/supabase/browserClient";
export default function AuthCallbackPage() {
const router = useRouter();
const searchParams = useSearchParams();
useEffect(() => {
const code = searchParams?.get("code");
if (code) {
supabase.auth.exchangeCodeForSession(code).then(({ data, error }) => {
if (!error) {
router.replace("/app");
} else {
router.replace("/auth?error=oauth");
}
});
}
}, [searchParams, router]);
return (
<div>Signing you in...</div>
);
}
@/lib/supabase/browserClient:
import { createBrowserClient } from "@supabase/ssr";
export const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
This issue #21183 seems to be related, but the steps recommended in the comments did not help.
Expected behavior
The PKCE code verifier should be correctly stored and sent in the token exchange, so the OAuth callback succeeds and the user is authenticated.
Screenshots


System information
- OS: macOS (host), Docker Compose (linux containers)
- Browser: Arc Version 1.106.0 (66192), Chromium Engine Version 138.0.7204.185, Also tested and reproduced on Safari 18.5
- Version of supabase-js: 2.52.0, @supabase/ssr: 0.6.1
- Version of Node.js: 20.10.0
Additional context
- All URLs are set correctly in .env (
NEXT_PUBLIC_SITE_URL=http://localhost:3000
,NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000
, internal services useSUPABASE_URL=http://kong:8000
) - The browser receives the correct redirect and code, but the code verifier is missing.
- The user is created in Supabase and its even shown to have an existing sign in
- Clearing cookies, localStorage, and sessionStorage does not help.
I also tried an alternative approach where the callback is on the server-side:
src/auth/callback/route.ts:
import { NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/serverClient";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
let next = searchParams.get("next") ?? "/app";
if (!next.startsWith("/")) next = "/";
if (code) {
const supabase = await createClient();
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(`${origin}${next}`);
}
}
return NextResponse.redirect(`${origin}/auth`);
}
This does not help and the issue happens here as well.