Site name: account.vistamusic.com
I’m having some issue with the PKCE flow in production and deploy preview, it works for a few hours after a fresh build but it breaks afterwards until I deploy a new build.
The browser cookies supabase-{project ID}-auth-token
and supabase-{project ID}-auth-token-verifier
are being set and updated properly.
When it breaks, some issues start to happen:
- Session isn’t created or persisted, redirected user back to
/login
page due to protected page, the redirect to/error
on the/auth/confirm
route handler did not execute, so I’m assuming theverifyOtp()
has been executed without any issue - The email link works but it is served with data belonging to different users
- The supabase
/verify
request does not appear on the supabase authentication logs
All of my routes, api route handler are dynamic based on the info printed out by next build --debug
, no Invalid or expired token
logged on the supabase authentication logs.
// /auth/confirm route handler
import { NextResponse } from 'next/server';
import { supabaseServerClient } from '@/lib/supabase/supabaseServerClient';
export const dynamic = 'force-dynamic';
export async function GET(request) {
const { searchParams } = new URL(request.url);
const token_hash = searchParams.get('token_hash');
const type = searchParams.get('type');
const redirectUrl = searchParams.get('redirectUrl');
const next = redirectUrl ? new URL(redirectUrl).pathname : '/';
const redirectTo = request.nextUrl.clone();
redirectTo.searchParams.delete('token_hash');
redirectTo.searchParams.delete('type');
redirectTo.searchParams.delete('redirectUrl');
if (token_hash && type) {
const supabase = await supabaseServerClient();
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
});
if (error) {
redirectTo.pathname = '/error';
redirectTo.searchParams.set('code', error.status.toString());
redirectTo.searchParams.set('message', error.message);
} else {
redirectTo.pathname = next;
}
return NextResponse.redirect(redirectTo);
}
redirectTo.pathname = '/error';
redirectTo.searchParams.set('code', '422');
redirectTo.searchParams.set('message', 'Missing OTP verification parameters in the link');
return NextResponse.redirect(redirectTo);
}
// Middleware
import { NextResponse } from 'next/server';
import { createServerClient } from '@supabase/ssr';
const unprotectedRoutes = ['/login', '/signup', '/reset-password'];
export async function updateSession(request) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, {
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
response = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options));
},
},
});
const user = await supabase.auth.getUser();
const isAuthApiRoute = request.nextUrl.pathname.startsWith('/auth');
const isUnprotectedRoute = unprotectedRoutes.some((route) => request.nextUrl.pathname.startsWith(route));
// When the user is logged in but tries to access public route
if (!user.error && !isAuthApiRoute && isUnprotectedRoute) {
return NextResponse.redirect(new URL('/', request.url));
}
return response;
}
// I'm using this function as a data access layer to protect server actions and pages
import { cache } from 'react';
import { supabaseServerClient } from '@/lib/supabase/supabaseServerClient';
export const verifySession = cache(async function () {
const supabase = await supabaseServerClient();
const { data, error } = await supabase.auth.getUser();
if (error) {
console.info(
`Error retrieving current user details; Code: ${error.code}, name: ${error.name}, message: ${error.message}`
);
}
return error || !data?.user
? null
: {
email: data.user.email,
id: data.user.id,
};
});