Skip to main content

Sign In With Ethereum

AppKit provides a simple solution for integrating with "Sign In With Ethereum" (SIWE), a new form of authentication that enables users to control their digital identity with their Ethereum account. SIWE is a standard also known as EIP-4361.

One-Click Auth

One-Click Auth represents a key advancement within WalletConnect v2, streamlining the user authentication process in AppKit by enabling them to seamlessly connect with a wallet and sign a SIWE message with just one click.

Connecting a wallet, proving control of an address with an off-chain signature, authorizing specific actions. These are the kinds of authorizations that can be encoded as "ReCaps". ReCaps are permissions for a specific website or dapp that can be compactly encoded as a long string in the message you sign and translated by any wallet into a straight-forward one-sentence summary. WalletConnect uses permissions expressed as ReCaps to enable a One-Click Authentication.

Installation

npm i @web3modal/siwe

Configure your SIWE Client

import { SiweMessage } from 'siwe'
import {
type SIWESession,
type SIWEVerifyMessageArgs,
type SIWECreateMessageArgs,
createSIWEConfig,
formatMessage,
} from '@web3modal/siwe'

const BASE_URL = 'http://localhost:8080';

/* Function that returns the user's session - this should come from your SIWE backend */
async function getSession(){
const res = await fetch(BASE_URL + "/session", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
credentials: 'include',
});
if (!res.ok) {
throw new Error('Network response was not ok');
}

const data = await res.json();
return data == "{}" ? null : data as SIWESession;
}

/* Use your SIWE server to verify if the message and the signature are valid */
const verifyMessage = async ({ message, signature }: SIWEVerifyMessageArgs) => {
try {
const response = await fetch(BASE_URL + "/verify", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
mode: 'cors',
body: JSON.stringify({ message, signature }),
credentials: 'include'
});

if (!response.ok) {
return false;
}

const result = await response.json();
return result === true;
} catch (error) {
return false;
}
}

// Check the full example for signOut and getNonce functions ...

/* Create a SIWE configuration object */
const siweConfig = createSIWEConfig({
getMessageParams: async () => ({
domain: window.location.host,
uri: window.location.origin,
chains: [1, 2020],
statement: 'Please sign with your account',
}),
createMessage: ({ address, ...args }: SIWECreateMessageArgs) => formatMessage(args, address),
getNonce,
getSession,
verifyMessage,
signOut,
})

Server Side

Setting up a backend server using Express for a web application that interacts with the Siwe protocol.

Routes:

  • GET '/nonce': Generates and returns a nonce (single-use random number).
  • POST '/verify': Uses the Siwe protocol to verify the message, requiring a signature (the one you are going to approve throw the UX) and a nonce stored in the session.
  • GET '/session': Retrieves the stored Siwe object from the session.
  • GET '/signout': Clears the session.
import cors from 'cors';
import express from 'express';
import Session from 'express-session';
import { generateNonce, SiweMessage } from 'siwe';

const app = express();

// configure cors and sessions
app.use(cors({
origin: 'http://localhost:5173', // frontend URL
credentials: true,
}))
app.use(express.json())
app.use(Session({
name: 'siwe-quickstart',
secret: "siwe-quickstart-secret",
resave: true,
saveUninitialized: true,
cookie: { secure: false, sameSite: true }
}));

app.get('/nonce', function (_, res) {
res.setHeader('Content-Type', 'text/plain');
console.log("/nonce");
res.send(generateNonce());
});

// verify the message
app.post('/verify', async (req, res) => {
try {
if (!req.body.message) {
return res.status(400).json({ error: 'SiweMessage is undefined' });
}
let SIWEObject = new SiweMessage(req.body.message);
const { data: message } = await SIWEObject.verify({ signature: req.body.signature, nonce: req.session.nonce });

const address = message.address;
const chainId = message.chainId;

// save the session with the address and chainId (SIWESession)
req.session.siwe = { address, chainId };
req.session.save(() => res.status(200).send(true));
} catch (e) {
// clean the session
req.session.siwe = null;
req.session.nonce = null;
req.session.save(() => res.status(500).json({ message: e.message }));
}
});

/// ... check the github repository for the others endpoints

// get the session
app.get('/session', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send(req.session.siwe);
});

Check the github full example to see the full flow working: siwe-quickstart

verifySignature

Verify a SIWE signature.

import { verifySignature } from '@web3modal/siwe'

const isValid = await verifySignature({ address, message, signature, chainId, projectId })

Initialize AppKit with your siweConfig

Add the siweConfig to your createWeb3Modal parameters

createWeb3Modal({
//..
siweConfig
})

SIWE Config Parameters

getMessageParams () => Promise<{ domain: string, uri: string, chains: number[], statement: string }>

Parameters to create the SIWE message internally.

getNonce () => Promise<string>

The getNonce method functions as a safeguard against spoofing, akin to a CSRF token. The siwe package provides a generateNonce() helper, or you can utilize an existing CSRF token from your backend if available.

createMessage (args: SIWECreateMessageArgs) => string

The official siwe package offers a straightforward method for generating an EIP-4361-compatible message, which can subsequently be authenticated using the same package. The nonce parameter is derived from your getNonce endpoint, while the address and chainId variables are sourced from the presently connected wallet.

verifyMessage (args: SIWEVerifyMessageArgs) => Promise<boolean>

The function to ensure the message is valid, has not been tampered with, and has been appropriately signed by the wallet address.

getSession () => Promise<SIWESession | null>

The backend session should store the associated address and chainId and return it via the getSession method.

signOut () => Promise<boolean>

The users session can be destroyed calling signOut.

onSignIn (session?: SIWESession) => void

Callback when user signs in (Optional).

onSignOut () => void

Callback when user signs out (Optional).

signOutOnDisconnect boolean

  • defaults to true

Whether or not to sign out when the user disconnects their wallet (Optional).