How to sign a message and verify it on server side

In this post we will see how to sign a message with your private key and verify it on server side. This allow you to check that the message you received on the server on the behalf of a user is really from this user.

Imagine you have a web application where users can publish messages on a public wall. If someone ask your backend app to publish a message on the wall, you want to be sure that the message is really from the user who ask to publish it. To do that the user need to send a request to your backend app with the signed message and the address of the user. Your backend will then verify the signature and if it's valid, it will publish the message on the wall.

An other use case is after a user connect his wallet to your app and before interacting with your backend you ask him to authenticate himself with a signed message. This allow you to be sure that the user is really the owner of the wallet he connected to your app.

In this post we will implement a basic version of the second use case.

Configure app and sign message

First create a new next application with the following command:

npx create-next-app --example with-tailwdindcss sign-message-sample

Then install and configure wagmi, RainbowKit and ethers.js :

yarn add @rainbow-me/rainbowkit wagmi ethers

// pages/_app.tsx
import '../styles/globals.css'
import '@rainbow-me/rainbowkit/styles.css'

import { RainbowKitProvider, getDefaultWallets } from '@rainbow-me/rainbowkit'
import { WagmiConfig, configureChains, createClient } from 'wagmi'
import { arbitrum, mainnet, optimism, polygon } from 'wagmi/chains'

import type { AppProps } from 'next/app'
import { alchemyProvider } from 'wagmi/providers/alchemy'
import { publicProvider } from 'wagmi/providers/public'

const { chains, provider } = configureChains(
  [mainnet],
  [
    alchemyProvider({ apiKey: process.env.ALCHEMY_ID as string }),
    publicProvider(),
  ]
)

const { connectors } = getDefaultWallets({
  appName: 'Connect Wallet App',
  chains,
})

const wagmiClient = createClient({
  autoConnect: true,
  connectors,
  provider,
})

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <WagmiConfig client={wagmiClient}>
      <RainbowKitProvider chains={chains}>
        <Component {...pageProps} />;
      </RainbowKitProvider>
    </WagmiConfig>
  )
}

export default MyApp

To be able to sign a message we use the useSignMessage hook from wagmi.

import { useSignMessage } from 'wagmi'

function App() {
  const { data, isError, isLoading, isSuccess, signMessage } = useSignMessage({
    message: 'gm wagmi frens',
  })

  return (
    <div>
      <button disabled={isLoading} onClick={() => signMessage()}>
        Sign message
      </button>
      {isSuccess && <div>Signature: {data}</div>}
      {isError && <div>Error signing message</div>}
    </div>
  )
}

This is pretty straightforward, we just need to pass the message we want to sign to the useSignMessage hook and call the signMessage function when we want to sign the message. But in our use case we want to sign the message,then send the signature to the server and diplay the result of the verification.

To do that we need to use the useSignMessage hook in a different way with signMessageAsync function wich is a promise we can use in a function.

const { signMessageAsync } = useSignMessage()
const authenticate = async () => {
  const signed = await signMessageAsync({
    message: `message to sign`,
  })
}

We can now create a hook hooks/useAuthenticate.ts to handle the authentication process.

import * as React from 'react'

import { useAccount, useSignMessage } from 'wagmi'

export function useAuthenticate() {
  const { address } = useAccount()
  const { signMessageAsync } = useSignMessage()
  const [error, setError] = React.useState<string | null>(null)
  const [success, setSuccess] = React.useState<string | null>(null)
  const [pending, setPending] = React.useState(false)

  const authenticate = async () => {
    try {
      setPending(true)
      setSuccess(null)
      setError(null)

      const signed = await signMessageAsync({
        message: `authenticate:${address}`,
      })

      const response = await fetch('/api/authenticate', {
        method: 'POST',
        body: JSON.stringify({ signed, address }),
        headers: { 'Content-Type': 'application/json' },
      }).then((res) => res.json())

      response.success
        ? setSuccess(`${address} authenticated`)
        : setError(`Invalid signature`)
      setPending(false)
    } catch (error: any) {
      setPending(false)
      setError(error.toString())
    }
  }

  return {
    success,
    pending,
    error,
    authenticate,
  }
}

In this hook first we use the useAccount hook to get the address of the connected wallet. Then we use the useSignMessage hook to get the signMessageAsync function. We create a state to handle the authentication process and a function authenticate to call the signMessageAsync function and send the signature to the server.

Verify signature on server side

To do that we need use the verifyMessage function from ethers.js. It take two parameters the message to verify and the signature of this message. In our case this will be:

const signerAddress = verifyMessage(`authenticate:${address}`, signed)

The function returns the address that signed message. If the address is the same as the address of the connected wallet then the signature is valid.

Let's implement an this logic, create an api route pages/api/authenticate.ts to handle the authentication request.

import type { NextApiRequest, NextApiResponse } from 'next'

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { verifyMessage } from 'ethers/lib/utils.js'

type Data =
  | {
      success: boolean
    }
  | { error: string }

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  if (req.method === 'POST') {
    const { address, signed } = req.body
    try {
      if (!address || !signed) {
        throw new Error('invalid body')
      }
      const signerAddress = verifyMessage(`authenticate:${address}`, signed)
      res.status(201).json({ success: signerAddress === address })
    } catch (error: any) {
      console.error(error.toString())
      res.status(400).json({ error: 'bad request' })
    }
  } else {
    res.status(400).json({ error: 'bad request' })
  }
}

This api route expect to receive the address of the connected wallet and the signature of the message from the request body. Then it verify the signature and return true if the signature is valid.

Display the result

Now that we have the authentication process implemented we can display the result in our app. In pages/index.tsx add this :

import { ConnectButton } from '@rainbow-me/rainbowkit'
import ConnectWallet from '../components/Authentication'
import Head from 'next/head'
import Image from 'next/image'
import type { NextPage } from 'next'
import { useAccount } from 'wagmi'
import { useIsMounted } from '../hooks/useIsMounted'

const Home: NextPage = () => {
  const mounted = useIsMounted()
  const { isConnected } = useAccount()
  if (!mounted) return null
  return (
    <div className="flex min-h-screen flex-col items-center justify-center py-2">
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className="flex flex-1 flex-col items-center justify-center space-y-2 px-20 text-center">
        <ConnectButton />
        {isConnected && <ConnectWallet />}
      </main>
    </div>
  )
}

export default Home

And voila we have a working authentication process by signing and checking signature of message. This is just a simple example if you want to implement this in production your can use a better implementation like Sign-In with Ethereum . The code source is available on here