Skip to main content

SIWE Authentication Flow

Implement Sign-In with Ethereum (SIWE) to authenticate users and unlock 2x rate limits. This tutorial covers the complete flow from nonce generation to session management.

What You'll Learnโ€‹

  • The SIWE authentication flow
  • How to generate and sign SIWE messages
  • Session token management
  • Frontend and backend implementations
  • Wagmi + RainbowKit integration

Time: ~40 minutes
Difficulty: Intermediate


Why SIWE?โ€‹

FeatureAnonymousSIWE Authenticated
Daily requests100200
Requests/minute3060
Usage trackingBy IPBy wallet
/api/me accessโŒโœ…
API key creationโŒโœ…

Authentication Flowโ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Client โ”‚ โ”‚ API โ”‚
โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚ GET /api/auth/siwe/nonce โ”‚
โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚
โ”‚ โ”‚
โ”‚ { nonce: "abc123..." } โ”‚
โ”‚ โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚ โ”‚
โ”‚ [User signs SIWE message] โ”‚
โ”‚ โ”‚
โ”‚ POST /api/auth/siwe/verify โ”‚
โ”‚ { message, signature } โ”‚
โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚
โ”‚ โ”‚
โ”‚ { token, address, expiresAt } โ”‚
โ”‚ โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚ โ”‚
โ”‚ GET /api/me โ”‚
โ”‚ Authorization: Bearer <token> โ”‚
โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚
โ”‚ โ”‚
โ”‚ { address, usage, ... } โ”‚
โ”‚ โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚

Step 1: Get a Nonceโ€‹

The nonce prevents replay attacks. Request one before each sign-in:

cURLโ€‹

curl https://api.web3identity.com/api/auth/siwe/nonce

Response:

{
"nonce": "8a3x9Z2bN4cD5eF6"
}

JavaScriptโ€‹

const BASE_URL = 'https://api.web3identity.com';

async function getNonce() {
const response = await fetch(`${BASE_URL}/api/auth/siwe/nonce`);
const { nonce } = await response.json();
return nonce;
}

Pythonโ€‹

import requests

def get_nonce():
response = requests.get("https://api.web3identity.com/api/auth/siwe/nonce")
return response.json()["nonce"]

Step 2: Create & Sign the SIWE Messageโ€‹

Install:

npm install siwe viem
import { SiweMessage } from 'siwe';
import { createWalletClient, custom } from 'viem';
import { mainnet } from 'viem/chains';

async function createSiweMessage(address, nonce) {
const message = new SiweMessage({
domain: 'api.web3identity.com',
address,
statement: 'Sign in to Web3 Identity API for enhanced access.',
uri: 'https://api.web3identity.com',
version: '1',
chainId: 1,
nonce,
issuedAt: new Date().toISOString(),
expirationTime: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24h
});

return message.prepareMessage();
}

async function signSiweMessage(walletClient, message) {
const signature = await walletClient.signMessage({
account: walletClient.account,
message,
});
return signature;
}

// Usage with browser wallet
const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum),
});

const [address] = await walletClient.getAddresses();
const nonce = await getNonce();
const message = await createSiweMessage(address, nonce);
const signature = await signSiweMessage(walletClient, message);

console.log('Message:', message);
console.log('Signature:', signature);

Manual Message Constructionโ€‹

If you don't want the siwe library:

function createSiweMessageManual(address, nonce) {
const domain = 'api.web3identity.com';
const uri = 'https://api.web3identity.com';
const issuedAt = new Date().toISOString();

return `${domain} wants you to sign in with your Ethereum account:
${address}

Sign in to Web3 Identity API for enhanced access.

URI: ${uri}
Version: 1
Chain ID: 1
Nonce: ${nonce}
Issued At: ${issuedAt}`;
}

Step 3: Verify and Get Tokenโ€‹

Send the signed message to the verify endpoint:

cURLโ€‹

curl -X POST https://api.web3identity.com/api/auth/siwe/verify \
-H "Content-Type: application/json" \
-d '{
"message": "api.web3identity.com wants you to sign in...",
"signature": "0x..."
}'

Response:

{
"token": "eyJhbGciOiJIUzI1NiIs...",
"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"expiresAt": "2026-02-09T05:17:00Z"
}

JavaScriptโ€‹

async function verifySiwe(message, signature) {
const response = await fetch(`${BASE_URL}/api/auth/siwe/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, signature }),
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Verification failed');
}

return response.json();
}

// Complete flow
const { token, address, expiresAt } = await verifySiwe(message, signature);
console.log(`Authenticated as ${address}`);
console.log(`Token expires: ${expiresAt}`);

Pythonโ€‹

from eth_account import Account
from eth_account.messages import encode_defunct

def sign_in(private_key: str) -> dict:
account = Account.from_key(private_key)

# 1. Get nonce
nonce = get_nonce()

# 2. Create message
message = f"""api.web3identity.com wants you to sign in with your Ethereum account:
{account.address}

Sign in to Web3 Identity API for enhanced access.

URI: https://api.web3identity.com
Version: 1
Chain ID: 1
Nonce: {nonce}
Issued At: {datetime.utcnow().isoformat()}Z"""

# 3. Sign message
signable = encode_defunct(text=message)
signed = account.sign_message(signable)

# 4. Verify with API
response = requests.post(
"https://api.web3identity.com/api/auth/siwe/verify",
json={
"message": message,
"signature": signed.signature.hex()
}
)
response.raise_for_status()

return response.json()

Step 4: Use Authenticated Requestsโ€‹

Include the token in the Authorization header:

cURLโ€‹

# Get your profile
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
https://api.web3identity.com/api/me

# Get your usage stats
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
https://api.web3identity.com/api/me/usage

JavaScriptโ€‹

class AuthenticatedClient {
constructor(token) {
this.token = token;
this.baseUrl = 'https://api.web3identity.com';
}

async fetch(endpoint, options = {}) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.token}`,
},
});

if (response.status === 401) {
throw new Error('Token expired - please sign in again');
}

return response.json();
}

async getMe() {
return this.fetch('/api/me');
}

async getUsage() {
return this.fetch('/api/me/usage');
}

async resolveENS(name) {
return this.fetch(`/api/ens/resolve/${name}`);
}
}

// Usage
const client = new AuthenticatedClient(token);

const me = await client.getMe();
console.log('My address:', me.address);

const usage = await client.getUsage();
console.log(`Used ${usage.used}/${usage.limit} requests today`);

Step 5: React + Wagmi Integrationโ€‹

Full integration with Wagmi and RainbowKit:

Setupโ€‹

npm install wagmi viem @rainbow-me/rainbowkit @tanstack/react-query siwe

AuthProvider.tsxโ€‹

import { createContext, useContext, useState, useCallback, useEffect } from 'react';
import { useAccount, useSignMessage } from 'wagmi';
import { SiweMessage } from 'siwe';

const BASE_URL = 'https://api.web3identity.com';

interface AuthState {
token: string | null;
address: string | null;
expiresAt: string | null;
isAuthenticated: boolean;
}

interface AuthContextValue extends AuthState {
signIn: () => Promise<void>;
signOut: () => void;
loading: boolean;
error: string | null;
}

const AuthContext = createContext<AuthContextValue | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
const { address, isConnected } = useAccount();
const { signMessageAsync } = useSignMessage();

const [state, setState] = useState<AuthState>(() => {
// Load from localStorage on init
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('siwe-session');
if (saved) {
const parsed = JSON.parse(saved);
// Check if expired
if (new Date(parsed.expiresAt) > new Date()) {
return { ...parsed, isAuthenticated: true };
}
}
}
return { token: null, address: null, expiresAt: null, isAuthenticated: false };
});

const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

// Sign out when wallet disconnects or address changes
useEffect(() => {
if (state.isAuthenticated && state.address !== address) {
signOut();
}
}, [address, state.address, state.isAuthenticated]);

