Sign-In with Ethereum

On this blog, we have previously discussed Web3 authentication. This post offered technical insight into connecting Metamask to Next.js. It helps however to take a step back and understand efforts to standardize Web3 authentication.

So far, the Web3 community has been fragmented around multiple wallets and authentication methods. However, recently a proposal has been picking up some traction. The proposal is called Sign-In with Ethereum. To understand the need for such a standard, it helps to understand the state of digital identity.

What is a digital identity?

Everywhere you go, you have a digital identity. This identity is the collection of data that represents your activity online. It is difficult to stay truly anonymous online in this era of social media.

If you are present on a social like Facebook or TikTok, you probably have seen disturbing advertisements after visiting a website seemingly unrelated to those platforms. This is because websites implement a variety of cross-site tracking technologies, identifying you across the Internet.

You do not have ownership of your digital identity in most services you consume

This statement is not intended to scare you. It is meant to help you realize the importance of your digital identity. The data you create has a value, and you have a right to control it and to own it. This is a core idea behind Sign-In with Ethereum.

Overview of the Sign-In with Ethereum proposal

The idea behind the proposal is to standardize the way you authenticate with a digital identity. The following steps are followed:

  1. A server creates a unique number called a Nonce for each user that will sign-in.
  2. A user requests to connect a site with their wallet.
  3. The user is presented with a unique message that indicates the Nonce and information about the website.
  4. The user signs in with their wallet.
  5. The user is then authenticated and the website is given access to data about the user that was approved.

For your users, this can be implemented pretty simply as this demo shows:

The best advantage of this proposal is a clear and well communicated process for users. They are given clearly communicated information about the process and the data they are giving.

Implementing Sign-In with Ethereum

Implementing Sign-In with Ethereum is not as simple as it sounds. You will need to understand the following:

  • Generating a unique nonce for each user
  • Encrypting a JWT for each session
  • Signing messages from user's wallets.

This is a complex endeavour and requires a lot of technical knowledge. We'll guide you through the process of implementing Sign-In with Ethereum.

You should use a Full Stack framework automatically when building in Web3!

Do not assume that everything happens only client-side! We've touched on this topic more than once in the past. A good user experience will often require a server-side implementation for topics like authentication, even in Web3.

You can find my full reference implementation of Sign-In with Ethereum here. This implementation uses the following libraries:

To setup the authentication backend, we are creating a pages/api/auth/[...nextauth].ts file with the following content:

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

Once all is setup, we can implement the following Sign In functions:

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

Conclusion

Sign-In with Ethereum is a step in the right direction. It lets you build a user experience that is more secure and more intuitive for your users in Web3.

You can always support this effort by following me on Twitter!