Sign-In with Ethereum to Next.js Applications

In our continued updates on additional support for different languages and frameworks for Sign-In with Ethereum, we're pleased to announce our guide on how to implement Sign-In with Ethereum to authenticate users in your Next.js app by using NextAuth.

Sign-In with Ethereum to Next.js Applications

In our continued updates on additional support for different languages and frameworks for Sign-In with Ethereum, we're pleased to announce our guide on how to implement Sign-In with Ethereum to authenticate users in your Next.js app by using NextAuth:

NextAuth.js - Sign-In with Ethereum
A complete open source authentication solution.

NextAuth.js is an easy-to-implement, full-stack (client/server) open-source authentication library originally designed for Next.js and serverless applications. This guide also features the use of our core library alongside wagmi, which is a React hooks library for Ethereum.

The completed example can be found here.

Building the Example

Requirements

Getting Started

  • First clone the official NextAuth.js example using your terminal:
git clone https://github.com/nextauthjs/next-auth-example
  • After cloning, modify the given .env.local.example file, and populate it with the following variables:
NEXTAUTH_SECRET=somereallysecretsecret
JWT_SECRET=itshouldbealsoverysecret
DOMAIN=localhost:3000

Note: After this, rename the file to .env. This example will be routed to http://localhost:3000.

  • Next Add siwe and wagmi as dependencies. In this example, we're using wagmi, which is a well-known React hooks library for Ethereum. In your terminal, navigate to the project we originally cloned and add the dependencies via the following commands:
cd next-auth-example
yarn add siwe wagmi
  • Now, modify pages/_app.tsx to inject the WagmiProvider component:
import { SessionProvider } from "next-auth/react"
import type { AppProps } from "next/app"
import "./styles.css"
import { WagmiProvider } from "wagmi"

// Use of the <SessionProvider> is mandatory to allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({ Component, pageProps }: AppProps) {
  return (
    <WagmiProvider autoConnect>
      <SessionProvider session={pageProps.session} refetchInterval={0}>
        <Component {...pageProps} />
      </SessionProvider>
    </WagmiProvider>
  )
}
  • We're going to now add the provider that will handle the message validation. Since it's not possible to sign in using the default page, the original provider should be removed from the list of providers before rendering. Modify pages/api/auth/[...nextauth].ts with the following:
import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import { getCsrfToken } from "next-auth/react"
import { SiweMessage } from "siwe"

// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export default async function auth(req, res) {
  const providers = [
    CredentialsProvider({
      name: "Ethereum",
      credentials: {
        message: {
          label: "Message",
          type: "text",
          placeholder: "0x0",
        },
        signature: {
          label: "Signature",
          type: "text",
          placeholder: "0x0",
        },
      },
      async authorize(credentials) {
        try {
          const siwe = new SiweMessage(JSON.parse(credentials?.message || "{}"))
          const domain = process.env.DOMAIN
          if (siwe.domain !== domain) {
            return null
          }

          if (siwe.nonce !== (await getCsrfToken({ req }))) {
            return null
          }

          await siwe.validate(credentials?.signature || "")
          return {
            id: siwe.address,
          }
        } catch (e) {
          return null
        }
      },
    }),
  ]

  const isDefaultSigninPage =
    req.method === "GET" && req.query.nextauth.includes("signin")

  // Hides Sign-In with Ethereum from default sign page
  if (isDefaultSigninPage) {
    providers.pop()
  }

  return await NextAuth(req, res, {
    // https://next-auth.js.org/configuration/providers/oauth
    providers,
    session: {
      strategy: "jwt",
    },
    jwt: {
      secret: process.env.JWT_SECRET,
    },
    secret: process.env.NEXT_AUTH_SECRET,
    callbacks: {
      async session({ session, token }) {
        session.address = token.sub
        session.user.name = token.sub
        return session
      },
    },
  })
}
  • The default sign-in page can't be used because there is no way to hook wagmi to listen for clicks on the default sign-in page provided by next-auth, so a custom page must be created to handle the sign-in flow. Create pages/siwe.tsx and populate it with the following:
import { getCsrfToken, signIn } from 'next-auth/react'
import { SiweMessage } from 'siwe'
import { useAccount, useConnect, useNetwork, useSignMessage } from 'wagmi'
import Layout from "../components/layout"


function Siwe() {
  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)
    }
  }

  return (
    <Layout>
    <button
      onClick={(e) => {
        e.preventDefault()
        handleLogin()
      }}
    >
      Sign-In with Ethereum
    </button>
    </Layout>
  )
}

Siwe.Layout = Layout

export default Siwe
  • Finally, modify the components/header.tsx in order to clean it up and add a SIWE tab to navigate to the newly created page:
import { signIn, signOut, useSession } from "next-auth/react"
import Link from "next/link"
import styles from "./header.module.css"

// The approach used in this component shows how to build a sign in and sign out
// component that works on pages which support both client and server side
// rendering, and avoids any flash incorrect content on initial page load.
export default function Header() {
  const { data: session, status } = useSession()
  const loading = status === "loading"

  return (
    <header>
      <noscript>
        <style>{`.nojs-show { opacity: 1; top: 0; }`}</style>
      </noscript>
      <div className={styles.signedInStatus}>
        <p
          className={`nojs-show ${
            !session && loading ? styles.loading : styles.loaded
          }`}
        >
          {!session && (
            <>
              <span className={styles.notSignedInText}>
                You are not signed in
              </span>
            </>
          )}
          {session?.user && (
            <>
              {session.user.image && (
                <span
                  style={{ backgroundImage: `url('${session.user.image}')` }}
                  className={styles.avatar}
                />
              )}
              <span className={styles.signedInText}>
                <small>Signed in as</small>
                <br />
                <strong>{session.user.email ?? session.user.name}</strong>
              </span>
              <a
                href={`/api/auth/signout`}
                className={styles.button}
                onClick={(e) => {
                  e.preventDefault()
                  signOut()
                }}
              >
                Sign out
              </a>
            </>
          )}
        </p>
      </div>
      <nav>
        <ul className={styles.navItems}>
          <li className={styles.navItem}>
            <Link href="/">
              <a>Home</a>
            </Link>
          </li>
          <li className={styles.navItem}>
            <Link href="/siwe">
              <a>SIWE</a>
            </Link>
          </li>
        </ul>
      </nav>
    </header>
  )
}
  • Run the application using the following commands:
yarn install
yarn dev

Navigate to localhost:3000 - now you are now ready to Sign-In with Ethereum. Just click the SIWE link in the header, hit the "Sign-In with Ethereum" button, sign the message, and you are now authenticated!


If you're interested in integrating Sign-In with Ethereum into your dapp, app, or service, we are more than happy to help and provide any support we can.

As we continue our work supporting Sign-In with Ethereum, we especially welcome implementers who already have users relying on similar workflows, authors of related EIPs, and wallet vendors who would like to do more to support user-owned identities to join us.

If you are interested in being involved or integrating this plugin, please join our Discord server: