auth-helpers: invalid request: both auth code and code verifier should be non-empty

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 calling auth.exchangeCodeForSession(code) this error is thrown:

AuthApiError: invalid request: both auth code and code verifier should be non-empty
    at /Users/k/git/myrepo/node_modules/@supabase/gotrue-js/dist/main/lib/fetch.js:41:20
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  __isAuthError: true,
  status: 400

To Reproduce

Login callback handler:

const code = event.url.searchParams.get('code')
if (typeof code === 'string') {
  const { data, error } = await event.locals.supabase.auth.exchangeCodeForSession(code)
}

This is how I’m starting the login:

const supabaseAnonPkce = createClient<Database>(
	PUBLIC_SUPABASE_URL,
	PUBLIC_SUPABASE_ANON_KEY,
	{
		auth: {
			flowType: 'pkce',
		},
	}
)
const { error } = await supabaseAnonPkce.auth.signInWithOtp({
	email,
})

Expected behavior

  • It seems like an unexpected error since it mentions a “code verifier”, so I’m guessing it’s not supposed to happen
  • Errors should be returned in the error property, not thrown.

Screenshots

If applicable, add screenshots to help explain your problem.

System information

  • OS: macOS
  • Browser: Brave 1.51.114, Chromium 113.0.5672.92
  • Version of supabase-js: 2.21.0
  • Version of Node.js: 18.15.0

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 32
  • Comments: 63 (15 by maintainers)

Most upvoted comments

I’m still facing the issue. Followed the documentation https://supabase.com/docs/guides/auth/auth-helpers/sveltekit#server-side and I’m trying server side authentication.

Supabase auth logs are showing the same:

{“component”:“api”,“error”:“400: invalid request: both auth code and code verifier should be non-empty”,“level”:“info”,“method”:“POST”,“msg”:“400: invalid request: both auth code and code verifier should be non-empty”,“path”:“/token”,“referer”:“”,“remote_addr”:“<ip-addr>”,“time”:“2023-07-09T11:21:32Z”,“timestamp”:“2023-07-09T11:21:32Z”}

I played with this in the debugger and found a reliable repro. The blog post also confirms the following flow:

  1. User clicks “sign in with magic link”, Supabase signInWithOTP gets called with a redirect, usually the project’s /auth/callback
  2. Supabase generates a code verifier, saves it in a cookie called sb-<project ID>-auth-token-code-verifier
  3. User clicks the magic link in the email, gets redirected to /auth/callback with the code query param
  4. Supabase checks the code from the query param against the code verifier in the cookie
  5. If the user clicks the link from another client, the code verifier cookie won’t be found and causes the error thrown here

“Another client” cold mean an incognito window, another browser, another device, etc. E.g. someone initiated sign in from the browser on a desktop, then open the magic link from a mobile email app. Or initiated from the main browser app, but continued in a webview with a separate cookies store.

If I understand correctly, the code verifier shouldn’t leave the client (that’s the whole point of PKCE). So the fix here would just be failing gracefully from Supabase/GoTrue, and let the project display a recovery path. Something shorter than

Looks like you requested sign in from another device.

Open this page in Chrome on Windows
– or –
Sign in with another magic link on this iPhone

Workaround for now is to catch it (as it’s not returned via result.error)

Edit: Example with screenshots and code added here: https://github.com/supabase/auth-helpers/issues/545#issuecomment-1666960329

I am also seeing this with using google oauth. Our initiation of the auth flow is on the client and I am seeing this error on the server, not sure if that is important.

Issue for me is with email confirmation flow not oauth. Works fine locally. Should point out, it seems to actually confirm the email but throws an error.

This is happening on Vercel and it’s kicking out:

- error AuthApiError: invalid request: both auth code and code verifier should be non-empty
    at /var/task/.next/server/chunks/928.js:3374:24
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  __isAuthError: true,
  status: 400
}

For now, I have worked around this simply by suppressing the offending function. This obviously isn’t an ideal situation though:

try {
    await supabase.auth.exchangeCodeForSession(code)
} catch (error) {}

Looks like the problem still continues for signInWithOAuth in nextjs@13.4.19. @silentworks
with the same code not getting any error if i use nextjs@13.4.7

error AuthApiError: invalid flow state, no valid flow state found

client component

const signInWithGithub = async () => {
    await supabase.auth.signInWithOAuth({
      provider: "github",
      options: {
        redirectTo: window.location.origin + "/auth/callback",
      },
    });
  };

app/auth/callback/route.js

import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";

export async function GET(request) {
  const requestUrl = new URL(request.url);
  const code = requestUrl.searchParams.get("code");

  if (code) {
    const supabase = createRouteHandlerClient({ cookies });
    await supabase.auth.exchangeCodeForSession(code);
  }

  return NextResponse.redirect(requestUrl.origin);
}

