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

Next.js13+NextAuth+以太坊登录:客户端与服务端CSRF Token不匹配问题

NextAuth + SIWE 以太坊登录CSRF Token不匹配问题解决

问题描述

在Next.js 13应用中使用NextAuth和SIWE实现以太坊登录时,出现客户端与服务端CSRF Token(nonce)不匹配的问题:客户端调用getCsrfToken()始终返回相同的nonce,但服务端生成的Token却不同,导致SIWE验证失败。

相关代码片段

服务端代码(app/api/auth/[...nextauth]/route.ts)

import { NextApiRequest, NextApiResponse } from "next";
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { getCsrfToken } from 'next-auth/react'; // 错误的导入
import { NextRequest, NextResponse } from "next/server";
import { SiweMessage } from 'siwe';

// @ts-ignore
export const authOptions = ({ req }) => ({
  secret: process.env.NEXTAUTH_SECRET,
  providers: [
    CredentialsProvider({
      name: 'Ethereum',
      type: 'credentials',
      credentials: {
        message: { label: 'Message', type: 'text', placeholder: '0x0' },
        signature: { label: 'Signature', type: 'text', placeholder: '0x0' },
      },
      authorize: async (credentials) => {
        try {
          const siwe = new SiweMessage(
            JSON.parse(credentials?.message ?? '{}')
          );
          const nonce = await getCsrfToken({ req:{headers:req?.headers} }); // 参数传递错误
          
          console.log("NONCE IS: ",nonce);

          const verifyParams = {
            signature: credentials?.signature || '',
            nonce: nonce,
          };

          const verificationResult = await siwe.verify(verifyParams, {});

          if (!verificationResult.success) {
            return null;
          }

          return { id: verificationResult.data.address };
        } catch (error) {
          console.error({ error });
          return null;
        }
      },
    }),
  ],
  callbacks: {
    // @ts-ignore
    session: ({ session, token }) => ({
      ...session,
      user: { ...session.user, id: token.sub },
    }),
  },
});

// @ts-ignore
const Auth = (req, res: NextApiResponse) => {
  const authOpts = authOptions({ req });
  return NextAuth(req, res, authOpts);
};

export { Auth as GET, Auth as POST };

客户端代码(Components/Navbar.tsx)

"use client";
import Link from "next/link";
import { Button } from "./ui/button";
import { useWeb3Modal } from "@web3modal/react";
import { useAccount, useSignMessage } from 'wagmi';
import { useEffect, useState } from "react";
import { getCsrfToken, signIn, signOut, useSession } from 'next-auth/react';
import { SiweMessage } from 'siwe';

export default function Navbar() {
    const { data: sessionData } = useSession();
    const { open } = useWeb3Modal();
    const { signMessageAsync } = useSignMessage();
    const { address, isConnected } = useAccount();
    const { chain } = useNetwork();
    const [isMounted, setIsMounted] = useState(false);

    const onClickSignIn = async () => {
        try {
          const message = new SiweMessage({
            domain: window.location.host,
            address: address,
            statement: 'Sign in to App.',
            uri: window.location.origin,
            version: '1',
            chainId: chain?.id,
            nonce: await getCsrfToken(),
          });
          const signature = await signMessageAsync({
            message: message.prepareMessage(),
          });
          signIn('credentials', {
            message: JSON.stringify(message),
            redirect: false,
            signature,
          });
        } catch (error) {
          window.alert(error);
        }
      };

      const onClickSignOut = async () => {
        await signOut();
      };

    useEffect(() => setIsMounted(true), [])
    if (!isMounted) return <></>;

    return (
        <nav className="fixed inset-x-0 top-0 bg-white dark:bg-gray-950 z-[10] h-fit border-b border-zinc-300 py-2">
            <div className="flex items-center justify-center h-full gap-2 px-8 mx-auto sm:justify-between max-w-7xl">
                <Link href="/" className="items-center hidden gap-2 sm:flex" >
                    <p className="rounded-lg border-2 border-b-4 border-r-4 border-black px-2 py-1 text-xl  transition-all hover:-translate-y-[2px] md:block dark:border-white font-semibold">
                        NextAuth Error
                    </p>
                </Link>
                <div className="flex items-center">
                    <Button variant="default" onClick={open}>
                        {isConnected ? 'Connected' : 'Connect Wallet'}
                    </Button>
                    {isConnected && (
                        <Button className="ml-2" variant="outline" onClick={sessionData ? onClickSignOut:onClickSignIn}>
                            {sessionData?'Sign Out':"Sign in"}
                        </Button>
                    )}
                </div>
            </div>
        </nav>
    )
}

错误日志

NONCE IS:  ac0e019c927cf06ded90aad76e50a66f444923ca2b6fd4a356deca28724fa509
{
  error: {
    success: false,
    data: SiweMessage {
      domain: 'localhost:3000',
      address: '0x4BF20785a0B2E6a375B1d49Ba64c6145AC50AAD6',
      statement: 'Sign in to App.',
      uri: 'http://localhost:3000',
      version: '1',
      chainId: 80001,
      nonce: '6c0876fe35f884fa2dd1f064d45a86e7ff1af082ea50eac5570e1b0661043cd1',
      issuedAt: '2023-09-10T05:51:10.990Z'
    },
    error: SiweError {
      type: 'Nonce does not match provided nonce for verification.',
      expected: 'ac0e019c927cf06ded90aad76e50a66f444923ca2b6fd4a356deca28724fa509',
      received: '6c0876fe35f884fa2dd1f064d45a86e7ff1af082ea50eac5570e1b0661043cd1'
    }
  }
}

使用版本

"siwe": "^2.1.4",
"next": "13.4.19",
"next-auth": "^4.23.1",

解决方案

问题根源

  1. 服务端导入错误:服务端代码中使用了客户端版本的getCsrfToken(来自next-auth/react),服务端必须使用next-auth/server提供的版本才能正确读取CSRF Token。
  2. req参数不完整:调用getCsrfToken时仅传递了headers,没有传递完整的请求对象,导致服务端无法读取客户端发送的CSRF cookie。

具体修改

1. 修正服务端getCsrfToken导入

将route.ts中的导入语句替换为:

import { getCsrfToken } from 'next-auth/server';

2. 正确传递完整req对象获取nonce

在authorize函数中,修改nonce获取代码:

const nonce = await getCsrfToken({ req });

3. 适配App Router的NextAuth处理函数

由于使用Next.js 13 App Router,调整Auth函数以适配NextRequest:

import NextAuth from "next-auth/next";

// @ts-ignore
const Auth = async (req: NextRequest) => {
  const authOpts = authOptions({ req });
  return NextAuth(req, authOpts);
};

验证修改

修改完成后重启服务,再次尝试登录,服务端打印的nonce将与客户端SIWE消息中的nonce一致,SIWE验证会成功通过。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 05:12:02