I'm using NextAuth with a custom backend in my Next.js app.
When I refresh the page, two refresh requests are sent almost simultaneously.
The first request correctly calls /auth/refresh and gets a new accessToken and refreshToken.
Immediately after that, a second request is sent, but it still uses the old refresh token.
The backend then returns 401, invalidating both tokens.
After this, no further API calls succeed until I log in again.
I'm trying to understand:
Why is the NextAuth jwt callback called twice during page reload?
How can I prevent multiple refresh calls from happening at the same time?
Here’s the relevant part of my config:
// auth.config.ts import type { NextAuthConfig } from "next-auth" import Credentials from "next-auth/providers/credentials" import validateCredential from "../server/actions/user/validateCredential" async function refreshAccessToken(token: any) { const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL! try { const res = await fetch(`${BACKEND_URL}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: token.refreshToken }), cache: "no-store", }) if (!res.ok) throw new Error("Refresh failed") const data = await res.json() const newAccessToken = data.access_token || token.accessToken const newRefreshToken = data.refresh_token || token.refreshToken const payload = JSON.parse( Buffer.from(newAccessToken.split(".")[1], "base64").toString() ) const newExp = payload?.exp ? payload.exp * 1000 : undefined return { ...token, accessToken: newAccessToken, refreshToken: newRefreshToken, accessTokenExpires: newExp, } } catch (e) { return { ...token, error: "RefreshAccessTokenError" } } } const authConfig: NextAuthConfig = { providers: [ Credentials({ async authorize(credentials) { const user = await validateCredential(credentials as any) return user ?? null }, }), ], callbacks: { async jwt({ token, user }) { if (user) { return { ...token, accessToken: user.accessToken, refreshToken: user.refreshToken, accessTokenExpires: user.accessTokenExpires, } } const now = Date.now() if (token.accessTokenExpires && now < token.accessTokenExpires - 10000) { return token } if (token.refreshToken) { return await refreshAccessToken(token) } return { ...token, error: "RefreshAccessTokenError" } }, }, } export default authConfig And my credential validation:
// validateCredential.ts export default async function validateCredential(values) { const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, cache: 'no-store', body: JSON.stringify(values), }) if (!res.ok) return null const data = await res.json() return { id: String(data.user.id), accessToken: data.access_token, refreshToken: data.refresh_token, accessTokenExpires: data.access_token_exp, } }