Skip to content

Next Auth JWT Refresh across multiple tabs #8402

@ArcaneTSGK

Description

@ArcaneTSGK

Question 💬

Hi, I am attempting to implement the refresh flow as shown in the docs. I have successfully implemented this when the user is on a single tab, but when using multiple tabs in the same browser, or when spamming the refresh button, the auth flow breaks and signs the user out.

EDIT: It appears I was wrong, it is not multiple tabs causing the problem but the second refresh.

  1. Sign in grants access token / refresh token
  2. AT expires -> refresh is called issues new AT/RT and that works fine
  3. Attempt to refresh the browser again and the JWT callback is using the initial token where the RT is no longer accepted.

Please can someone take a look and make any suggestions to help solve this issue? Thanks

EDIT 2: Looks like it is related to this and there is not yet a suitable workaround #6642

How to reproduce ☕️

next-auth: 4.23.1
next: 13.4.9
react: 18.2.0
node: 18.17.0
pnpm: 8

route.ts:

import NextAuth from 'next-auth';
import { options } from './options';

const handler = NextAuth(options);

export { handler as GET, handler as POST };

options.ts

async function refreshAccessToken(token: JWT) {
  try {
    var res = await fetch("https://myapi/api/v1/refresh", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${token.accessToken}`
      },
      body: JSON.stringify({
        refreshToken: token.refreshToken,
        username: token.username
      })
    });

    const refreshedTokens = await res.json();

    if (!res.ok) throw refreshedTokens;

    return {
      ...token,
      accessToken: refreshedTokens.token,
      expires: Date.now() + (refreshedTokens.expires - 10) * 1000,
      refreshToken: refreshedTokens.refreshToken
    }
  } catch (err) {
    console.log(err)

    await signOut()

    return {
      token,
      error: "RefreshTokenAccessError"
    }
  }
}

export const options: NextAuthOptions = {
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {},
      async authorize(
        credentials: Record<string, string> | undefined
      ): Promise<any> {
        var res = await fetch('https://myapi/api/v1/login', {
          method: 'POST',
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify({
            username: credentials?.username,
            password: credentials?.password
          })
        })

        if (res.ok) {
          const user = await res.json();
          return {
            username: user.username,
            password: '',
            accessToken: user.accessToken.token,
            refreshToken: user.accessToken.refreshToken,
            expires: Date.now() + (user.accessToken.expiresIn - 10) * 1000
          };
        }

        return null
      },
    }),
  ],
  pages: {
    signIn: '/login',
  },
  session: {
    strategy: 'jwt',
    maxAge: 24 * 60 * 60,
  },
  callbacks: {
    async redirect() {
      return '/protected';
    },
    async jwt({ token, user, account }) {
      if (account && user) {
        return {...token, ...user}
      }

      // @ts-ignore
      if (Date.now() < token.expires) {
        return token
      }

      return await refreshAccessToken(token)
    },
    async session({ session, token }) {
      session.user = token as {
        username: string;
        password: string;
        expires: ISODateString;
        accessToken: string;
        refreshToken: string;
      };
      return session;
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
};

As stated, this works perfectly fine in a single tab, I've set the accesstoken to expire at 60 seconds, so predictably at >50 seconds when refreshing the page or navigating protected routes it refreshes the token and I get new accessToken and refreshToken. If I have two tabs open at the same time as requiring a refresh then the tab that has a browser refresh second seems to have stale JWT token data, which then causes a 404 on the /refresh api and signs the user out.

Any help would be appreciated.

Contributing 🙌🏽

Yes, I am willing to help answer this question in a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionAsk how to do something or how something works

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions