Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supabase Auth bug exchangeCodeForSession returns "both auth code and code verifier should be non-empty" #1026

Open
2 tasks done
luisfelipeluis49 opened this issue Jan 17, 2025 · 26 comments
Labels
bug Something isn't working

Comments

@luisfelipeluis49
Copy link

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

Architecture:

This is Cross-Platform web-based (SEO focused) project, that was built in CSR due to bad performance using SSR w/ Capacitor framework.

Client: Svelte + Vite + Capacitor

Due to use of vanilla Svelte, to handle navigation our choice was "svelte-routing", for building the app on both web and mobile (iOS and Android) we use Capacitor.

Server: Fastify + Supabase

Our server framework of choice was Fastify, as long w/ Supabase, and thus we need to use the Supabase Auth solutions, what prevent us from using tools like Capacitor Generic OAuth2.


Problem

Following the Supabase guide to implement Google OAuth2, when storing the user session, got an AuthApiError: "invalid request: both auth code and code verifier should be non-empty"


Packages:

// package.json
{
    ...,
    "dependencies": {
        "@fastify/compress": "^8.0.1",
        "@fastify/cookie": "^11.0.2",
        "@fastify/cors": "^10.0.2",
        "@fastify/env": "^5.0.2",
        "@fastify/formbody": "^8.0.2",
        "@fastify/multipart": "^9.0.2",
        "@fastify/static": "^8.0.4",
        "@supabase/ssr": "^0.5.2",
        "@supabase/supabase-js": "^2.47.16",
        "dotenv": "^16.4.7",
        "fastify": "^5.2.1",
        ...
    },
    "packageManager": "yarn@4.5.1"
}

Code

Front:

Google sign-in button:

// AuthFormFooter.svelte

<script>
    // -- IMPORTS

    ...

    // -- FUNCTIONS

    async function signInWithOAuth(
        provider
        )
    {
        ...

        try
        {
            let redirectionToUrl;

            switch ( platform )
            {
                case 'android':
                    redirectionToUrl = 'com.myapp://auth';
                    break;
                case 'ios':
                    redirectionToUrl = 'com.myapp://auth';
                    break;
                default:
                    redirectionToUrl = 'http://localhost:5173/auth';
            }

            let data = await fetchData(
                '/api/auth/open-auth',
                {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify( { provider, redirectionToUrl } )
                }
                );

            if ( data.error )
            {
                console.error( 'Server sign-in error:', data.error );
            }
            else
            {
                if ( data.url )
                {
                    window.location.href = data.url;
                }
                else
                {
                    console.error( 'Server sign-in error:', data );
                }
            }
        }
        catch ( error )
        {
            console.error( errorText, error );
        }
    }
</script>

<div class="auth-modal-socials">
    <div
        class="auth-modal-socials-item"
        on:click={() => signInWithOAuth( 'google' )}>
        <span class="google-logo-icon size-150"></span>
    </div>
</div>

Auth Callback:

// AuthPage.svelte

<script>
    // -- IMPORTS

    import { onMount } from 'svelte';
    import { fetchData } from '$lib/base';
    import { navigate } from 'svelte-routing';

    // -- FUNCTIONS

    async function authCallback(
        code,
        next
        )
    {
        try
        {
            let response = await fetchData(
                '/api/auth/callback',
                {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify( { code } )
                }
                );

            if ( response.success )
            {
                navigate( `/${ next.slice( 1 ) }`, { replace: true } );
            }
            else
            {
                console.error( 'Authentication failed' );
            }
        }
        catch ( error )
        {
            console.error( 'Error during authentication', error );
        }
    }

    onMount(
        async () =>
        {
            let params = new URLSearchParams( window.location.search );
            let code = params.get( 'code' );
            let next = params.get( 'next' ) || '/';
            if ( code )
            {
                await authCallback( code, next );
            }
            else
            {
                console.error( 'No code found in query params' );
            }
        }
        );
</script>

Server:

Supabase client configuration:

// supabase_service.js

class SupabaseService
{
    // -- CONSTRUCTORS

    constructor(
        )
    {
        this.client = null;
    }

    // -- OPERATIONS

    initalizeDatabaseClient(
        request,
        reply
        )
    {
        this.client = createServerClient(
            process.env.SUPABASE_DATABASE_URL,
            process.env.SUPABASE_DATABASE_KEY,
            {
                cookies:
                {
                    getAll()
                    {
                        return parseCookieHeader( request.headers.cookie ?? '' );
                    },
                    setAll( cookiesToSet )
                    {
                        cookiesToSet.forEach(
                            ( { name, value, options } ) =>
                            {
                                let serializedCookie = serializeCookieHeader( name, value, options );

                                reply.header( 'Set-Cookie', serializedCookie );
                            }
                            );
                    }
                },
                auth:
                {
                    flowType: 'pkce'
                }
            }
            );
    }

