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

NextJS集成NextAuth时JWT回调报错问题求助

问题:NextAuth JWT回调报错 "JWT Claims Set MUST be an object"

问题场景

我在NextJS中使用自定义Token服务实现NextAuth认证,核心代码如下:

async function refreshAccessToken(authToken: AuthToken) {
    try {
        const tokenResponse = await AuthApi.refreshToken(authToken.refresh_token);
        return new AuthToken(
            tokenResponse.access_token,
            tokenResponse.token_type,
            tokenResponse.expires_in,
            tokenResponse.refresh_token
        );

    } catch (error) {
        return new AuthToken(
            authToken?.access_token,
            authToken?.token_type,
            authToken?.expires_in,
            authToken?.refresh_token,
            "An error occurred whilst refreshing token"
        );
    }
}

export const authOptions: NextAuthOptions = {
    providers: [
        CredentialsProvider({
            name: 'credentials',
            credentials: {
                username: { label: 'Username', type: 'text' },
                password: { label: 'Password', type: 'password' }
            },
            authorize: async (credentials) => {
                try {
                    if (!credentials) {
                        return null;
                    }

                    const { username, password } = credentials;
                    const authToken = await AuthApi.login(username, password);

                    const session: Session = new Session(authToken, null);
                    if (authToken != null && authToken.error == '') {
                        const userDetails = await AuthApi.getUserDetails(authToken.access_token);
                        if (userDetails != null) {
                            session.user = userDetails;
                        }
                    }
                    return session as any;

                } catch (error) {
                    console.error(error);
                    throw error;
                }
            }
        })
    ],
    session: {
        strategy: 'jwt',
        maxAge: Constants.AccessTokenLifetimeSeconds
    },
    secret: process.env.APP_SECRET,
    jwt: {
        secret: process.env.APP_SECRET,
        maxAge: Constants.AccessTokenLifetimeSeconds
    },
    pages: { signIn: '/login' },
    callbacks: {
        jwt: async ({ user }: any) => {
    
            let token: AuthToken | null = null;

            if (user != null && user.token != null) {

                token = user.token;

                const clock = new Clock();
                if (user.token.expiry_date_time < clock.nowUtc()) {
                    token = await refreshAccessToken(user.token);
                }
            }
            return token;
        },
        session: async ({ session, user, token }) => {
            return session;
        }
    }
};
export default NextAuth(authOptions);

相关类定义:

export default class AuthToken implements IAuthToken {
    public expiry_date_time: Date;

    constructor(
        public access_token: string,
        public token_type: string,
        public expires_in: number,
        public refresh_token: string,
        public error: string = ""
    ) {
        const clock = new Clock();
        this.expiry_date_time = new Date(
            clock.nowUtc().getTime() + (this.expires_in - 60) * 1000
        );
    }
}
export default class Session implements ISession {
    constructor(
        public token: AuthToken | null,
        public user: UserDetails | null
    ) { }
}

报错信息

error - TypeError: JWT Claims Set MUST be an object
at new ProduceJWT (C:\VRPM\Repos\portal\node_modules\jose\dist\node\cjs\jwt\produce.js:10:19)
at new EncryptJWT (C:\VRPM\Repos\portal\node_modules\jose\dist\node\cjs\jwt\encrypt.js:7:1)
at Object.encode (C:\VRPM\Repos\portal\node_modules\next-auth\jwt\index.js:49:16)
at async Object.callback (C:\VRPM\Repos\portal\node_modules\next-auth\core\routes\callback.js:429:22)
at async NextAuthHandler (C:\VRPM\Repos\portal\node_modules\next-auth\core\index.js:295:28)
at async NextAuthNextHandler (C:\VRPM\Repos\portal\node_modules\next-auth\next\index.js:23:19)
at async C:\VRPM\Repos\portal\node_modules\next-auth\next\index.js:59:32
at async Object.apiResolver (C:\VRPM\Repos\portal\node_modules\next\dist\server\api-utils\node.js:184:9)
at async DevServer.runApi (C:\VRPM\Repos\portal\node_modules\next\dist\server\next-server.js:403:9)
at async Object.fn (C:\VRPM\Repos\portal\node_modules\next\dist\server\base-server.js:493:37) {
page: '/api/auth/[...nextauth]'

我返回的AuthToken明明是对象(控制台输出如下),但仍报错:

token: AuthToken {
    access_token: '...Nn_ooqUtFvdfh53k',
    token_type: 'Bearer',
    expires_in: 86400,
    refresh_token: '...ZddCpNTc',
    error: '',
    expiry_date_time: 2023-02-09T19:29:15.307Z
}

我发现返回return { token: token };时错误消失,但想了解正确的处理方式是什么?


问题原因

NextAuth的jwt回调要求返回值必须是普通JavaScript对象(Plain Object),而你返回的是自定义AuthToken类的实例。类实例包含原型链信息,并非纯对象,底层依赖的jose库无法将其正确序列化为JWT的Claims Set,因此抛出错误。

正确处理方式

1. 将类实例转换为普通对象

在返回前,把AuthToken实例转为纯对象,可使用扩展运算符或Object.assign():

// jwt回调中修改返回逻辑
return token ? { ...token } : null;

注意:如果expiry_date_time是Date类型,转成纯对象后会保留为Date实例,后续序列化JWT时会自动转为字符串,无需额外处理。

2. 按NextAuth规范构造返回对象

推荐将token的属性平铺或嵌套在标准结构中,既符合JWT要求,也便于后续session回调使用:

// jwt回调中返回结构化对象
return token ? {
  access_token: token.access_token,
  token_type: token.token_type,
  expires_in: token.expires_in,
  refresh_token: token.refresh_token,
  expiry_date_time: token.expiry_date_time.toISOString(),
  error: token.error
} : null;

3. 调整authorize返回值逻辑

在authorize回调中直接返回普通对象,而非自定义Session类实例,从源头避免类实例问题:

// 修改authorize回调的返回值
return {
  user: userDetails,
  token: { ...authToken } // 提前转成普通对象
} as any;

为什么返回{ token: token }能解决问题?

因为此时返回的是一个普通对象,jose库可以正确识别并序列化它。AuthToken实例作为这个对象的属性值,在序列化过程中会被自动转为普通对象(前提是类的属性都是可枚举的),因此不会触发报错。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 03:31:38