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.


Please install Metamask to use this example

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 })
  }