    // ~~

    getClient(
        request,
        reply
        )
    {
        return this.client;
    }
}

// -- VARIABLES

export let supabaseService
    = new SupabaseService();

Auth controller:

// authentication_controller.js

...

// -- FUNCTIONS

...

// ~~

async function openAuth(
    request,
    reply
    )
{
    reply.header( 'Access-Control-Allow-Credentials', true );
    reply.header( 'Access-Control-Allow-Origin', request.headers.origin );

    let { redirectionToUrl, provider } = request.body;

    try
    {
        let { data, error } = await supabaseService.getClient().auth.signInWithOAuth(
            {
                provider,
                options: { redirectTo: redirectionToUrl }
            }
            );

        if ( data.url )
        {
            let url = data.url;

            return reply.code( 200 ).send( { url } );
        }
        else
        {
            return reply.code( 400 ).send( { error: 'auth-sign-in-failed' } );
        }
    }
    catch ( error )
    {
        return reply.code( 500 ).send(
            {
                error: 'Server error', details: error
            }
            );
    }
}

// ~~

async function authCallback(
    request,
    reply
    )
{
    reply.header( 'Access-Control-Allow-Credentials', true );
    reply.header( 'Access-Control-Allow-Origin', request.headers.origin );

    let code = request.body.code;
    let route = request.body.route ?? '/';

    try
    {
        if ( code )
        {
            let { data, error } =
                await supabaseService.getClient().auth.exchangeCodeForSession( code );

            if ( error )
            {
                return reply.code( 400 ).send(
                    {
                        success: false,
                        error: error.message
                    }
                    );
            }

            return reply.code( 200 ).send(
                {
                    success: true,
                    route
                }
                );
        }
        else
        {
            return reply.code( 400 ).send(
                {
                    success: false,
                    error: 'No code provided'
                }
                );
        }
    }
    catch ( error )
    {
        return reply.code( 500 ).send(
            {
                success: false,
                error: 'Server error', details: error
            }
            );
    }
}

// ~~

...

// -- EXPORT

export
{
    ...,
    openAuth,
    authCallback,
    ...
}

Updated all packages, enabled flow type pkce, implemented getAll and setAll instead of get, set and remove on cookies options. But all for nothing, got the same error and couldn't get the solution to this error


System information

  • OS: Windows 11
  • Browser: Microsoft Edge
  • Version of Node.js: 20.16.0
@luisfelipeluis49 luisfelipeluis49 added the bug Something isn't working label Jan 17, 2025
@LuisBamLu LuisBamLu marked this as a duplicate of #1025 Jan 17, 2025
@qxygene1
Copy link

qxygene1 commented Jan 18, 2025

Is this why redirectTo not working? It only works on base /.

Also trğggers ...supabase.co/auth/v1/user twice.

@j4w8n
Copy link
Contributor

j4w8n commented Jan 18, 2025

Have you logged code to see if it's there? Also, after the error, is there a code verifier cookie?

The exchangeCodeForSession needs access to both of these - by passing in the code and having access to cookies.

@luisfelipeluis49
Copy link
Author

Is this why redirectTo not working? It only works on base /.

Also trğggers ...supabase.co/auth/v1/user twice.

redirectTo works for me though...

@j4w8n yes debugged on client and server, both are getting the code, and sorry wdym code verifier cookie?

@j4w8n
Copy link
Contributor

j4w8n commented Jan 18, 2025

Is this why redirectTo not working? It only works on base /.

Also trğggers ...supabase.co/auth/v1/user twice.

redirectTo works for me though...

@j4w8n yes debugged on client and server, both are getting the code, and sorry wdym code verifier cookie?

@luisfelipeluis49 I see you're using the ssr library, which uses cookies for storage.

With the pkce flows, supabase will create a code verifier and store it as sb-<proj_id>-code-verifier. Then, when you call exchangeCodeForSession, it takes the code you pass in and then checks storage for the code verifier and sends both to the Supabase backend. If either of them aren't there, supabase returns the error you mentioned.

@j4w8n
Copy link
Contributor

j4w8n commented Jan 18, 2025

