You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 15中Supabase Google OAuth PKCE报错:auth code与code verifier不能为空

Next.js 15 + Supabase Google OAuth PKCE 认证错误:"invalid request: both auth code and code verifier should be non-empty"

我在Next.js 15应用中集成Supabase Google OAuth PKCE认证时遇到持续问题,多次尝试修复后,OAuth重定向后仍收到错误:"invalid request: both auth code and code verifier should be non-empty"。

客户端OAuth初始化代码

const handleGoogleSignIn = async () => {
  const redirectTo = `${window.location.origin}/auth/callback/student`;

  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      redirectTo,
      queryParams: {
        prompt: 'consent',
        access_type: 'offline'
      }
    },
  });

  if (data?.url) {
    window.location.href = data.url;
  }
};

Supabase客户端配置

export const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    flowType: 'pkce',
    storage: typeof window !== 'undefined' ? window.localStorage : undefined,
    storageKey: 'sb-auth-token',
  },
})

OAuth回调处理器

export async function GET(request: NextRequest, { params }: RouteParams) {
  const code = requestUrl.searchParams.get('code');
  
  if (code && !type) {
    // DEBUG: Log all cookies
    console.log('All cookies:', cookieStore.getAll().map(c => c.name));

    // Manually extract code verifier cookie
    const codeVerifierCookie = cookieStore.get('sb-auth-token-code-verifier');
    console.log('Code verifier cookie exists:', !!codeVerifierCookie);
    
    if (!codeVerifierCookie?.value) {
      // Redirect with error
      return NextResponse.redirect(new URL(`${loginRoute}?error=Code verifier not found`, requestUrl.origin));
    }

    // Decode the code verifier (Supabase double-encodes it)
    let decodedVerifier: string;
    const rawValue = codeVerifierCookie.value;
    
    if (rawValue.startsWith('base64-')) {
      decodedVerifier = JSON.parse(atob(rawValue.replace('base64-', '')));
    } else {
      decodedVerifier = rawValue;
    }

    // Manual token exchange
    const tokenResponse = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=pkce`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'apikey': supabaseAnonKey,
        'Authorization': `Bearer ${supabaseAnonKey}`,
      },
      body: JSON.stringify({
        auth_code: code,
        code_verifier: decodedVerifier,
      }),
    });

    if (!tokenResponse.ok) {
      const errorText = await tokenResponse.text();
      console.error('Token exchange failed:', tokenResponse.status, errorText);
      // Handle error...
    }

    const tokenData = await tokenResponse.json();
    
    // Set session and redirect...
  }
}

中间件配置片段

const isPublicPage = 
  req.nextUrl.pathname === ROUTES.HOME || 
  req.nextUrl

错误流程详情

  • 用户点击“Continue with Google”
  • 跳转到Google OAuth授权页面
  • 授权完成后,重定向至/auth/callback/student?code=...&scope=...
  • 服务器日志显示:"Token exchange failed: 400 {"error":"invalid_request","error_description":"both auth code and code verifier should be non-empty"}"

排查与修复方案

1. 避免手动处理PKCE流程(核心修复)

Supabase SDK已内置完整PKCE流程处理,手动提取code verifier并调用token接口极易出错。直接使用官方auth.exchangeCodeForSession方法替代手动请求:

export async function GET(request: NextRequest) {
  const url = new URL(request.url);
  const code = url.searchParams.get('code');

  if (code) {
    const supabase = createServerClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
      {
        cookies: {
          getAll: () => request.cookies.getAll(),
          set: (name, value, options) => request.cookies.set(name, value, options),
          remove: (name, options) => request.cookies.delete(name, options),
        },
      }
    );

    const { data, error } = await supabase.auth.exchangeCodeForSession(code);

    if (error) {
      console.error('Session exchange failed:', error);
      return NextResponse.redirect(new URL('/login?error=auth_failed', url.origin));
    }

    return NextResponse.redirect(new URL('/dashboard', url.origin));
  }

  return NextResponse.redirect(new URL('/login', url.origin));
}

2. 修正中间件白名单配置

补全isPublicPage判断,将回调路由加入白名单,避免中间件拦截认证Cookie:

const isPublicPage = 
  req.nextUrl.pathname === ROUTES.HOME || 
  req.nextUrl.pathname.startsWith('/auth/callback');

3. 验证Cookie访问权限

确保sb-auth-token-code-verifier Cookie的路径配置正确,回调路由/auth/callback/student必须能读取到该Cookie。若使用子域名,需检查Cookie的domain参数是否匹配。

4. 统一客户端与服务端配置

  • 客户端和服务端的Supabase配置需保持一致,服务端创建createServerClient时无需额外指定flowType,SDK会自动适配PKCE流程。
  • 确认Supabase控制台中设置的授权回调URL与代码中redirectTo参数完全一致(包含协议、域名、完整路径)。

5. 修正手动解码逻辑(若坚持自定义流程)

Supabase存储的code verifier是原始字符串,无需base64解码,错误解码会导致verifier无效:

// 移除多余解码步骤,直接使用Cookie值
const decodedVerifier = codeVerifierCookie.value;

内容的提问来源于stack exchange,提问作者Saaif Suleman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.11 18:42:37