Web 3 authentication example
This example shows how to implement Web3 authentication using NextAuth.js, Wagmi and SIWE. The Sign-In With Ethereum is a pattern aimed at standardizing the way sessions are handled when connecting a cryptocurrency wallet to an application.
Following this pattern in Next.js helps us build our application using the full capacities of Next.js as we now have access to users wallet both in the browser and on the server.
Not connected
Login
We'll be using NextAuth.js
to save the user session in a JWT. In order to do so, we'll have to configure NextAuth.js:
// pages/api/auth/[...nextauth].ts
import type { NextApiRequest, NextApiResponse } from 'next'
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { getCsrfToken } from 'next-auth/react'
import { SiweMessage } from 'siwe'
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
const providers = [
CredentialsProvider({
name: 'Ethereum',
credentials: {
message: {
label: 'Message',
type: 'text',
placeholder: '0x0',
},
signature: {
label: 'Signature',
type: 'text',
placeholder: '0x0',
},
},
async authorize(credentials) {
try {
if (!process.env.NEXTAUTH_URL) {
throw 'NEXTAUTH_URL is not set'
}
// the siwe message follows a predictable format
const siwe = new SiweMessage(JSON.parse(credentials?.message || '{}'))
const nextAuthUrl = new URL(process.env.NEXTAUTH_URL)
if (siwe.domain !== nextAuthUrl.host) {
return null
}
// validate the nonce
if (siwe.nonce !== (await getCsrfToken({ req }))) {
return null
}
// siwe will validate that the message is signed by the address
await siwe.validate(credentials?.signature || '')
return {
id: siwe.address,
}
} catch (e) {
return null
}
},
}),
]
const isDefaultSigninPage =
req.method === 'GET' && req.query.nextauth.includes('signin')
if (isDefaultSigninPage) {
providers.pop()
}
return await NextAuth(req, res, {
providers,
session: {
strategy: 'jwt',
},
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
// after a user is logged in, we can keep the address in session
async session({ session, token }) {
session.address = token.sub
session.user!.name = token.sub
return session
},
},
})
}
This will give us access to implementing our login function
import { getCsrfToken, signIn, useSession, signOut } from 'next-auth/react'
import { SiweMessage } from 'siwe'
import { useAccount, useConnect, useNetwork, useSignMessage } from 'wagmi'
function Home() {
// contains the JWT session from NextAuth.js
const session = useSession()
// Wagmi hooks to interact with Metamask
const [{ data: connectData }, connect] = useConnect()
const [, signMessage] = useSignMessage()
const [{ data: networkData }] = useNetwork()
const [{ data: accountData }] = useAccount()
const handleLogin = async () => {
try {
await connect(connectData.connectors[0])
const callbackUrl = '/protected'
const message = new SiweMessage({
domain: window.location.host,
address: accountData?.address,
statement: 'Sign in with Ethereum to the app.',
uri: window.location.origin,
version: '1',
chainId: networkData?.chain?.id,
nonce: await getCsrfToken(),
})
const { data: signature, error } = await signMessage({
message: message.prepareMessage(),
})
signIn('credentials', {
message: JSON.stringify(message),
redirect: false,
signature,
callbackUrl,
})
} catch (error) {
window.alert(error)
}
}
const handleLogout = async () => {
signOut({ redirect: false })
}