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",
解决方案
问题根源
- 服务端导入错误:服务端代码中使用了客户端版本的
getCsrfToken(来自next-auth/react),服务端必须使用next-auth/server提供的版本才能正确读取CSRF Token。 - 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
相关产品推荐
相关产品推荐

