I'm currently working on implementing Ethereum sign-in functionality in my Next.js 13 application using NextAuth and Siwe. However, I'm encountering an issue where the CSRF token doesn't match between the client and server sides. Here's the relevant code snippets:
In 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', // default for Credentials
// Default values if it was a form
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 verifyOpts = {};
// const fields = await siwe.validate(credentials?.signature || "");
const verificationResult = await siwe.verify(
verifyParams,
verifyOpts
);
// if (fields.nonce !== nonce) {
if (!verificationResult.success) {
return null;
}
// Create User Account
console.log(verificationResult);
// Create User Account End
//
return {
id: verificationResult.data.address,
};
} catch (error) {
// Uncomment or add logging if needed
console.error({ error });
return null;
}
},
}),
/**
* ...add more providers here.
*
* Most other providers require a bit more work than the Discord provider. For example, the
* GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
* model. Refer to the NextAuth.js docs for the provider you want to use. Example:
*
* @see https://next-auth.js.org/providers/github
*/
],
callbacks: {
// token.sub will refer to the id of the wallet address
// @ts-ignore
session: ({ session, token }) => ({
...session,
user: {
...session.user,
id: token.sub,
},
}),
},
});
// @ts-ignore
const Auth = (req, res: NextApiResponse) => {
const authOpts = authOptions({ req });
// const isDefaultSigninPage =
// req.method === 'GET';
// // Hide Sign-In with Ethereum from default sign page
// if (isDefaultSigninPage) {
// // Removes from the authOptions.providers array
// authOpts.providers.pop();
// }
return NextAuth(req, res, authOpts);
};
export { Auth as GET, Auth as POST };
In Components/Navbar.tsx
"use client";
import Link from "next/link";
import { Button } from "./ui/button";
import { useWeb3Modal } from "@web3modal/react";
import { useAccount, useDisconnect, useNetwork, useSignMessage } from 'wagmi';
import { useEffect, useState } from "react";
import { getCsrfToken, signIn, signOut, useSession } from 'next-auth/react';
import { SiweMessage } from 'siwe';
type Props = {
}
export default function Navbar(props: Props) {
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 is used from CSRF token
nonce: await getCsrfToken(),
});
const signature = await signMessageAsync({
message: message.prepareMessage(),
});
signIn('credentials', {
message: JSON.stringify(message),
redirect: false,
signature,
});
// document.cookie = "_csrf=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
} 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">
{/* Connect Wallet button goes here. */}
<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>
)
}
In terminal
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'
}
}
}
Versions:
"siwe": "^2.1.4",
"next": "13.4.19",
"next-auth": "^4.23.1",
On the client side, I consistently receive the same nonce every time I call getCsrfToken(). However, on the server side, it generates a different CSRF token.
Thank you in advance!
Not the ideal solution, but after reading and looking at a ton of different reports and articles.
One being: Reddit
I came up with a similar solution:
Mainly just pulling the csrf from the cookie rather:
const csrf = cookies().get('next-auth.csrf-token')?.value.split('|')[0]
authorize: async (credentials) => {
try {
const csrf = cookies().get('next-auth.csrf-token')?.value.split('|')[0]
const siwe = new SiweMessage(JSON.parse(credentials?.message || '{}'))
const nextAuthUrl = new URL(process.env.NEXTAUTH_URL!)
if (siwe.domain !== nextAuthUrl.host) {
return null
}
// siwe will validate that the message is signed by the address
const verifyParams = {
signature: credentials?.signature || '',
nonce: csrf,
};
const verifyOpts = {};
// const fields = await siwe.validate(credentials?.signature || "");
const verificationResult = await siwe.verify(
verifyParams,
verifyOpts
);
// if (fields.nonce !== nonce) {
if (!verificationResult.success) {
return null;
}
return {
id: siwe.address,
}
} catch (error) {
// Uncomment or add logging if needed
console.error({ error });
return null;
}
},
}),