Nuxt中Next-Auth(AuthJs)刷新token后无法持久化问题求助
我正在开发一个Nuxt应用,集成自定义OAuth认证提供商,使用Auth.js(原NextAuth)实现认证。登录流程正常,但刷新access_token时出现异常:access_token有效期为2小时,在jwt回调中已将刷新后的token传递给session回调,但下一次自动触发会话检查时,jwt回调中的token仍为旧值。初始登录时设置的token可正常持久化,但刷新操作无法更新token的refresh_token与expire_at属性,导致持续刷新直至初始refresh_token失效。
请问:
- 该问题有哪些解决方法?
- 为何初始登录时(通过callback中的account参数判断)返回的token值能正常持久化?
相关代码
// server/api/auth/[...].ts import type { AuthConfig, TokenSet } from "@auth/core/types"; import { NuxtAuthHandler } from "#auth"; import { JWT } from "@auth/core/jwt"; const runtimeConfig = useRuntimeConfig(); const BASE_URL = "Base url"; async function refreshAccessToken(token: JWT): Promise<TokenSet> { const params = new URLSearchParams({ client_id: process.env.NUXT_AUTH_CLIENT_ID!, client_secret: process.env.NUXT_AUTH_CLIENT_SECRET!, grant_type: "refresh_token", refresh_token: token.refresh_token as string, }); const response = await fetch(`${BASE_URL}/oauth/token`, { headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString(), method: "POST", }); const tokens: TokenSet = await response.json(); if (!response.ok) { throw tokens; } return { ...token, access_token: tokens.access_token, refresh_token: tokens.refresh_token, error: null, expires_at: Math.floor(Date.now() / 1000 + 30), // 30秒测试用 // expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in!), }; } export const authOptions: AuthConfig = { secret: runtimeConfig.authJs.secret, debug: true, callbacks: { async jwt({ token, account }) { console.log('-----------------------------------'); console.log("token in jwt is ", token); const now = Date.now(); let resToken = null; if (account) { console.log("initial login", token, account); // 初始登录 resToken = { ...token, access_token: account.access_token, refresh_token: account.refresh_token, expires_at: Math.floor(Date.now() / 1000 + 30), // expires_at: Math.floor(Date.now() / 1000 + account.expires_in!), error: null, }; } else if (now < (token.expires_at as number) * 1000) { console.log("token is valid ", token); // token仍有效,直接返回 resToken = token; } else { // 刷新token try { console.log("before refresh token is ", token); // 刷新后的token未持久化,expires_at、access_token和refresh_token仍为初始值 // 导致该代码块重复执行,直到refresh_token失效 resToken = await refreshAccessToken(token); console.log("resToken is ", resToken); console.log('-----------------------------------'); } catch (error) { console.error("Error refreshing access token", error); return { ...token, error: "RefreshAccessTokenError" as const }; } } return resToken; }, async session({ session, token }) { return { ...session, access_token: token.access_token, refresh_token: token.refresh_token, error: token.error, }; }, }, providers: [ { id: "projectId", name: "projectName", type: "oauth", issuer: BASE_URL, clientId: process.env.NUXT_AUTH_CLIENT_ID, clientSecret: process.env.NUXT_AUTH_CLIENT_SECRET, token: `${BASE_URL}/oauth/token`, authorization: { url: `${BASE_URL}/oauth/authorize`, params: { scope: "api_v3" }, }, redirectProxyUrl: "http://localhost:3000/api/auth", userinfo: `${BASE_URL}/api/v3/users/me`, }, ], }; export default NuxtAuthHandler(authOptions, runtimeConfig);
问题解答
一、为什么初始登录的token能持久化?
初始登录时,account参数存在,这是Auth.js识别首次认证的标志。此时框架会自动将jwt回调返回的token完整存储到会话(默认是加密Cookie)中,因为这是首次获取用户认证凭证的流程,框架内置了完整的持久化逻辑,确保所有字段都被正确存储。
而后续调用jwt回调时,account为null,框架会从现有会话中读取token并传入回调,此时需要确保返回的token对象包含所有需要更新的字段,且格式正确,框架才会覆盖原有存储的token。
二、问题解决方法
1. 确保刷新后返回完整的token对象
在refreshAccessToken函数中,必须确保返回的对象包含原token的所有核心字段(如用户标识sub),同时正确更新新的token信息:
- 如果OAuth提供商刷新后返回新的
refresh_token,则替换旧值;如果不返回,保留原有的refresh_token - 用提供商返回的
expires_in计算expires_at,避免硬编码测试值
修正后的refreshAccessToken代码:
async function refreshAccessToken(token: JWT): Promise<TokenSet> { // ... 原有请求代码 return { ...token, // 保留原token的核心字段 access_token: tokens.access_token, // 仅当提供商返回新refresh_token时才更新,否则复用旧的 refresh_token: tokens.refresh_token ?? token.refresh_token, error: null, // 用提供商返回的有效期计算过期时间,秒级时间戳 expires_at: Math.floor(Date.now() / 1000 + (tokens.expires_in ?? 7200)), }; }
2. 避免返回null,确保每次都返回有效token对象
在jwt回调中,resToken初始值设为token而非null,避免分支判断遗漏导致返回null:
async jwt({ token, account }) { console.log('-----------------------------------'); console.log("token in jwt is ", token); const now = Date.now(); let resToken = { ...token }; // 初始化复制原token if (account) { // 初始登录逻辑 resToken = { ...token, access_token: account.access_token, refresh_token: account.refresh_token, expires_at: Math.floor(Date.now() / 1000 + (account.expires_in ?? 7200)), error: null, }; } else if (now >= (token.expires_at as number) * 1000) { // 仅当token过期时才刷新 try { console.log("before refresh token is ", token); resToken = await refreshAccessToken(token); console.log("resToken is ", resToken); } catch (error) { console.error("Error refreshing access token", error); resToken = { ...token, error: "RefreshAccessTokenError" as const }; } } console.log('-----------------------------------'); return resToken; }
3. 检查Auth.js配置正确性
- 确保
secret配置正确:runtimeConfig.authJs.secret必须是一个安全的随机字符串,这是JWT签名和验证的关键,签名错误会导致token无法正确更新 - 确认session策略为
jwt:默认是jwt,如果切换为database策略,需要额外配置数据库连接,否则无法持久化token
4. 验证OAuth提供商的刷新机制
- 打印
tokens对象,确认提供商是否返回新的refresh_token和expires_in字段 - 有些提供商要求刷新时必须携带额外参数(如
scope),如果缺少会导致刷新失败或返回无效token
5. 调试token存储与解析
利用debug: true的日志输出,重点检查:
- 刷新token后,jwt回调返回的
resToken是否包含更新后的access_token、refresh_token和expires_at - 下一次调用jwt回调时,传入的
token是否是更新后的值,如果不是,说明框架没有正确持久化新token,需要检查secret或字段格式
内容的提问来源于stack exchange,提问作者Mohammed Safvan

