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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 08:14:51