auth-helpers
auth-helpers copied to clipboard
Token refresh logs out the user in nextjs
Bug report
- [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
Once a token needs to be refreshed in nextjs the client will start to throw Invalid Refresh Token errors and will log out the user.
To Reproduce
Steps to reproduce the behavior, please provide code snippets or a repository:
- git clone https://github.com/imownbey/supabase-auth-repro
- supabase start
- go to http://localhost:3000/
- register a user
- log in
- wait 60 seconds and refresh the page
- wait another 60 seconds and refresh the page again
You will now be logged out
Expected behavior
Tokens should refresh and not log a user out everytime
Screenshots
Sessions DB before initial refresh (after log in, I have logged in with 2 users is why there are two rows)
After first refresh:
Refreshing after that will not change the session table and user is logged out.
Debug log from refresh which logs the user out: debugger_output.txt
System information
- OS: [e.g. macOS, Windows]
- Browser (if applies) [e.g. chrome, safari]
- Version of supabase-js: latest
- Version of Node.js: v20.5.1
Additional context
I am also seeing this in prod and local in my production app, it is a new behavior after switching to ssr from auth-helpers/next
To try to make it a bit easier to understand whats going on I changed the middleware to be:
if (!request.nextUrl.pathname.startsWith("/_next") && !request.nextUrl.pathname.startsWith("/favicon")) {
console.log({path: request.nextUrl.pathname})
const { supabase, response } = createClient(request)
// Refresh session if expired - required for Server Components
// https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware
await supabase.auth.getSession()
return response
}
so it doesnt try to refresh for every request just the main one.
Here are the logs from the first refresh (which generated 3 new refresh tokens in the DB): first-refresh.txt and the second refresh (where the user is logged out): second-refresh.txt
It seems to refresh the token 3 times (even though we are only calling getSession once? Although that is maybe server components also?), and then on the second refresh still is trying to use the original token from before the second refresh again.
Ok I am 99% sure that the cookie set middleware client code just does not work.
Ok I figured out the bug, the middleware uses assigning as if javascript had pointers or something. Instead it needs to do something like this:
export const createClient = (request: NextRequest) => {
// Create an unmodified response
const response = {ref: 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) {
console.log({name, setValue: decodeURI(value)})
// If the cookie is updated, update the cookies for the request and response
request.cookies.set({
name,
value,
...options,
})
response.ref = NextResponse.next({
request: {
headers: request.headers,
},
})
response.ref.cookies.set({
name,
value,
...options,
})
},
remove(name: string, options: CookieOptions) {
// If the cookie is removed, update the cookies for the request and response
request.cookies.set({
name,
value: '',
...options,
})
response.ref = NextResponse.next({
request: {
headers: request.headers,
},
})
response.ref.cookies.set({
name,
value: '',
...options,
})
},
},
auth: {
debug: true
}
}
)
return { supabase, response }
}
Ah yeah sorry, I mean the middleware code described in the docs. Seems like you wrote your own
@imownbey what exactly was the fix?
This is how my middleware.ts
file looks like (same as yours, only without the .ref
thingy bc that gives me errors) and I still get the error:
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { type NextRequest, NextResponse } from 'next/server';
export const createClient = (request: NextRequest) => {
// Create an unmodified response
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) {
// If the cookie is updated, update the cookies for the request and response
request.cookies.set({
name,
value,
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value,
...options,
});
},
remove(name: string, options: CookieOptions) {
// If the cookie is removed, update the cookies for the request and response
request.cookies.set({
name,
value: '',
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value: '',
...options,
});
},
},
auth: {
autoRefreshToken: true,
// debug: true,
},
},
);
return { supabase, response };
};
Thank you for investigating this.
I stumbled on your Issue, because I have a similar problem. I get logged out occasionally on using the supabase.auth.refreshSession() function. I use that, to detect if a user is deleted, on page load.
Using Next 14, with server components and custom middleware.
Not sure if it's related to your find though...
I spent a few hours debugging this and tracked it down to having already been fixed in #760. What was happening (in my case at least) is the server-set cookie would be chunked into 3 cookies, whereas the browser-set cookie would only require 2 cookie chunks. The browser would only set those 2 chunks with the new cookie data, and then attempt to combine those chunks with the third one leftover from the server, resulting in an invalid session and the cookies being all cleared.
Unfortunately, it seems to be taking a little longer than usual for Supabase to release an update here, and this is causing a major headache for us.
We've identified this as a symptom of a few issues. This is what we're doing about it: https://github.com/orgs/supabase/discussions/27037
Linking the PR for reference: https://github.com/supabase/ssr/pull/1
Unfortunately, it seems to be taking a little longer than usual for Supabase to release an update here, and this is causing a major headache for us.
I'm very sorry about taking so long, but we were basically 100% focused on this for about a month. We did not want to release any more partial fixes and then have to do another cycle of debugging to understand what's going on.
@hf thank you for the clarification and I'm glad this is being worked on! The main thing causing confusion and frustration from my end was the lack of communication on what was happening, so this public explanation is very welcome :)
I ran into a variant of this bug, but not sure if it's the same. However, I'm pretty sure the middleware code in the doc for set() doesn't work when the cookie is chunked. The middleware code in question from the doc is:
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,
})
},
The symptom I see is that my cookie is split into two chunks. The chunks are set individually in this callback. However, because the call to NextResponse.next() replaces response, the first chunk that was set is now lost. In the end, the only chunk that is sent to the client is the last chunk. The cookie is now broken, with a stale first chunk, which includes the old expires_at timestamp. In subsequent trips, the user is logged out because of that. When I commented out that NextResponse.next() line, it works correctly.