You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Nuxt中Next-Auth(AuthJs)刷新token后无法持久化问题求助

Nuxt + Auth.js 自定义OAuth刷新token不持久化问题

我正在开发一个Nuxt应用,集成自定义OAuth认证提供商,使用Auth.js(原NextAuth)实现认证。登录流程正常,但刷新access_token时出现异常:access_token有效期为2小时,在jwt回调中已将刷新后的token传递给session回调,但下一次自动触发会话检查时,jwt回调中的token仍为旧值。初始登录时设置的token可正常持久化,但刷新操作无法更新token的refresh_token与expire_at属性,导致持续刷新直至初始refresh_token失效。

请问:

  1. 该问题有哪些解决方法?
  2. 为何初始登录时(通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 18:42:04