I ran into the same issue, I am using Supabase+Next.js auth helpers. The code:

// invite_user.ts

import { NextResponse } from 'next/server'
import { signInWithMagicLink } from '@/utils/supabase-queries'
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'

export async function GET(request: Request) {
    const supabase = createRouteHandlerClient<Database>({ cookies })

    const { searchParams } = new URL(request.url)
    const email = searchParams.get('email')

    if (email) {
        await signInWithMagicLink(supabase, email)
        return NextResponse.json('Invite sent!')
    }

    return NextResponse.json('No email provided')
}
// supabase-queries.ts
import { SupabaseClient } from '@supabase/supabase-js'
import { toSiteURL } from './utils'
import { errors } from './errors'

export const signInWithMagicLink = async (
    supabase: SupabaseClient<Database>,
    email: string
) => {
    const { error } = await supabase.auth.signInWithOtp({
        email,
        options: {
            emailRedirectTo: toSiteURL('/auth/callback'),
        },
    })

    if (error) {
        errors.add(error.message)
        throw error
    }
}
// /auth/callback/route.ts

import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export const dynamic = 'force-dynamic'

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

    console.log('requestUrl', request.url)
    console.log('code', code)

    if (code) {
        try {
            const supabase = createRouteHandlerClient<Database>({ cookies })
            await supabase.auth.exchangeCodeForSession(code)
        } catch (error) {
            console.error('Failed to exchange code for session: ', error)
        }
    }

    const redirectTo = new URL('/required-session', requestUrl.origin)

    return NextResponse.redirect(redirectTo)
}

I am getting the code correctly from Supabase API:

requestUrl: http://localhost:3000/auth/callback?code=1aeb35ec-029b-494c-88d3-6ce887d86035
code: 1aeb35ec-029b-494c-88d3-6ce887d86035

But there is the same error:

Failed to exchange code for session:  AuthApiError: invalid request: both auth code and code verifier should be non-empty

I have the same issue but with social login did you find a solution? I’m using astro SSR btw.

Nope. Still struggling with this… 😦

Here’s a possible flow. More code but better UX imo.

No client JS needed in this example. Should work with SSR and SSG.

As mentioned above, missing cookie/clicking from a different device is a normal case that should be handled imo.

Screenshots here. Code below.

Flow

  1. Present login options
    Login in form with 'Login with magic link' button

  2. Show a quick tip after sending magic link
    Landing page shown after logging in via magic link with message 'Magic link sent. Make sure to click the link from this device. Why? You can only login with a magic link from the same device you requested it from. This is to prevent bad guys from stealing the link from your email.'

  3. Green path: home page; missing cookie (this issue): show recovery options. Optionally, show other ways to login, e.g. password
    Login page after clicking a magic link from a different device with the message 'Looks like you requested this magic link from a different device. No worries. You can try again from this device.'

  4. Bonus (unrelated to this issue). If the link has been clicked or expired, return to login page with a message:
    Login page after clicking an expired magic link with the message 'Looks like the magic link has expired. No worries. You can try again here.'

These messages can probably be improved if you work with a design team.

