NextAuth(Next.js App Router)令牌刷新后未更新问题求助
问题:NextAuth JWT未同步刷新令牌轮换后的新令牌
我用Next.js前端搭配NestJS后端,后端已实现**刷新令牌轮换(refresh token rotation)**的令牌生成逻辑,现在要在前端通过NextAuth配置使用这些令牌。我的auth.ts配置如下:
import NextAuth from "next-auth"; import Credentials from "next-auth/providers/credentials"; import { API_BASE_URL } from "./lib/constants"; import { JWT } from "next-auth/jwt"; async function refreshAccessToken(token: JWT) { try { const res = await fetch(`${API_BASE_URL}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh: token.refreshToken }), }); if (!res.ok) throw new Error("Refresh failed!"); const data = await res.json(); console.log(data); return { ...token, accessToken: data.tokens.accessToken, refreshToken: data.tokens.refreshToken, accessTokenExpires: data.tokens.accessTokenExpires, refreshTokenExpires: data.tokens.refreshTokenExpires, }; } catch (e) { console.error("❌ Refresh token failed:", e); } } export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [ Credentials({ name: "Credentials", credentials: { email: { label: "Email", type: "text" }, password: { label: "Password", type: "password" }, }, authorize: async (credentials) => { try { const res = await fetch(`${API_BASE_URL}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: credentials?.email, password: credentials?.password, }), }); if (!res.ok) return null; const data = await res.json(); return { id: data.user.id, image: data.user.avatar, name: data.user.name, accessToken: data.tokens.accessToken, refreshToken: data.tokens.refreshToken, accessTokenExpires: data.tokens.accessTokenExpires, refreshTokenExpires: data.tokens.refreshTokenExpires, }; } catch (error) { console.error("Login error:", error); return null; } }, }), ], session: { strategy: "jwt", maxAge: 7 * 24 * 60 * 60, }, callbacks: { async jwt({ token, user, trigger, session }) { if (user) { return { ...token, sub: user.id, name: user.name, picture: user.image, accessToken: user.accessToken, refreshToken: user.refreshToken, accessTokenExpires: user.accessTokenExpires, refreshTokenExpires: user.refreshTokenExpires, }; } if (trigger === "update" && session) { return { ...token, ...session }; } if (Date.now() < (token.accessTokenExpires - 5000)) { return token; } return refreshAccessToken(token); }, async session({ session, token }) { session.user.id = token.sub as string; session.user.image = token.picture as string; session.user.name = token.name as string; session.accessToken = token.accessToken as string; session.refreshToken = token.refreshToken as string; return session; }, }, jwt: { maxAge: 7 * 24 * 60 * 60, }, pages: { signIn: "/login", }, });
问题现象
每次调用/auth/refresh接口后,后端返回新令牌,但NextAuth的JWT未同步更新这些新令牌。下一次尝试刷新时,后端返回:
invalid refresh token!
原因是前端仍在发送已被轮换失效的旧刷新令牌。
需求
调用/auth/refresh时,NextAuth需始终将JWT更新为最新的访问/刷新令牌,确保刷新令牌轮换逻辑正常工作。
解决方案
问题核心在于两点:refreshAccessToken函数错误处理不完整,以及JWT回调未确保返回有效令牌对象。以下是具体修复步骤:
1. 完善refreshAccessToken的错误处理
刷新失败时需返回包含错误标记的令牌对象,避免JWT回调接收undefined导致令牌不更新:
async function refreshAccessToken(token: JWT) { try { const res = await fetch(`${API_BASE_URL}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh: token.refreshToken }), }); if (!res.ok) throw new Error("Refresh failed!"); const data = await res.json(); return { ...token, accessToken: data.tokens.accessToken, refreshToken: data.tokens.refreshToken, accessTokenExpires: data.tokens.accessTokenExpires, refreshTokenExpires: data.tokens.refreshTokenExpires, }; } catch (e) { console.error("❌ Refresh token failed:", e); // 返回带错误标记的令牌,供后续回调处理 return { ...token, error: "RefreshAccessTokenError" as const, }; } }
2. 优化JWT回调逻辑
确保回调始终返回有效令牌对象,同时处理刷新失败的情况:
async jwt({ token, user, trigger, session }) { if (user) { return { ...token, sub: user.id, name: user.name, picture: user.image, accessToken: user.accessToken, refreshToken: user.refreshToken, accessTokenExpires: user.accessTokenExpires, refreshTokenExpires: user.refreshTokenExpires, }; } if (trigger === "update" && session) { return { ...token, ...session }; } // 已存在刷新错误时直接返回令牌,避免重复尝试 if (token.error) return token; // accessToken未过期则返回原令牌 if (Date.now() < (token.accessTokenExpires - 5000)) { return token; } // 调用刷新函数,兜底处理刷新失败的情况 const refreshedToken = await refreshAccessToken(token); return refreshedToken || token; }
3. 调整JWT有效期配置
确保JWT的maxAge不短于后端返回的刷新令牌有效期,避免JWT提前过期导致令牌丢失:
jwt: { // 根据后端刷新令牌有效期调整,示例为30天 maxAge: 30 * 24 * 60 * 60, },
4. 在Session回调中传递错误状态
将刷新错误状态传递到session,方便前端处理登出逻辑:
async session({ session, token }) { session.error = token.error; session.user.id = token.sub as string; session.user.image = token.picture as string; session.user.name = token.name as string; session.accessToken = token.accessToken as string; session.refreshToken = token.refreshToken as string; return session; }
额外注意事项
- 确认后端返回的
accessTokenExpires和refreshTokenExpires为毫秒级时间戳,如果是秒则需乘以1000转换。 - 生产环境中,可在检测到
RefreshAccessTokenError时直接调用signOut()强制用户重新登录。 - 测试时可在
refreshAccessToken中打印新旧令牌,验证是否正确更新。
内容的提问来源于stack exchange,提问作者Sudeep Lamichhane
相关产品推荐
相关产品推荐

