Reputation: 87
I am creating a Nextjs project using Supabase. I am authenticating user using Supabase clients through Google OAuth providers. I followed the exact code of this github repo: https://github.com/SamuelSackey/nextjs-supabase-example
The entire authentication process works well. But when the user has been logged in for some time, after some time, the page does not load and I get the error:
Error: Cookies can only be modified in a Server Action or Route Handler. Read more:
https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-
options
at Object.set (./src/lib/supabase/server-clients.ts:27:73)
at Array.map (<anonymous>)
Here is my server-client.ts
import { type CookieOptions, createServerClient } from '@supabase/ssr';
import { getCookie, setCookie } from 'cookies-next';
import { cookies } from 'next/headers';
import { type NextRequest, type NextResponse } from 'next/server';
import { SUPABASE_ANON_KEY, SUPABASE_URL } from '@/constant/env';
// server component can only get cookies and not set them, hence the "component" check
export function createSupabaseServerClient(component = false) {
return createServerClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
cookies: {
get(name: string) {
return cookies().get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
if (component) return;
cookies().set(name, value, options);
},
remove(name: string, options: CookieOptions) {
if (component) return;
cookies().set(name, '', options);
},
},
});
}
export function createSupabaseServerComponentClient() {
return createSupabaseServerClient(true);
}
export function createSupabaseReqResClient(
req: NextRequest,
res: NextResponse
) {
cookies().getAll(); // Keep cookies in the JS execution context for Next.js build
return createServerClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
cookies: {
get(name: string) {
return getCookie(name, { req, res });
},
set(name: string, value: string, options: CookieOptions) {
setCookie(name, value, { req, res, ...options });
},
remove(name: string, options: CookieOptions) {
setCookie(name, '', { req, res, ...options });
},
},
});
}
middleware.ts
import { type NextRequest, NextResponse } from 'next/server';
import { createSupabaseReqResClient } from '@/lib/supabase/server-clients';
export async function middleware(request: NextRequest) {
const response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createSupabaseReqResClient(request, response);
const {
data: { session },
} = await supabase.auth.getSession();
const user = session?.user;
if (!user && request.nextUrl.pathname.startsWith('/my-profile')) {
return NextResponse.redirect(new URL('/', request.url));
}
return response;
}
export const config = {
matcher: ['/my-profile'],
};
api/auth/callback/route.ts
import { NextResponse } from 'next/server';
import { createSupabaseServerClient } from '@/lib/supabase/server-clients';
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get('code');
// if "next" is in param, use it in the redirect URL
// const next = searchParams.get('next') ?? '/';
if (code) {
const supabase = createSupabaseServerClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(origin);
}
}
// TODO:
// return the user to an error page with instructions
return NextResponse.redirect(`${origin}/auth/auth-error`);
}
Why do I get this error only after some time of user being logged in and not during the initial process of authentication? How do solve this error?
Upvotes: 0
Views: 532
Reputation: 9
I have got a solution for this.
// server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))
}
catch (error) {
// This can be ignored if the middleware is refreshing the user session.
}
},
},
}
)
}
When the user session expires middleware will be refreshing the session and hence the setAll(cookiesToSet)
can be ignored. This will solve the issue
Upvotes: 0
Reputation: 21
I'm wondering if the SamuelSackey code has an error, in
lib/supabase/server-client the code is not spreading the options.
See lines 18 and 22 from the repo (https://github.com/SamuelSackey/nextjs-supabase-example/blob/main/src/lib/supabase/server-client.ts)
set(name: string, value: string, options: CookieOptions) {
if (component) return;
cookies().set(name, value, options);
},
remove(name: string, options: CookieOptions) {
if (component) return;
cookies().set(name, "", options);
},
The Supabase SSR docs has the following
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: '', ...options })
},
For example (on the Supabase SSR docs) https://supabase.com/docs/guides/auth/server-side/creating-a-client?environment=route-handler
His middleware code also looks abbreviated, here are the supabase docs for middleware
https://supabase.com/docs/guides/auth/server-side/creating-a-client?environment=middleware, note the get, set, and remove code is missing from his middleware.ts. When calling the ssr CreateServerClient, the third argument is the cooking handling (cookies and cookieOptions),
declare function createServerClient<Database = any, SchemaName extends string & keyof Database = 'public' extends keyof Database ? 'public' : string & keyof Database, Schema extends GenericSchema = Database[SchemaName] extends GenericSchema ? Database[SchemaName] : any>(supabaseUrl: string, supabaseKey: string, options: SupabaseClientOptions<SchemaName> & {
cookies: CookieMethods;
cookieOptions?: CookieOptionsWithName;
}): _supabase_supabase_js.SupabaseClient<Database, SchemaName, Schema>;
Edit: also found this on the Supabase Social Login documents ( https://supabase.com/docs/guides/auth/social-login/auth-google)
Google does not send out a refresh token by default, so you will need to pass parameters like these to signInWithOAuth() in order to extract the provider_refresh_token:
Upvotes: 0