Skip to content
Examples
/
Sign-In with Ethereum

Sign-In with Ethereum

Sign-In with Ethereum is an authentication standard (EIP-4361) that enables secure communication between a frontend and backend. SIWE is a powerful method for creating user sessions based on a wallet connection, and much more!

The example below builds on the Connect Wallet and Sign Message examples. Try it out before moving on.

Pretty cool, right?! You can refresh the window or disconnect your wallet, and you are still securely logged in.

Overview

Implementing SIWE only takes four steps:

  1. Connect wallet
  2. Sign SIWE message with nonce generated by backend
  3. Verify submitted SIWE message and signature via POST request
  4. Add validated SIWE fields to session (via JWT, cookie, etc.)
💡

This guide uses Next.js API Routes for the backend and iron-session to secure the user session, but you can also use other backend frameworks and storage methods.

Prerequisites

Install siwe and iron-session with your package manager of choice:

npm install siwe iron-session
iron-session TypeScript Set Up

In order for TypeScript to work properly with iron-session and siwe, you need to add a couple properties to the IronSessionData interface. Add the following to types/iron-session/index.d.ts.

import 'iron-session'
import { SiweMessage } from 'siwe'

declare module 'iron-session' {
  interface IronSessionData {
    nonce?: string
    siwe?: SiweMessage
  }
}

Then, update your tsconfig.json to include the custom types directory:

{
  "compilerOptions": {
    // ...
  },
-  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types/**/*.d.ts"],
  "exclude": ["node_modules"]
}

Step 1: Connect Wallet

Follow the Connect Wallet guide to get this set up.

Step 2: Add API routes

First, create an API route for generating a random nonce. This is used to identify the session and prevent against replay attacks.

pages/api/nonce.ts

import { withIronSessionApiRoute } from 'iron-session/next'
import { NextApiRequest, NextApiResponse } from 'next'
import { generateNonce } from 'siwe'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const { method } = req
  switch (method) {
    case 'GET':
      req.session.nonce = generateNonce()
      await req.session.save()
      res.setHeader('Content-Type', 'text/plain')
      res.send(req.session.nonce)
      break
    default:
      res.setHeader('Allow', ['GET'])
      res.status(405).end(`Method ${method} Not Allowed`)
  }
}

export default withIronSessionApiRoute(handler, ironOptions)

Next, add an API route for verifying a SIWE message and creating the user session.

pages/api/verify.ts

import { withIronSessionApiRoute } from 'iron-session/next'
import { NextApiRequest, NextApiResponse } from 'next'
import { SiweMessage } from 'siwe'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const { method } = req
  switch (method) {
    case 'POST':
      try {
        const { message, signature } = req.body
        const siweMessage = new SiweMessage(message)
        const fields = await siweMessage.validate(signature)

        if (fields.nonce !== req.session.nonce)
          return res.status(422).json({ message: 'Invalid nonce.' })

        req.session.siwe = fields
        await req.session.save()
        res.json({ ok: true })
      } catch (_error) {
        res.json({ ok: false })
      }
      break
    default:
      res.setHeader('Allow', ['POST'])
      res.status(405).end(`Method ${method} Not Allowed`)
  }
}

export default withIronSessionApiRoute(handler, ironOptions)

ironOptions should look something like this:

{
  cookieName: 'siwe',
  password: 'complex_password_at_least_32_characters_long',
  cookieOptions: {
    secure: process.env.NODE_ENV === 'production',
  },
}

Finally, add two simple API routes for retrieving the signed-in user:

pages/api/me.ts

import { withIronSessionApiRoute } from 'iron-session/next'
import { NextApiRequest, NextApiResponse } from 'next'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const { method } = req
  switch (method) {
    case 'GET':
      res.send({ address: req.session.siwe?.address })
      break
    default:
      res.setHeader('Allow', ['GET'])
      res.status(405).end(`Method ${method} Not Allowed`)
  }
}

export default withIronSessionApiRoute(handler, ironOptions)

And logging out:

pages/api/logout.ts

import { withIronSessionApiRoute } from 'iron-session/next'
import { NextApiRequest, NextApiResponse } from 'next'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const { method } = req
  switch (method) {
    case 'GET':
      req.session.destroy()
      res.send({ ok: true })
      break
    default:
      res.setHeader('Allow', ['GET'])
      res.status(405).end(`Method ${method} Not Allowed`)
  }
}

export default withIronSessionApiRoute(handler, ironOptions)

Step 3: Sign & Verify Message

Now that the connect wallet logic and API routes are set up, we can sign in the user! We'll create a new SiweMessage and sign it using the useSignMessage hook. We can also add a log out button and a side effect for fetching the logged in user when the page loads or window gains focus.

import * as React from 'react'
import { useAccount, useNetwork, useSignMessage } from 'wagmi'
import { SiweMessage } from 'siwe'

export function Profile() {
  const { data: accountData } = useAccount()
  const { data: networkData } = useNetwork()

  const [state, setState] = React.useState<{
    address?: string
    error?: Error
    loading?: boolean
  }>({})
  const { signMessageAsync } = useSignMessage()

  const signIn = React.useCallback(async () => {
    try {
      const address = accountData?.address
      const chainId = networkData?.chain?.id
      if (!address || !chainId) return

      setState((x) => ({ ...x, error: undefined, loading: true }))
      // Fetch random nonce, create SIWE message, and sign with wallet
      const nonceRes = await fetch('/api/nonce')
      const message = new SiweMessage({
        domain: window.location.host,
        address,
        statement: 'Sign in with Ethereum to the app.',
        uri: window.location.origin,
        version: '1',
        chainId,
        nonce: await nonceRes.text(),
      })
      const signature = await signMessageAsync({ message: message.prepareMessage() })

      // Verify signature
      const verifyRes = await fetch('/api/verify', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message, signature }),
      })
      if (!verifyRes.ok) throw new Error('Error verifying message')

      setState((x) => ({ ...x, address, loading: false }))
    } catch (error) {
      setState((x) => ({ ...x, error, loading: false }))
    }
  }, [])

  // Fetch user when:
  React.useEffect(() => {
    const handler = async () => {
      try {
        const res = await fetch('/api/me')
        const json = await res.json()
        setState((x) => ({ ...x, address: json.address }))
      } catch (_error) {}
    }
    // 1. page loads
    handler()

    // 2. window is focused (in case user logs out of another window)
    window.addEventListener('focus', handler)
    return () => window.removeEventListener('focus', handler)
  }, [])

  if (accountData) {
    return (
      <div>
        {/* Account content goes here */}

        {state.address ? (
          <div>
            <div>Signed in as {state.address}</div>
            <button
              onClick={async () => {
                await fetch('/api/logout')
                setState({})
              }}
            >
              Sign Out
            </button>
          </div>
        ) : (
          <button disabled={state.loading} onClick={signIn}>
            Sign-In with Ethereum
          </button>
        )}
      </div>
    )
  }

  return <div>{/* Connect wallet content goes here */}</div>
}

Wrap Up

That's it! You now have a way for users to securely sign in to an app using Ethereum wallets. You can start building rich web apps that use persistent user sessions while still letting users control their login identity (and so much more). Check out the Sign-In with Ethereum website for more info.

Combine "Connect Wallet" and "Sign & Verify Message" Steps

This guide splits SIWE into two steps: Connect wallet and sign SIWE message. If you want to combine both steps, sign and verify the SIWE message using the connector, immediately after connecting.

try {
  const res = await connect(connector) // connect from useConnect
  if (!res.data) throw res.error ?? new Error('Something went wrong')

  const nonceRes = await fetch('/api/nonce')
  const message = new SiweMessage({
    domain: window.location.host,
    address: res.data.account,
    statement: 'Sign in with Ethereum to the app.',
    uri: window.location.origin,
    version: '1',
    chainId: res.data.chain?.id,
    nonce: await nonceRes.text(),
  })

  const signer = await connector.getSigner()
  const signature = await signer.signMessage(message.prepareMessage())

  const verifyRes = await fetch('/api/verify', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ message, signature }),
  })
  if (!verifyRes.ok) throw new Error('Error verifying message')

  // It worked! User is signed in with Ethereum
  // wagmi
} catch (error) {
  // Do something with the error
  // ngmi
}

Additional Resources