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:
- Connect wallet
- Sign SIWE message with nonce generated by backend
- Verify submitted SIWE message and signature via POST request
- 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 { activeChain } = 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 = activeChain?.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 connectAsync(connector) // `connectAsync` from `useConnect`
const nonceRes = await fetch('/api/nonce')
const message = new SiweMessage({
domain: window.location.host,
address: res.account,
statement: 'Sign in with Ethereum to the app.',
uri: window.location.origin,
version: '1',
chainId: res.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
- Sign-In with Ethereum to Next.js Applications Guide for setting up SIWE with
next-auth
from the Spruce team.