const signIn = useCallback(async () => {
if (!address || !isConnected) {
setError('Please connect your wallet first');
return;
}

setLoading(true);
setError(null);

try {
// 1. Get nonce
const nonceRes = await fetch(`${BASE_URL}/api/auth/siwe/nonce`);
const { nonce } = await nonceRes.json();

// 2. Create SIWE message
const message = new SiweMessage({
domain: 'api.web3identity.com',
address,
statement: 'Sign in to Web3 Identity API for enhanced access.',
uri: 'https://api.web3identity.com',
version: '1',
chainId: 1,
nonce,
issuedAt: new Date().toISOString(),
});

const messageStr = message.prepareMessage();

// 3. Sign with wallet
const signature = await signMessageAsync({ message: messageStr });

// 4. Verify with API
const verifyRes = await fetch(`${BASE_URL}/api/auth/siwe/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: messageStr, signature }),
});

if (!verifyRes.ok) {
const error = await verifyRes.json();
throw new Error(error.message || 'Verification failed');
}

const { token, expiresAt } = await verifyRes.json();

// 5. Save session
const session = { token, address, expiresAt };
localStorage.setItem('siwe-session', JSON.stringify(session));
setState({ ...session, isAuthenticated: true });

} catch (err) {
const message = err instanceof Error ? err.message : 'Sign in failed';
setError(message);
throw err;
} finally {
setLoading(false);
}
}, [address, isConnected, signMessageAsync]);

const signOut = useCallback(() => {
localStorage.removeItem('siwe-session');
setState({ token: null, address: null, expiresAt: null, isAuthenticated: false });
}, []);

return (
<AuthContext.Provider value={{ ...state, signIn, signOut, loading, error }}>
{children}
</AuthContext.Provider>
);
}

export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}

SignInButton.tsxโ€‹

import { useAuth } from './AuthProvider';
import { useAccount, useConnect } from 'wagmi';
import { ConnectButton } from '@rainbow-me/rainbowkit';

export function SignInButton() {
const { isConnected } = useAccount();
const { isAuthenticated, signIn, signOut, loading, error } = useAuth();

if (!isConnected) {
return <ConnectButton />;
}

if (isAuthenticated) {
return (
<button onClick={signOut} className="btn btn-outline">
Sign Out
</button>
);
}

return (
<div>
<button
onClick={signIn}
disabled={loading}
className="btn btn-primary"
>
{loading ? 'Signing...' : 'Sign In with Ethereum'}
</button>
{error && <p className="error">{error}</p>}
</div>
);
}

AuthenticatedRequest.tsxโ€‹

import { useAuth } from './AuthProvider';
import { useState } from 'react';

export function ProfileViewer() {
const { token, isAuthenticated } = useAuth();
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(false);

const fetchMe = async () => {
if (!token) return;

setLoading(true);
try {
const res = await fetch('https://api.web3identity.com/api/me', {
headers: { 'Authorization': `Bearer ${token}` },
});
setProfile(await res.json());
} finally {
setLoading(false);
}
};

if (!isAuthenticated) {
return <p>Please sign in to view your profile</p>;
}

return (
<div>
<button onClick={fetchMe} disabled={loading}>
{loading ? 'Loading...' : 'Fetch My Profile'}
</button>
{profile && (
<pre>{JSON.stringify(profile, null, 2)}</pre>
)}
</div>
);
}

Step 6: Session Managementโ€‹

Token Expiration Handlingโ€‹

class SessionManager {
private token: string | null = null;
private expiresAt: Date | null = null;
private refreshThreshold = 60 * 60 * 1000; // 1 hour before expiry

isExpired(): boolean {
if (!this.expiresAt) return true;
return Date.now() > this.expiresAt.getTime();
}

needsRefresh(): boolean {
if (!this.expiresAt) return true;
return Date.now() > this.expiresAt.getTime() - this.refreshThreshold;
}

setSession(token: string, expiresAt: string) {
this.token = token;
this.expiresAt = new Date(expiresAt);
this.persist();
}

getToken(): string | null {
if (this.isExpired()) {
this.clear();
return null;
}
return this.token;
}

clear() {
this.token = null;
this.expiresAt = null;
localStorage.removeItem('siwe-session');
}

private persist() {
if (this.token && this.expiresAt) {
localStorage.setItem('siwe-session', JSON.stringify({
token: this.token,
expiresAt: this.expiresAt.toISOString(),
}));
}
}
}

Auto-Refresh Hookโ€‹

import { useEffect } from 'react';
import { useAuth } from './AuthProvider';

export function useSessionRefresh() {
const { isAuthenticated, expiresAt, signIn } = useAuth();

useEffect(() => {
if (!isAuthenticated || !expiresAt) return;

const checkExpiry = () => {
const expires = new Date(expiresAt);
const now = new Date();
const hoursUntilExpiry = (expires.getTime() - now.getTime()) / (1000 * 60 * 60);

if (hoursUntilExpiry < 1) {
// Re-authenticate before expiry
signIn();
}
};

// Check every 30 minutes
const interval = setInterval(checkExpiry, 30 * 60 * 1000);
return () => clearInterval(interval);
}, [isAuthenticated, expiresAt, signIn]);
}

Step 7: Backend Integration (Node.js)โ€‹

If you're building a backend that proxies requests:

// server.js
import express from 'express';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
import { SiweMessage } from 'siwe';

const app = express();
const BASE_URL = 'https://api.web3identity.com';

// Store tokens server-side (use Redis in production)
let serverToken = null;
let tokenExpiry = null;

async function ensureAuthenticated() {
if (serverToken && tokenExpiry && new Date() < tokenExpiry) {
return serverToken;
}

// Sign in with server wallet
const account = privateKeyToAccount(process.env.SERVER_WALLET_KEY);
const wallet = createWalletClient({
account,
chain: mainnet,
transport: http(),
});

// Get nonce
const nonceRes = await fetch(`${BASE_URL}/api/auth/siwe/nonce`);
const { nonce } = await nonceRes.json();

// Create SIWE message
const message = new SiweMessage({
domain: 'api.web3identity.com',
address: account.address,
statement: 'Server authentication for Web3 Identity API',
uri: 'https://api.web3identity.com',
version: '1',
chainId: 1,
nonce,
});

const messageStr = message.prepareMessage();
const signature = await wallet.signMessage({ message: messageStr });

// Verify
const verifyRes = await fetch(`${BASE_URL}/api/auth/siwe/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: messageStr, signature }),
});