Code

  1. Update your login page support the above layouts. For example, use a type query param with these values:

    1. diff_device
    2. bad_code
    3. sent_magic_link (optional)
  2. Update the emailRedirectTo field when handling the “Sign in with magic link” button click. You can do this on the client or the server.

    const urlSafeEmail = encodeURIComponent(email);
    await getServerActionSupabase().auth.signInWithOtp({
      email,
      options: {
        emailRedirectTo: `${YOUR_BASE_URL}/auth/callback/${urlSafeEmail}`,
      },
    });
    
    // Show the "magic link sent" layout here. Options:
    // Server or client: redirect to `/YOUR_LOGIN_PATH?type=sent_magic_link&email=${urlSafeEmail}`
    // Client: You can also use JS to show the message
    
  3. Example route handler in Next.js:

    // src/app/auth/callback/[email]/route.ts
    import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
    import { cookies } from "next/headers";
    import { NextResponse } from "next/server";
    
    import type { NextRequest } from "next/server";
    import { isAuthApiError } from "@supabase/supabase-js";
    
    type Props = {
      params: { email: string };
    };
    
    export async function GET(request: NextRequest, { params }: Props) {
      const requestUrl = new URL(request.url);
      const code = requestUrl.searchParams.get("code");
    
      if (!code) {
        return redirectBadCode();
      }
    
      const supabase = createRouteHandlerClient({ cookies });
    
      try {
        const { data } = await supabase.auth.exchangeCodeForSession(code);
        if (data.session) {
          return NextResponse.redirect(`${requestUrl.origin}/YOUR_HOME_PATH`");
        } else {
          return redirectBadCode();
        }
    
        /* https://github.com/supabase/auth-helpers/issues/545
         * When fixed, rm catch and use the error returned from exchangeCodeForSession(…)
         */
      } catch (error) {
        if (isAuthApiError(error)) {
          return redirectDiffDevice();
        }
    
        // If you, too, like living dangerously:
        throw error;
      }
    
      function redirectBadCode() {
        const searchParams = encodeSearchParams({
          email: params.email,
          type: "bad_code",
        });
    
        return NextResponse.redirect(`${requestUrl.origin}/YOUR_LOGIN_PATH?${searchParams}`);
      }
    
      function redirectDiffDevice() {
        const searchParams = encodeSearchParams({
          email: params.email,
          type: "diff_device",
        });
    
        return NextResponse.redirect(`${requestUrl.origin}/YOUR_LOGIN_PATH?${searchParams}`);
      }
    }
    
    function encodeSearchParams(params: Record<string, string>) {
      return new URLSearchParams(params).toString();
    }
    

Bit more complicated to maintain but worth it for the overall UX imo.

Should work with other SSR/SSG frameworks too since the above doesn’t need any client-side JS.

Share here if you found better ways 🙌

@silentworks To avoid confusing the original issue here, I’ve created https://github.com/supabase/auth-helpers/issues/549. The issue seems to be related to exchangeCodeForSession expecting user_id to be set in the flow state. It’s unclear to me how that would be, since this is the initial login, so no user has been created yet.

I am running into this with createRouteHandlerSupabaseClient (which is the same as createServerComponentSupabaseClient) that only takes headers and cookies, maybe something is lacking there?

I’m going to close this issue again until someone actually provides an reproducible example repo as I’ve showed in my recording and examples I’ve linked. This is working but folks keep on adding on here I have the same issue without any reproducible examples.

@edgarasben but signInWithOAuth doesn’t use email templates. You would still just have both endpoints in your project.

i’m also experiencing this error when i try to use the remix auth helpers following the official guide here.

using:

  • node 18.14.0
  • npm 9.4.2
  • remix 1.18.1
  • supabase js 2.26.0
  • supabase remix auth helpers 0.2.1
  • supabaseClient.auth.signInWithOtp

with auth email settings:

  • confirm email: off
  • secure email change: off
  • secure password change: off

auth works fine out of the box with this remix stack, but i’d like to switch to the official remix helpers.

Turns out I need to upgrade to the latest version of Node 18.x: https://github.com/vercel/next.js/issues/52209#issuecomment-1621889571

The issue in the code seems to be the use of two different clients. You are using the auth helpers along with the normal supabase client, this won’t work and will result in the error you are getting.

@silentworks, Is this mentioned anywhere in the docs? I spent pretty much all day today trying to get OAuth to work in SSR mode on a NextJS project with a refine.dev template and coming across this information earlier would have been incredibly helpful!

Also, the naming is rather misleading - auth-helpers strikes more as an add-on to supabase-js and not so much a replacement.

Is there a way to use PKCE easily with Cloudflare Workers? We’ve been using createClient (from @supabase/supabase-js) and it’s been working great, until trying to add PKCE.

const supabase_client = createClient(c.env.SUPABASE_URL, c.env.SUPABASE_KEY, {
  auth: {
    persistSession: false,
    flowType: 'pkce'
  }
})
const token_test = 'asdfasdf-asdfasdf-adfsadf-asdfsdf' // We get the actual token from our URL manually elsewhere
const wat = await supabase_client.auth.exchangeCodeForSession(token_test);
AuthApiError: invalid request: both auth code and code verifier should be non-empty
    at index.js:12757:18 {
  __isAuthError: true,
  name: AuthApiError,
  status: 400,
  stack: AuthApiError: invalid request: both auth code and …fier should be non-empty
    at index.js:12757:18,
  message: invalid request: both auth code and code verifier should be non-empty
}

@probablykasper I’m going to make a big ask of you, but can you create a minimal reproducible example repo that I can take a look at please?

The issue in the code seems to be the use of two different clients. You are using the auth helpers along with the normal supabase client, this won’t work and will result in the error you are getting. We have a branch of the auth-helpers with PKCE support that you should use until we make the final release. You can install this using:

npm install @supabase/auth-helpers-sveltekit@next

You shouldn’t mix the auth-helpers client with a normal supabase-js client unless you are planning to do things with the service_role key. There is an example project using these at the moment in this repo https://github.com/supabase-community/supabase-by-example/tree/next/magic-link/sveltekit. We will be releasing the next version of the auth-helpers with full support soon. Currently we are just finalising the documentation for release.