auth-helpers: Invalid Refresh Token: Refresh Token Not Found

Bug report

Describe the bug

I’m currently getting Invalid Refresh Token: Refresh Token Not Found error in my Next.js middleware

const res = NextResponse.next();
  const supabase = createMiddlewareSupabaseClient({ req, res });

  const {
    data: { session },
    error,
  } = await supabase.auth.getSession();

  if (error) {
    throw new Error(error.message);
  }

A clear and concise description of what the bug is.

To Reproduce

I think the issue is pretty similar to this one:

https://github.com/supabase/gotrue-js/issues/323 Steps to reproduce the behavior, please provide code snippets or a repository:

  1. Go to ‘…’
  2. Click on ‘…’
  3. Scroll down to ‘…’
  4. See error

Expected behavior

To logout the user, or keep them signed in

A clear and concise description of what you expected to happen.

Screenshots

If applicable, add screenshots to help explain your problem.

System information

  • OS: [e.g. macOS, Windows]
  • Browser (if applies) [e.g. chrome, safari]
  • Version of supabase-js: [e.g. 6.0.2]
  • Version of Node.js: [e.g. 10.10.0]

Additional context

Add any other context about the problem here.

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 25
  • Comments: 87 (12 by maintainers)

Most upvoted comments

@jareddr I’m not sure how you read my comment and came to this conclusion. Please read it again as this isn’t a bug, its how an application behaves when your token has expired. As I’ve asked in my last sentence Please reply to this message with what you expect to happen when your token has expired and not able to refresh due to whatever reason.

I’m not suggesting it’s a bug, but that it’s not a great developer experience. I’m trying to learn the Supabase framework and I’m building upon the Supabase Sveltekit example project that demonstrates auth flow.

What happens: After my token expires, if I try to load the homepage of my app, I’m met with a 400 error instead of my homepage. In the console I see the error

AuthApiError: Invalid Refresh Token: Refresh Token Not Found
    at /home/project/node_modules/@supabase/gotrue-js/dist/main/lib/fetch.js:41:20
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  __isAuthError: true,
  status: 400
}

I honestly can’t tell which call from my codebase is triggering this error.

What I expect to happen: I would expect the example app to show where to gracefully handle errors of this nature and to redirect the user a login screen if its not possible to authorize the session.

I want to emphasize again, that I’m a brand new supabase user. I’ve got an mvp of my app going and I’m sporadically running into this Auth problem. After digging around in my code base trying to figure out where it’s happening I started googling which ended up on this thread.

Seeing this issue after switching from the next auth helper to the SSR package. Not breaking anything. Just causing logouts to be super frequent

Seems like we have a bug here (using try without catch) , so the exception Invalid Refresh Token: Refresh Token Not Found is thrown here when you call getSession()

The typical way your users will see this problem is if they login, leave for a while, come back later (on the same browser) after their token has expired and try access a protected route, triggering a call to getSession or getUser somewhere in your code. This returns an error (the one in the title of this issue) rather than simply a null session (which would indicate the user is not logged in, according to the docs).

Basically if a user doesn’t log out at the end of their session (which I would consider the typical behavior of most users) you are likely to get this issue.

