WritingSign-In With Ethereum (SIWE): How to Implement Wallet-Based Authentication — Clixo
5 min readsiwe, sign-in-with-ethereum, authentication, web3, wallet

Sign-In With Ethereum (SIWE): How to Implement Wallet-Based Authentication

A practical implementation guide for Sign-In With Ethereum (SIWE), covering message construction, signature verification, session management, and security considerations.

Asking users to set a password for a Web3 product is incongruous. They already have a cryptographic identity — their wallet — and asking them to create a separate credential is friction they should not have to absorb. Sign-In With Ethereum (SIWE, EIP-4361) formalizes wallet-based authentication into a standard that is secure, verifiable, and increasingly supported by auth providers out of the box.

Here is how it works and how to implement it correctly in a production application.

What SIWE Is

SIWE is an open standard (EIP-4361) for using an Ethereum wallet signature as an authentication mechanism. Instead of a password, the user signs a structured human-readable message that asserts their wallet address, the domain they are signing into, and a nonce that prevents replay attacks. Your server verifies the signature against the message and, if valid, establishes a session.

The signed message is human-readable, which is intentional. Users can read exactly what they are authorizing before confirming the signature. This transparency is one reason SIWE is trusted more than opaque eth_sign requests.

Sign-In With Ethereum Implementation: The Core Flow

1. Generate a Nonce

The nonce prevents an intercepted SIWE message from being replayed to authenticate a different session. Generate a cryptographically random nonce on your server and associate it with the pending sign-in attempt.

// Server-side
import { generateNonce } from 'siwe';
 
app.get('/auth/nonce', (req, res) => {
  const nonce = generateNonce();
  req.session.nonce = nonce; // store in session, expires in ~5 minutes
  res.json({ nonce });
});

2. Construct the SIWE Message on the Client

import { SiweMessage } from 'siwe';
 
async function buildSiweMessage(address: string, chainId: number, nonce: string) {
  const message = new SiweMessage({
    domain: window.location.host,      // your domain — must match server validation
    address,
    statement: 'Sign in to Clixo App',
    uri: window.location.origin,
    version: '1',
    chainId,
    nonce,
  });
  return message.prepareMessage();
}

The domain field is critical for security. The verifying server must confirm that the domain in the signed message matches the server's own domain. A SIWE message signed for attacker.com must not be accepted by yourapp.com.

3. Request the Signature

import { useWalletClient } from 'wagmi';
 
async function signIn() {
  const { data: walletClient } = useWalletClient();
  const { address } = useAccount();
  const { chainId } = useChainId();
 
  // Fetch nonce from your server
  const { nonce } = await fetch('/auth/nonce').then(r => r.json());
 
  // Build and sign the message
  const message = await buildSiweMessage(address, chainId, nonce);
  const signature = await walletClient.signMessage({ message });
 
  // Send to your server for verification
  await fetch('/auth/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message, signature }),
  });
}

4. Verify the Signature on the Server

import { SiweMessage } from 'siwe';
 
app.post('/auth/verify', async (req, res) => {
  const { message, signature } = req.body;
 
  const siweMessage = new SiweMessage(message);
 
  let verified;
  try {
    verified = await siweMessage.verify({
      signature,
      nonce: req.session.nonce,       // must match what was issued
      domain: 'yourapp.com',          // must match your actual domain
    });
  } catch (error) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
 
  if (!verified.success) {
    return res.status(401).json({ error: 'Verification failed' });
  }
 
  // Clear the nonce — it is single-use
  req.session.nonce = null;
 
  // Establish an authenticated session
  req.session.address = verified.data.address;
  req.session.chainId = verified.data.chainId;
 
  res.json({ address: verified.data.address });
});

5. Protect Routes with the Session

Any route requiring authentication checks the session for an authenticated address:

function requireAuth(req, res, next) {
  if (!req.session.address) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  next();
}
 
app.get('/api/profile', requireAuth, (req, res) => {
  res.json({ address: req.session.address });
});

Security Requirements

Nonces must be single-use. Clear the nonce from the session immediately after a successful or failed verification attempt. A reused nonce allows replay attacks.

Domain validation is not optional. Always pass domain to siweMessage.verify(). A missing domain check means a SIWE signature intended for any site can authenticate to yours.

Set nonce expiry. Nonces should expire within a few minutes. Store them with a timestamp and reject any verification where the nonce is older than your defined window (5-10 minutes is typical).

Store addresses in lowercase. Ethereum addresses are case-insensitive, but comparisons can fail if one is mixed-case (EIP-55 checksum) and the other is lowercase. Normalize to lowercase on storage and comparison.

Session security applies. SIWE establishes identity, but your session management (cookie flags, secure transport, CSRF protection) must be as secure as any standard web auth flow. Use httpOnly and secure cookie flags in production.

Using SIWE With Auth Libraries

If you are using NextAuth.js, there is a first-party next-auth Ethereum provider (@next-auth/ethereum-provider) that handles the nonce, signature, and session lifecycle for you. Privy and Dynamic also abstract SIWE into their auth flows if you are already using an embedded wallet provider.

Custom SIWE implementation makes sense when you need full control over the session format, when you are integrating with an existing backend auth system, or when your stack does not align with available library adapters.


SIWE-based authentication is the right default for any Web3 application where users will have accounts. If you need wallet authentication implemented and integrated with your existing backend, Start a build with Clixo.