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

NextAuth.js中JWT回调无法更新accessTokenExpiry导致重复刷新令牌

NextAuth.JS刷新令牌轮换逻辑异常:每次触发JWT回调都执行刷新操作

我在NextAuth.JS的options.ts中实现刷新令牌轮换逻辑,参考官方文档方案:在JWT回调里检查access token是否过期,过期则调用refreshToken函数从服务器获取新令牌,服务器返回{ accessToken, refreshToken, accessTokenExpiry }。

但现在的问题是,哪怕已经按照文档更新了accessTokenExpiry字段,每次触发JWT回调时都会执行令牌刷新操作。

我加了console.log排查,发现:

  • 服务器返回的data.accessTokenExpiry是正确的(我把access token有效期设为5秒,当前时间53:41时,过期时间显示为53:46)
  • 但JWT回调里的token.accessTokenExpiry时间始终错误

日志输出截图

其中:

  • 第96行日志来自:console.log(new Date(Date.now()), new Date(token.accessTokenExpiry)
  • 第18行日志来自:console.log(new Date(Date.now()), new Date(data.accessTokenExpiry)

相关代码

[...nextauth]/options.ts

import { UUID } from 'crypto';
import { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const refreshToken = async (token: any) => {
   try {
      const response = await fetch(`${process.env.SERVER_URL}/admins/auth/refresh`, {
         method: 'POST',
         headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${token.refreshToken}`,
         },
      });

      const data = await response.json();

      console.log('row 18:', new Date(Date.now()), new Date(data.accessTokenExpiry));

      if (!response.ok) {
         throw data;
      }

      return {
         accessToken: data.accessToken,
         accessTokenExpiry: data.accessTokenExpiry,
         refreshToken: data.refreshToken ?? token.refreshToken,
      };
   } catch (error) {
      console.error(error);

      return {
         ...token,
         error: 'RefreshAccessTokenError',
      };
   }
};

export const options: NextAuthOptions = {
   session: {
      maxAge: 60 * 24 * 60 * 60,
   },
   secret: process.env.JWT_SECRET,
   providers: [
      CredentialsProvider({
         name: 'Credentials',
         credentials: {
            username: { label: 'Username', type: 'text' },
            password: { label: 'Password', type: 'password' },
         },
         authorize: async credentials => {
            const response = await fetch(`${process.env.SERVER_URL}/admins/auth/login`, {
               method: 'POST',
               headers: { 'Content-Type': 'application/json' },
               body: JSON.stringify(credentials),
            });

            if (response.status === 404) {
               throw new Error('404');
            }

            if (response.status === 403) {
               throw new Error('403');
            }

            const data = await response.json();

            const user = await fetch(`${process.env.SERVER_URL}/admins/admin`, {
               headers: {
                  Authorization: `Bearer ${data.accessToken}`,
               },
            }).then(res => res.json());

            return {
               ...user,
               accessToken: data.accessToken,
               refreshToken: data.refreshToken,
               accessTokenExpiry: data.accessTokenExpiry,
            };
         },
      }),
   ],
   callbacks: {
      async jwt({ token, user }) {
         if (user) {
            token.id = user.id as UUID;
            token.email = user.email;
            token.name = user.name;
            token.surname = user.surname;
            token.role = user.role;
            token.accessToken = user.accessToken!;
            token.refreshToken = user.refreshToken!;
            token.accessTokenExpiry = user.accessTokenExpiry;
         }

         console.log('row 96:', new Date(Date.now()), new Date(token.accessTokenExpiry));


//       \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/
//       Checking if the access token is expired
         if (Date.now() < token.accessTokenExpiry) {
            return token;
         }

         return await refreshToken(token);
      },
      async session({ session, token }) {
         session.user.email = token.email;
         session.user.role = token.role;
         session.user.surname = token.surname;
         session.user.name = token.name;
         session.user.id = token.id;
         session.accessToken = token.accessToken;
         session.refreshToken = token.refreshToken;
         session.accessTokenExpiry = token.accessTokenExpiry;

         return session;
      },
   },
};

[...nextauth]/route.ts

import NextAuth from 'next-auth/next';
import { options } from './options';

const handler = NextAuth(options);

export { handler as GET, handler as POST };

routes/admins/auth/auth-admin.service.ts

async refreshToken(req: Request) {
      const token = req.headers.authorization.split(' ')[1];

      const payload = this.jwtService.decode(token);

      const admin = await this.adminsRepository.findOne({
         where: { id: payload.id },
      });

      if (token !== admin.refreshToken) {
         throw new UnauthorizedException('Invalid refresh token');
      }

      console.log('refresh');

      // const accessTokenExpiry = Date.now() + 12 * 60 * 60 * 1000;
      const accessTokenExpiry = Date.now() + 5 * 1000;
      const accessTokenPayload = { id: payload.id, exp: accessTokenExpiry };

      const newAccessToken = this.jwtService.sign(accessTokenPayload);

      return {
         accessToken: newAccessToken,
         refreshToken: payload.refreshToken,
         accessTokenExpiry,
      };
   }

内容的提问来源于stack exchange,提问作者Lvasche

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 10:13:16