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": "[email protected]"
}
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