Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 37 additions & 19 deletions app/auth/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,33 @@ import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get('code');
const next = searchParams.get('next') ?? '/';

// GoTrue redirects here with error params when OAuth fails server-side
// Reject non-relative paths (including //host bypasses) to prevent open-redirect.
const rawNext = searchParams.get('next') ?? '/';
const next = rawNext.startsWith('/') && !rawNext.startsWith('//') ? rawNext : '/';

// GoTrue redirects here with error params when OAuth fails server-side.
const oauthError = searchParams.get('error');
const oauthErrorDescription = searchParams.get('error_description');
if (oauthError) {
const params = new URLSearchParams({ error: oauthError, ...(oauthErrorDescription ? { error_description: oauthErrorDescription } : {}) });
const params = new URLSearchParams({ error: oauthError });
const desc = searchParams.get('error_description');
if (desc) params.set('error_description', desc);
return NextResponse.redirect(`${origin}/?${params}`);
}

// cookieStore is needed for both the success and error paths below.
const cookieStore = await cookies();

if (code) {
// Buffer cookies from setAll() so we can explicitly stamp them onto the
// redirect response. Next.js does not propagate cookies() mutations into
// NextResponse.redirect() automatically — omitting this loses the session.
const pendingCookies: Array<{
name: string;
value: string;
options: Record<string, unknown>;
}> = [];

const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
Expand All @@ -27,13 +41,7 @@ export async function GET(request: Request) {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// Handle cookie errors
}
pendingCookies.push(...cookiesToSet);
},
},
}
Expand All @@ -42,17 +50,27 @@ export async function GET(request: Request) {
const { error } = await supabase.auth.exchangeCodeForSession(code);

if (!error) {
return NextResponse.redirect(`${origin}${next}`);
const response = NextResponse.redirect(`${origin}${next}`);
for (const { name, value, options } of pendingCookies) {
response.cookies.set(
name,
value,
options as Parameters<typeof response.cookies.set>[2]
);
}
return response;
}

console.error('[auth/callback] exchangeCodeForSession failed:', error.message);
}

// Return to home with error. Clear only the PKCE code_verifier cookie so the
// next sign-in attempt starts with a fresh verifier. We avoid wiping the
// entire session (all sb-* cookies) to prevent logging out a user who has a
// valid session but hit a transient Supabase error during the exchange.
// Clear the PKCE verifier so the next attempt starts fresh.
// Avoid clearing all sb-* cookies — a valid parallel session should survive.
const errorResponse = NextResponse.redirect(`${origin}/?error=auth_callback_error`);
cookieStore.getAll()
.filter(c => c.name.endsWith('-auth-token-code-verifier'))
.forEach(c => errorResponse.cookies.set(c.name, '', { maxAge: 0, path: '/' }));
for (const c of cookieStore.getAll()) {
if (c.name.endsWith('-auth-token-code-verifier')) {
errorResponse.cookies.set(c.name, '', { maxAge: 0, path: '/' });
}
}
return errorResponse;
}
4 changes: 3 additions & 1 deletion src/components/LoginDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ export default function LoginDrawer({ opened, onClose }: LoginDrawerProps) {
setLoadingProvider(provider);

try {
authContext.signInWithProvider(provider);
await authContext.signInWithProvider(provider);
// On success, signInWithOAuth navigates the browser away immediately,
// so handleClose() is only reached on error paths.
handleClose();
} finally {
// Reset after a delay to allow popup to open
Expand Down
Loading