const { token, expiresAt } = await verifyRes.json();
serverToken = token;
tokenExpiry = new Date(expiresAt);

return token;
}

app.get('/api/ens/:name', async (req, res) => {
const token = await ensureAuthenticated();

const apiRes = await fetch(
`${BASE_URL}/api/ens/resolve/${req.params.name}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);

res.json(await apiRes.json());
});

app.listen(3000);

Authenticated Endpointsโ€‹

With a valid SIWE token, you can access:

EndpointMethodDescription
/api/meGETYour profile
/api/me/usageGETUsage statistics
/api/user/keysGETList API keys
/api/user/keysPOSTCreate API key
/api/user/keys/:idDELETERevoke API key

Troubleshootingโ€‹

ErrorCauseSolution
INVALID_NONCENonce expired/reusedRequest fresh nonce
INVALID_SIGNATURESignature doesn't matchVerify message format
EXPIRED_MESSAGEMessage too oldSet recent issuedAt
TOKEN_EXPIRED24h session endedSign in again
DOMAIN_MISMATCHWrong domain in messageUse api.web3identity.com

Security Best Practicesโ€‹

  1. Always use fresh nonces - Request before each sign-in
  2. Validate token server-side if passing to backend
  3. Store tokens securely - httpOnly cookies or secure storage
  4. Handle expiration gracefully - Auto re-auth or prompt user
  5. Clear tokens on disconnect - Don't leave stale sessions

Next Stepsโ€‹