supabase / ssr

Supabase clients for use in server-side rendering frameworks.
MIT License
50 stars 6 forks source link

Cookies not setting properly supabase ssr #36

Open Sof2222 opened 4 months ago

Sof2222 commented 4 months ago

Bug report

I have already checked and cant see the same issue.

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

nextjs version - 14.1.4

Additional context

Add any other context about the problem here.

aaa3334 commented 4 months ago

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 commented 4 months ago

Can take a look at this

simonha9 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 3 months ago

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 commented 3 months ago

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: https://github.com/vercel/next.js/issues/64166

ahosny752 commented 3 months ago

any fix on this? having the same issue

ThomasBurgess2000 commented 3 months ago

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 commented 3 months ago

@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 commented 3 months ago

@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 commented 3 months ago

@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 commented 3 months ago

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

ahosny752 commented 3 months ago

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 commented 2 months ago

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.

j4w8n commented 2 months ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

Thanks a lot @Sof2222!

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

eposha commented 1 month ago

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

cdgn-coding commented 1 month ago

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

eposha commented 1 month ago

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

mattrigg9 commented 1 month ago

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 commented 1 month ago

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: https://github.com/supabase/ssr/blob/ae6af1d3f9a405dc8251b595a6b3a3c9d56b9b7f/src/cookies.ts#L358

KedalenDev commented 2 weeks ago

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