Noor Fatima
Noor Fatima

Reputation: 87

Cookie Handle Error while authenticating users using supabase/ssr via Google OAuth Providers

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

Answers (2)

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

Derek Williams
Derek Williams

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

Related Questions