You may be able to “fix” this by catching the error from getSession and setting the "clear-site-data" cookie (See MDN Docs) in the headers of your response (assuming you are caling getSession on the server side). WARNING: this will clear all cookies for your app. The sensible solution would be to delete just the expired supabase cookie, unfortunately this is not currently possible in production because the cookie name is unknown (see my comment above and issue https://github.com/supabase/auth-helpers/issues/719)

Note this shouldn’t be an issue if you are calling supabase from the browser. Renaming cookies can be done using the example in the docs here. And you can easily use the document.cookie = 'my-access-token='... after catching the error above. I haven’t tested this since we don’t touch supabase on the browser

We haven’t been able to reproduce this issue and will find it hard to fix since we have no way of seeing or testing the issue. Can one of you on the thread provide a reproducible repository with the issue so we can take a look at it.

I think I found a reproducible one which produces a similar result. Could you please try this scenario?

  1. set JWT expiry limit to lowest as possible (which is 1200 currently)
  2. login to supabase auth based app
  3. in developer console, get the document.cookie for our JWT token. keep this somewhere else.
  4. logout of supabase auth based app we logged in from step2.
  5. wait for expiry time. (1200 seconds in this case)
  6. on a fresh app website page that isn’t logged in, inject the document.cookie from step3.
  7. refresh so that document.cookie takes effect.
  8. You get bunch of errors on server and client. Below statement prints multiple times in my server for next.js app.
[AuthApiError: Invalid Refresh Token: Refresh Token Not Found] {
1|vhub_fe  |   __isAuthError: true,
1|vhub_fe  |   name: 'AuthApiError',
1|vhub_fe  |   status: 400
1|vhub_fe  | }
  1. The problem here is that none of supabase-js api works so I can’t even await supabase.signOut().

Disclaimer here is that my bug happens to have same result, but I never log out of the app nor do I turn off my server. My server is always online, and “Refresh token not found” happens after some time has passed. Also not sure what the expiry was set in cookie for the actual error scenario, but in actual scenario, the error persisted even if I closed the browser and reentered the website.

My current solution to bypass Refresh Token Not Found in production - this simply logs out the user: (part of middleware.ts)

const res = NextResponse.next();
const supabase = createMiddlewareClient<Database>({ req, res });
const {
  data: { session },
  error,
} = await supabase.auth.getSession();

if (error) {
  res.cookies.delete("my-auth-token-name");
  return ["error", res];
}

Did you ever solve this issue? Im running into the same problem

Increasing the interval “solved” the issue. I don’t think this is the ideal solution, but for me it stopped breaking the application at least.

What did you increase this to?

I’m also running into this issue with auth.getSession() when using createRouteHandlerClient from @supabase/auth-helpers-nextjs

+1 for this issue! I’m having it in the dart client ever since I cloned the user_management_example project and have to sign in using the magic link every time.

I believe the issue is that you have info in your cookies that references a deleted user.

Any updates on this? Our users get signed out at random times because of this and it’s a big issue for our app.

From what I’ve found, it seems that the refresh token workflow works well until supabase/ssr decides to split the cookie in two chunks. Maybe the refresh token code misses to handle this situation.

In my case, the refresh token workflow works well with email login, but it doesn’t work properly with google signin/up as it pulls all user info in cookies and it gets split.

I didn’t found time to investigate this further and submit a PR yet, so take it with a grain of salt. In the meantime, going back to old auth-helpers package helps, but this would be really awesome for the refresh token workflow to work again reliably with supabase/ssr - because as of today users are forced-unlogged as soon as the JWT token expires (1h by default)

OK thanks for this insight. I’ll check the code there.

I encountered this error during testing due to being logged in with a cookie from a user I deleted from my database. I seem to have fixed it with this workaround, which clears all Supabase-related cookies if this error is thrown:

import { type NextRequest } from 'next/server';
import { createClient } from '@/utils/supabase/middleware';

export async function middleware(request: NextRequest) {
  const { supabase, response } = createClient(request);
  const { error } = await supabase.auth.getSession();
  
  if (error?.message.match("Invalid Refresh Token")) {
    const allCookies = request.cookies.getAll();
    allCookies.forEach(cookie => {
      // Delete all Supabase cookies starting with 'sb-'
      if (cookie.name.startsWith('sb-')) {
        response.cookies.delete(cookie.name)
      }
    });
  }

  return response;
}

@NickG-NZ - The problem is that it happens in a lot of non-typical circumstances, and there is also no way to “catch” the problem.

J

@silentworks

I just want to add some context to what’s going on here because it is fundamentally not resolved. If you log in a user, then wait until the JWT expires (typically this happens if waiting overnight and come back the next day), the next call to getSession will throw this error: Invalid Refresh Token: Refresh Token Not Found. This is “expected” because the token has expired but the browser is still sending the cookie because you never logged out (and therefore never deleted the cookie).

Here’s where the problem comes though: The way I would handle this (since the supabase team refuses to implement anything elegant), is to simply catch the error and delete the cookie. This seems easy enough but: Deleting the cookie requires you to know the name of the cookie, which is not possible unless you gave it a custom name.

Unfortunately giving the cookie a custom name (by passing cookieOptions to createServerClient) is broken (See my issue here: https://github.com/supabase/auth-helpers/issues/719 and may also be related to this issue: https://github.com/supabase/auth-helpers/issues/717

I don’t know how anyone else is handling this, but it is a complete road-block for us.

Seems there’s just a couple of console.log statements lying around in the supabase codebase (this is one of them we hit as well). Would be great to have this configurable with a logging level, or better yet, that we could supply our own logger. As console.log is not recommended in production in Node.js if I remember correctly (it’s not a big problem in the browser of course).

@mb21 you can supply your own debug logger in the latest version of supabase-js

https://github.com/supabase/gotrue-js/releases/tag/v2.52.0

I’ve just re-read all the messages here and now I’m wondering what the actual issue is? All the scenarios provided seem to lead to the same error but the developer should be handling the error in their app. This isn’t something that supabase-js or auth-helpers should manage as this is down to error management in your app itself. Please reply to this message with what you expect to happen when your token has expired and not able to refresh due to whatever reason.

One of the places I found this error is when calling getSession, it sees the session as expired, then calls the gotrue api to try and refresh the session. Part of that process is trying to find a user that has the refresh token. If something goes wrong when searching for a user, it’ll return this error.

Can you look in your auth.refresh_tokens table and find an entry that matches the token?

Also one thing that may be confusing you is if you are getting this error on localhost, not on production. This is somewhat expected on localhost because the cookies remain in place forever, but your backend / database is not there. So it’s normal to get Invalid Refresh Token errors if your database has changed.

How many of you are actually seeing this on a real, live project (or staging environment) – not local development?

I can reproduce it in production always. I just need to wait less than a day, I check the production site and it always fails on the first load.

From what I’ve found, it seems that the refresh token workflow works well until supabase/ssr decides to split the cookie in two chunks. Maybe the refresh token code misses to handle this situation.

In my case, the refresh token workflow works well with email login, but it doesn’t work properly with google signin/up as it pulls all user info in cookies and it gets split.

I didn’t found time to investigate this further and submit a PR yet, so take it with a grain of salt. In the meantime, going back to old auth-helpers package helps, but this would be really awesome for the refresh token workflow to work again reliably with supabase/ssr - because as of today users are forced-unlogged as soon as the JWT token expires (1h by default)

Ugh, and still no good solution to this problem? Because I tried everything I found in github issues, reddit and there is still no valid workaround it seems.

You nailed it. Note also that the cookie names generated by the SSR package include a random hash. So even if you want to manipulate them manually you can’t because you don’t know the name.

My preference would be that supabase automatically deletes the expired cookie and returns a null session (indicating the user is not logged in).

Note that this problem can be resolved manually now that the issue with setting a custom cookie name has been fixed. (You can set your own name for the cookie by passing an option to “creatServerClient”)

This means if this error occurs, you can just delete the cookie. And follow what ever your normal flow is for unauthenticated users.

Alternatively you can use the solution mentioned above and just remove all cookies starting with “sb-”. This is the solution we initially implemented to get around the problem

I was simply away from my system for a while and my app was running in the dev mode. I was greeted with this error.

I am using the new @supabase/ssr package.

Found this thread after encountering the same error following the official docs here: https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware

As was already described by others, the error happens after being inactive some time.

I posted a fix for this in #691

Nothing against the code, but this looks more like a workaround than an actual fix, right? 😅

I don’t understand the underlying mechanism in detail, but I’d expect something like this to work:

export async function middleware(req: NextRequest) {
  const res = NextResponse.next()
  const supabase = createMiddlewareClient({ req, res })
  const { error } = await supabase.auth.getSession()
  if (error) {
    // somehow handle the error
  }
  return res
}

I tried redirecting, rewriting, refreshing the session and logging the user out, but didn’t have any success. It would be great if we could get an updated example in the docs that shows a way to handle this error gracefully.

This is reproducible with a local supabase with jwt_expiry turned down to 60 (or some other low number) and the default app created with npx create-next-app -e with-supabase. The first refresh goes fine, although 10 refresh tokens get created: Here is refresh_tokens table before the initial refresh: image Here it is after first refresh: image And then refreshing again the table stays the same and you get a ton of these errors. Here are the debugger logs debugger_output.txt

At that point you get logged out of the app.

Same here - reminds me of an original bug in auth-helpers. Logs you out, in my case “token already used”.

the issue is still there. Any solution or workaround?

@wdavidturner I have the same issue, it happens when the auth token expires but the user is still logged in.

@seho0808 Thank you for providing the solution. I’m facing the same issue.

But is this also a solution, or will it only make the issue very less like to happen? So in the example of this max seconds being a week, will will get the error again if we logon to the app only to open it 8 days later…?