I should correct myself: the ssr library typically shows using cookies for storage, but you may be passing it a different type of storage (you don't show that code).

UPDATE: yes you show cookie storage, I'm sorry.

@qxygene1
Copy link

Have you logged code to see if it's there? Also, after the error, is there a code verifier cookie?

The exchangeCodeForSession needs access to both of these - by passing in the code and having access to cookies.

I dont use ssr, it is pwa; redirectTo site/auth/dashboard doesnt create a session, without redirectTo it creates session but only to base /

@j4w8n
Copy link
Contributor

j4w8n commented Jan 18, 2025

Have you logged code to see if it's there? Also, after the error, is there a code verifier cookie?

The exchangeCodeForSession needs access to both of these - by passing in the code and having access to cookies.

I dont use ssr, it is pwa; redirectTo site/auth/dashboard doesnt create a session, without redirectTo it creates session but only to base /

@qxygene1 if you're not using the ssr library, you should probably open your own issue so we don't get messages confused.

@luisfelipeluis49
Copy link
Author

I should correct myself: the ssr library typically shows using cookies for storage, but you may be passing it a different type of storage (you don't show that code).

UPDATE: yes you show cookie storage, I'm sorry.

So, no idea what's happening right?

@j4w8n
Copy link
Contributor

j4w8n commented Jan 20, 2025

I should correct myself: the ssr library typically shows using cookies for storage, but you may be passing it a different type of storage (you don't show that code).

UPDATE: yes you show cookie storage, I'm sorry.

So, no idea what's happening right?

@luisfelipeluis49 is the code verifier cookie there?

@luisfelipeluis49
Copy link
Author

luisfelipeluis49 commented Jan 20, 2025

@j4w8n I'm not sure, I know that I'm passing as supposed to be, but I don't know where I can check this

    setAll( cookiesToSet )
    {
        cookiesToSet.forEach(
            ( { name, value, options } ) =>
            {
                let serializedCookie = serializeCookieHeader( name, value, options );

                reply.header( 'Set-Cookie', serializedCookie );
                }
                );
    }

When I tried to see what was on client I added this code to see in the debugger

        let client = supabaseService.getClient();

        let { data, error } = await client.auth.signInWithOAuth(
            {
                provider,
                options: { redirectTo: redirectionToUrl }
            }
            );

        if ( data.url )
        {
            let url = data.url;
            let headers = client;

            return reply.code( 200 ).send( { url, headers } );
        }

And inside of client there was it:

Image

@j4w8n
Copy link
Contributor

j4w8n commented Jan 20, 2025

I'm not sure how to check if you're using mobile. If possible, might try on a desktop browser.

@luisfelipeluis49
Copy link
Author

luisfelipeluis49 commented Jan 20, 2025

@j4w8n I'm testing on desktop chromium based browser (Microsoft Edge), using http://localhost:5173

@j4w8n
Copy link
Contributor

j4w8n commented Jan 20, 2025

Then you should be able to check for the cookie in dev tools - after you try to login.

@luisfelipeluis49
Copy link
Author

luisfelipeluis49 commented Jan 20, 2025

Then you should be able to check for the cookie in dev tools - after you try to login.

Ok so, they should be there after the redirect from google consent screen or before the redirect?

I assumed it is after, so here it is the headers:

Image

@j4w8n
Copy link
Contributor

j4w8n commented Jan 20, 2025

It gets set when you call signInWithOAuth and gets accessed and then deleted when you call exchangeCodeForSession. I don't believe you will ever see it in a header unless you're calling exchangeCodeForSession on the server side.

@luisfelipeluis49
Copy link
Author

It gets set when you call signInWithOAuth and gets accessed and then deleted when you call exchangeCodeForSession. I don't believe you will ever see it in a header unless you're calling exchangeCodeForSession on the server side.

@j4w8n it supposed to set when calling signInWithOAuth, I can see it when debugging on setAll, but I'm not sure if it's there when calling exchangeCodeForSession, it is indeed on server side, do you know how can I check it?

@j4w8n
Copy link
Contributor

j4w8n commented Jan 20, 2025

It gets set when you call signInWithOAuth and gets accessed and then deleted when you call exchangeCodeForSession. I don't believe you will ever see it in a header unless you're calling exchangeCodeForSession on the server side.

@j4w8n it supposed to set when calling signInWithOAuth, I can see it when debugging on setAll, but I'm not sure if it's there when calling exchangeCodeForSession, it is indeed on server side, do you know how can I check it?

Should be in the request headers of your auth callback then. If there is middleware, might check there too.

@luisfelipeluis49
Copy link
Author

Should be in the request headers of your auth callback then. If there is middleware, might check there too.

There's no middleware, and checked here and is not there on head as well:

Image

@luisfelipeluis49
Copy link
Author

And just to show, when on cookiesToSet this code is generated, it just doesn't seem to be stored:

Image

// supabase_service.js

setAll( cookiesToSet )
{
    cookiesToSet.forEach(
        ( { name, value, options } ) =>
        {
            let serializedCookie = serializeCookieHeader( name, value, options );

            reply.header( 'Set-Cookie', serializedCookie );
        }
        );
}

@j4w8n
Copy link
Contributor

j4w8n commented Jan 20, 2025

I'm not sure why it wouldn't be stored, but this is def your issue.

@luisfelipeluis49
Copy link
Author

@j4w8n can you at least say to me what should be inside of storage here in:

// GoTrueClient.ts

async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
  await this.initializePromise

  return this._acquireLock(-1, async () => {
    return this._exchangeCodeForSession(authCode)
  })
}

private async _exchangeCodeForSession(authCode: string): Promise<
  | {
      data: { session: Session; user: User; redirectType: string | null }
      error: null
    }
  | { data: { session: null; user: null; redirectType: null }; error: AuthError }
> {
  const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`)
  const [codeVerifier, redirectType] = ((storageItem ?? '') as string).split('/')

  try {
    const { data, error } = await _request(
      this.fetch,
      'POST',
      `${this.url}/token?grant_type=pkce`,
      {
        headers: this.headers,
        body: {
          auth_code: authCode,
          code_verifier: codeVerifier,
        },
        xform: _sessionResponse,
      }
    )
    await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
    if (error) {
      throw error
    }
    if (!data || !data.session || !data.user) {
      return {
        data: { user: null, session: null, redirectType: null },
        error: new AuthInvalidTokenResponseError(),
      }
    }
    if (data.session) {
      await this._saveSession(data.session)
      await this._notifyAllSubscribers('SIGNED_IN', data.session)
    }
    return { data: { ...data, redirectType: redirectType ?? null }, error }
  } catch (error) {
    if (isAuthError(error)) {
      return { data: { user: null, session: null, redirectType: null }, error }
    }

    throw error
  }
}

I made a workaround to store and pass the key as a header of my request, but it seems to "break" here for me, cause storage it's like this for me:

Image

@j4w8n
Copy link
Contributor

j4w8n commented Jan 20, 2025

The storage is whatever you want it to be. Like I said, with the ssr library people typically use a cookies implementation, but you can use whatever works best for what you're trying to do. I'm not familiar with Capacitor and what it has access to.

@luisfelipeluis49
Copy link
Author

luisfelipeluis49 commented Jan 21, 2025

The storage is whatever you want it to be. Like I said, with the ssr library people typically use a cookies implementation, but you can use whatever works best for what you're trying to do. I'm not familiar with Capacitor and what it has access to.

Capacitor doesn't play a role in here, I only mentioned to avoid solutions like "use SvelteKit", because that isn't an option for my use case. It's a simple CSR website using Svelte and Fastify.

In the end, I found a way to access the storage and pass the auth-token-code-verifier, it's requests in
const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`) at the _exchangeCodeForSession.

Now, my only remaining question, what does it expect it to be? I put the value it passes on cookiesToSet, but it gives me an AuthApiError: code challenge does not match previously saved code verifier

e.g. base64-c2ItY2pzcHdxcWhkcGtqbXF5Y3d4eXMtYXV0aC10b2tlbi1jb2RlLXZlcmlmaWVyPWJhc2U2NC1JbUl5TWpRM1lqTXlZamxoTWpjNU5tVXlZVGRoWkRoaU5tRTBPRGs1TVdSbVpqY3dZek16WTJSak1XUTBObVpqWm1ZeFpUazJNR1U0T0RWalpXUTBaVFkzTnpRMFpHUTFNalUzTnpRMU56QTBNbVJqTW1ZNFpHTTBZbUZpTkRFMVpUUTNNbU0xWmpOa1lqVTFOMlpqTmpjaTsgTWF4LUFnZT0zNDU2MDAwMDsgUGF0aD0vOyBTYW1lU2l0ZT1MYXg

@j4w8n
Copy link
Contributor

j4w8n commented Jan 21, 2025

That format looks correct, but it's definitely unique per call to signInWithOAuth. That error sounds like maybe it's sending the verifier from the previous request??

@j4w8n
Copy link
Contributor

j4w8n commented Jan 21, 2025

I think your getAll is likely what you'd want, but you need the cookie to already be in the request.headers.cookie when it hits the server.

getAll()
{
    return parseCookieHeader( request.headers.cookie ?? '' );
}

What does your Supabase client-creation code look like on the client side?

@luisfelipeluis49
Copy link
Author

That format looks correct, but it's definitely unique per call to signInWithOAuth. That error sounds like maybe it's sending the verifier from the previous request??

Unless I'm triggering this twice, then should be the same.

I think your getAll is likely what you'd want, but you need the cookie to already be in the request.headers.cookie when it hits the server.

getAll()
{
    return parseCookieHeader( request.headers.cookie ?? '' );
}

What does your Supabase client-creation code look like on the client side?

I'm not using any supabase on client only on server side

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants