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

Cookies not setting properly supabase ssr #36

Open
Sof2222 opened this issue Apr 14, 2024 · 32 comments
Open

Cookies not setting properly supabase ssr #36

Sof2222 opened this issue Apr 14, 2024 · 32 comments
Labels
bug Something isn't working

Comments

@Sof2222
Copy link

Sof2222 commented Apr 14, 2024

Bug report

I have already checked and cant see the same issue.

  • [x ] I confirm this is a bug with Supabase, not with my own application.
  • [x ] I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug

I am using supabase-ssr package to log on.

I thought this was only an issue in dev mode as when I ran build mode on Friday it worked, but perhaps I had not properly deleted the cookie when I was testing so am getting the error again now.

Basically the auth-token cookie is not setting properly. If I log on twice, it sets but the first time i log on only sb-__-auth-token-code-verifier is set.
I am unsure if it is something on my side which is causing the error or if there is something timing out in the setting of the second cookie. My code is below.
Note I am using a otp sent to emails for this.

To Reproduce

This is to get the code:

export async function signuplogin(prevState: any, formData: FormData) {
  console.log(formData);
  const validatedFields = schema.safeParse({
    email: formData.get("email"),
  });
  console.log(validatedFields);

  if (!validatedFields.success) {
    console.log(validatedFields.error.flatten().fieldErrors.email);
    return {
      message: validatedFields.error.flatten().fieldErrors,
    };
  }
  console.log(formData);
  const email = formData.get("email") as string;
  return signInOTP({ email });

}

This is the server component for createClient:

import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";

export const createClient = (cookieStore: ReturnType<typeof cookies>) => {
  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value, ...options });
          } catch (error) {
            // The `set` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: "", ...options });
          } catch (error) {
            // The `delete` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  );
};

This is to check the OTP code:

export async function precheckOTP(prevState: any, formData: FormData) {
  console.log(formData);
  const validatedFields = schema.safeParse({
    code: formData.get("code"),
  });
  console.log(validatedFields);

  if (!validatedFields.success) {
    console.log(validatedFields.error.flatten().fieldErrors.code);
    return {
      message: validatedFields.error.flatten().fieldErrors,
    };
  }
  console.log(formData);
  const code = formData.get("code") as string;
  const user = formData.get("user") as string;
  return checkOTP({ token: code, user: user });
}


const checkOTP = async ({ token, user }: { token: string; user: string }) => {
  const cookieStore = cookies();
  const supabase = createClient(cookieStore);
  const email = atob(user);

  try {
    const { error } = await supabase.auth.verifyOtp({
      email,
      token,
      type: "email",
      options: {
        redirectTo: "/dashboard",
      },
    });

    if (error) {
      return {
        message: { error: "Something went wrong. Please try again." },
      };
    }
  } catch (error) {
    console.log(error);
    return {
      message: { error: "Something went wrong. Please try again." },
    };
  }

  return redirect("/dashboard");
};

I am redirected to the dashboard.

However the cookies are not being set properly. The first time:

sb--auth-token-code-verifier is set properly.
The second time I log on sb-
-auth-token is set

(Note this is called when someone is on protected:

export async function updateSession(request: NextRequest) {
  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: {
        get(name: string) {
          return request.cookies.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value,
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value,
            ...options,
          });
        },
        remove(name: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value: "",
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value: "",
            ...options,
          });
        },
      },
    }
  );

  await supabase.auth.getUser();

  return { supabase, response };
}

I have tried with our without this: await supabase.auth.getUser();

But then what happens is I get thrown from the route a moment later or if I try to navigate and I am thrown out of the protected route.
I then have to log in again in which case the second cookie is set.

Expected behavior

That the cookies would all set in the first instance and the user is not required to log on twice for them to set

Screenshots

If applicable, add screenshots to help explain your problem.

System information

  • OS: [e.g. macOS,]
  • Browser (if applies) [e.g., safari and firefox]
  • Version of supabase-js: [e.g. 2.42.3] (ssr 0.0.10)
  • Version of Node.js: [e.g. 18.17.0]

nextjs version - 14.1.4

Additional context

Add any other context about the problem here.

@Sof2222 Sof2222 added the bug Something isn't working label Apr 14, 2024
@aaa3334
Copy link

aaa3334 commented Apr 22, 2024

Anyone have an idea or have the same issue? It is constantly happening so its either something in my sign up flow (which as far as I can tell is identical to the docs but I could be wrong), or an issue on supabase side.. either way would 1) need better docs or 2) is a bug in the ssr package which we have all been repeatedly told to upgrade to

@simonha9
Copy link

Can take a look at this

@simonha9
Copy link

Ok - I am unable to repro this, I am following the docs here and here , have tried both a magiclink and OTP. afaik whether you auth.signup/signin with OTP you should get a session in the returned data , are you able to call auth.getUser() in the protected route after signup (1st or 2nd time)? and what do you get?

Also if anyone else more familiar wants to jump in please feel free to.

@aaa3334
Copy link

aaa3334 commented May 1, 2024

So I did quite a bit more investigation (still have no clue where the issue is) - tested in both safari and firefox. Same issue - first logon pass it doesnt set the auth cookie properly, the second it does (and it stays set too).

The auth-token is definitly coming through from request in the first instance - it is just not being set properly and i don't really know where it is being set.. I have tried to go through the code but I cannot see anywhere I would be eg. removing it (and if it was, it stays after the second log on everytime anyway). (only the auth-token-code-verifier is being set first pass)

I did upgrade a while back from using auth-helper and js, so there could potentially be some legacy code from that, but I cannot find anything obvious at least and don't have those imports in my code anymore either.

I have added a bunch of console.logs to the updateSession function to try and follow and see where the issues are. I also tried to set it manually (but when I set it manually it was actually removed (i saw the remove statements) the first pass then set properly the second. While the get function is definitly called many times (and especially on the first pass get finds auth-token) it is not finding auth-token upon page refresh.. (so I am guessing it is uing the headers originally, as it also does go to the dashboard but is just not being set properly).

I cannot see anywhere I am deleting this cookie and have searched everywhere I am accessing the cookies too.

I am not sure where the set functions are being called though - I don't get any console.logs from there at all... The remove I only ever saw 1 console log which was when I tried to set it myself manually. (even when the cookies are definitly being set). Is that on a different part of the supabase client maybe?

export async function updateSession(request: NextRequest) {
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  });
  // console.log("request.cookies", request.cookies);
  // console.log("response", response);
  console.log("request", request);
  console.log(
    "auth cookie investigation",
    request.cookies.get("sb-...-auth-token")?.value
  );
  const cookieval = request.cookies.get(
    "sb-...-auth-token"
  )?.value;
  const cookievaljson = JSON.parse(cookieval || "{}");
  console.log("auth cookie investigation json", cookievaljson.access_token);
  console.log(
    "auth cookie investigation options",
    request.cookies.get("sb-...-auth-token")
  );

  // request.cookies.set({
  //   name: "sb-cmhsbvzpocwhojvycyin-auth-token",
  //   value: cookievaljson.access_token || "",
  // });
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          console.log("cookies name", name);
          console.log("cookies value", request.cookies.get(name)?.value);

          return request.cookies.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          console.log(
            `Setting cookie ${name} to ${value} with options ${JSON.stringify(
              options
            )}`
          );
          request.cookies.set({
            name,
            value,
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value,
            ...options,
          });
          console.log("response after setting cookie", response);
        },
        remove(name: string, options: CookieOptions) {
          console.log("cookies removing name", name);
          console.log("cookies removing options", options);
          request.cookies.set({
            name,
            value: "",
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value: "",
            ...options,
          });
        },
      },
    }
  );

  const user = await supabase.auth.getUser();
  console.log("User:", user);
  // console.log("Supabase:", supabase);

  return { supabase, response };
}

@simonha9
Copy link

simonha9 commented May 2, 2024

Ok super weird - I thought I was getting the error for about an hour but now it doesn't seem to want to reproduce, afaict there isn't an issue with the auth code exchange, and it only takes me 1 signup or login to see both cookies, if you upgraded from the recent auth-helpers it sounds like the main difference is the way the cookies and its properties are managed, there is this example of supabase auth with ssr, but from the above middleware I can't find anything wrong with it. Maybe it could be something to do with the legacy code but it sounds ilke you've done a full migration :/

There is a possibly related ticket here but it's hard to tell 🤷 ,

@oldbettie
Copy link

I have a similar issue. I think it has to do with how you are setting the cookies. For some reason we can only set a single Set-Cookie header with next.js I have tried all the different ways to do it and it seems to be a constant issue.

If there is more then 1 cookie in the header then it seems to break the auth flow. I am trying to set a cookie for sharing auth with other micro services but it does not work and I have not figured out how to modify the auth cookie to be a base level domain try only setting a single cookie once per request

@aaa3334
Copy link

aaa3334 commented May 20, 2024

I have a similar issue. I think it has to do with how you are setting the cookies. For some reason we can only set a single Set-Cookie header with next.js I have tried all the different ways to do it and it seems to be a constant issue.

If there is more then 1 cookie in the header then it seems to break the auth flow. I am trying to set a cookie for sharing auth with other micro services but it does not work and I have not figured out how to modify the auth cookie to be a base level domain try only setting a single cookie once per request

Hm that makes sense! I guess there is a nextjs version that maybe has done this so maybe the issue is on the nextjs side not supabase's? Potentially this issue: vercel/next.js#64166

@ahosny752
Copy link

any fix on this? having the same issue

@ThomasBurgess2000
Copy link

So I'm not sure this is the same issue, but we had an issue related to cookies not getting set properly and logging us out, and removing

request.cookies.set({
            name,
            value,
            ...options,
          })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })

from the middleware, leaving just

let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  });

  const supabase = createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return request.cookies.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          response.cookies.set(name, value, options);
        },
        remove(name: string, options: CookieOptions) {
          response.cookies.set(name, "", options);
        },
      },
    },
  );

fixed this for us.

Would be interested if this works for others.

@koolerjaebee
Copy link

@ThomasBurgess2000 thanks for great solution! Although this solution partially works for me. If you use backend server and you stay on same page for access token's expiration time, server will send you unauthorized error due to access token expiration. If you refresh the page or move to other pages, it works fine. Any idea?

@ThomasBurgess2000
Copy link

@koolerjaebee I have not encountered this myself, having only recently implemented this solution, but what you're saying is quite possible. I will try to circle back to this in the next few days to test and see if I can find a solution. My workaround is not ideal, because clearly it's not what supabase intended, so I'm hoping they will chime in at some point.

Were you just setting the JWT expiration time really low to test? And then the behavior you're describing would happen consistently?

@koolerjaebee
Copy link

koolerjaebee commented May 24, 2024

@ThomasBurgess2000

Were you just setting the JWT expiration time really low to test? And then the behavior you're describing would happen consistently?

Yes, I was setting JWT expiration time to 1 min so that I can see if there is another error. 1 hour was my original setting and actually not a big problem because not many people would stay in same page for that long.
I've tested several time and it keep happened no matter what time I set. I just set expiration time to 24 hours for temporarily. Like you said, I hope they will figure it out.
Again thanks for sharing your idea.

@Yassdrk
Copy link

Yassdrk commented May 31, 2024

Any news ? I have the same issue with supabase ssr and last nextjs version :/

@ahosny752
Copy link

I fixed my issues by removing any auth stuff from server actions and putting them in routes. I think something with Next Caching causes bugs with the cookies

@fernando-plank
Copy link

fernando-plank commented Jul 2, 2024

still have an error event update to latest version of @supabase/ssr.

I did a quick fix that solved the error.
In the process of sign-in, I save a cookie reference for refresh and access tokens (sb-access-token, sb-refresh-token)

const cookieStore = cookies();

const { data, error } = await repository.auth.signInWithPassword({
    email,
    password,
  });
  
  cookieStore.set('sb-access-token', data?.session?.access_token);
  cookieStore.set('sb-refresh-token', data?.session?.refresh_token);

When I try to capture user information in my API routes I've an auxiliary function:

retrieveAuthOrSessionUser = async () => {
    let userFinal = null;
    const cookieStore = cookies();
    const accessToken = cookieStore.get('sb-access-token');
    const refreshToken = cookieStore.get('sb-refresh-token');

    const {
      data: { user },
      error,
    } = await this.supabase.auth.getUser();

    if (error) {
      const { data: dataSession } = await this.supabase.auth.setSession({
        access_token: accessToken?.value ?? '',
        refresh_token: refreshToken?.value ?? '',
      });
      userFinal = dataSession?.user;
    } else {
      userFinal = user;
    }

    const { id } = userFinal ?? {};

    return {
      id: id ?? '',
    };
  };

Remember to erase the cookies on the process of sign-out

  const supabaseCookies = cookies().getAll();

  supabaseCookies.map((cookie) => {
    if (cookie.name.includes('sb-')) {
      cookies().delete(cookie.name);
    }
  });

isn't the best scenario or code, but does the job. Hope this Helps.

@encima encima transferred this issue from supabase/supabase Jul 8, 2024
@j4w8n
Copy link
Contributor

j4w8n commented Jul 8, 2024

I'm not familiar with Next, but has anyone tried migrating to the new ssr 0.4.x version and using the setAll and getAll methods? It's supposed to better handle Next quirkiness. New docs are available here

@Sof2222
Copy link
Author

Sof2222 commented Jul 16, 2024

For anyone having the issues - I think it is a nextjs issue. Updating nextjs solved it and I have not had this issue the past week (have been testing)

@j4w8n
Copy link
Contributor

j4w8n commented Jul 16, 2024

For anyone having the issues - I think it is a nextjs issue. Updating nextjs solved it and I have not had this issue the past week (have been testing)

To what version?

@lukahukur
Copy link

For anyone having the issues - I think it is a nextjs issue. Updating nextjs solved it and I have not had this issue the past week (have been testing)

Ye, for what version?

@NotAProton
Copy link

Thanks a lot @Sof2222!

For reference I upgraded from "next": "14.2.4", to "next": "14.2.5", and it fixed the issue

@eposha
Copy link

eposha commented Jul 19, 2024

Same issue, any solutions? Next.js 14.2.5 doen't resolve this problem for me

@cdgn-coding
Copy link

We are experiencing this exact same issue. Next version 14.2.5.

@eposha
Copy link

eposha commented Jul 24, 2024

In my case, I used export const dynamic = 'force-dynamic' in a layout that blocked cookies on the first render.

@mattrigg9
Copy link

This doesn't directly solve OP's problem, but just throwing this out there in case anyone's banging their head against the table. Make sure you don't have any logout links inadvertently getting prefetched. My supabase auth cookie was getting assigned correctly, but then NextJS would prefetch the logout link on my account page, causing the server to immediately delete the cookie.

@AndrewLester
Copy link

In the password reset case, with Next.js pages router on 14.2.5 and most recent versions of @supabase/ssr and @supabase/auth-js (at time of this post), I have this workaround:

// @filename: src/pages/api/auth/confirm.ts
import { type EmailOtpType } from '@supabase/supabase-js';
import type { NextApiRequest, NextApiResponse } from 'next';

import { createClient } from '@/lib/utils/supabase/api';
import { serializeCookieHeader, stringToBase64URL } from '@supabase/ssr';

function stringOrFirstString(item: string | string[] | undefined) {
	return Array.isArray(item) ? item[0] : item;
}

export default async function handler(
	req: NextApiRequest,
	res: NextApiResponse,
) {
	if (req.method !== 'GET') {
		res.status(405).appendHeader('Allow', 'GET').end();
		return;
	}

	const queryParams = req.query;
	const token_hash = stringOrFirstString(queryParams.token_hash);
	const type = stringOrFirstString(queryParams.type);

	let next = '/error';

	if (token_hash && type) {
		const supabase = createClient(req, res);
		const { error, data } = await supabase.auth.verifyOtp({
			type: type as EmailOtpType,
			token_hash,
		});
		if (error) {
			console.error(error);
		} else {
			// Workaround for https://github.com/supabase/ssr/issues/36
			const supabaseURL = new URL(process.env.NEXT_PUBLIC_SUPABASE_URL!);
			const supabaseCookieDomainPrefix = supabaseURL.hostname.split('.')[0];
			const authTokenCookie = {
				name: `sb-${supabaseCookieDomainPrefix}-auth-token`,
				value: `base64-${stringToBase64URL(JSON.stringify(data.session!))}`,
				options: {
					path: '/',
					sameSite: 'lax',
					httpOnly: false,
					maxAge: 31536000000,
				},
			};
			res.setHeader(
				'Set-Cookie',
				[authTokenCookie].map(({ name, value, options }) =>
					serializeCookieHeader(name, value, options),
				),
			);
			next = stringOrFirstString(queryParams.next) || '/';
		}
	}

	res.redirect(next);
}

Maybe I've configured something wrong, but this if statement never runs the if branch because setItem is never called with the code-verifier cookie as the key:

if (key.endsWith("-code-verifier")) {

@KedalenDev
Copy link

This doesn't directly solve OP's problem, but just throwing this out there in case anyone's banging their head against the table. Make sure you don't have any logout links inadvertently getting prefetched. My supabase auth cookie was getting assigned correctly, but then NextJS would prefetch the logout link on my account page, causing the server to immediately delete the cookie.

God thank you, I rarely use NEXTJS and I always forget to remove the prefetch behaviour

@benrobertsonio
Copy link

@mattrigg9 you are a hero. thank you!

@ksloan
Copy link

ksloan commented Sep 15, 2024

@mattrigg9 I got caught by the exact same thing, thanks for the tip.

In my case this was only happening in production since "Prefetching is not enabled in development, only in production."

@Aoi-Takahashi
Copy link

Why is everyone using Next.js...? I am having the same problem with Remix :(

@foadgr
Copy link

foadgr commented Sep 30, 2024

tldr; it may be worth checking for version incompatibilities between the Supabase libraries being used.

i had this issue and resolved by making sure all supabase actions were using @supabase/ssr. It's possible you are using @supabase/auth-helpers-nextjs elsewhere in your app. Improper setting in the cookie likely stems from using different supabase client creation methods in different parts of your app.

@AdzeB
Copy link

AdzeB commented Sep 30, 2024

I have the same problem and I am using the latest version of the superbase client and also not using @supabase/auth-helpers-nextjs... My Quick fix(which still doesn't work for all my users) is to set the cookie manually

@jbro-io
Copy link

jbro-io commented Nov 7, 2024

This doesn't directly solve OP's problem, but just throwing this out there in case anyone's banging their head against the table. Make sure you don't have any logout links inadvertently getting prefetched. My supabase auth cookie was getting assigned correctly, but then NextJS would prefetch the logout link on my account page, causing the server to immediately delete the cookie.

You sir are a godsend, thank you!

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