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?โ
| Feature | Anonymous | SIWE Authenticated |
|---|---|---|
| Daily requests | 100 | 200 |
| Requests/minute | 30 | 60 |
| Usage tracking | By IP | By 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โ
Using siwe Library (Recommended)โ
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:
| Endpoint | Method | Description |
|---|---|---|
/api/me | GET | Your profile |
/api/me/usage | GET | Usage statistics |
/api/user/keys | GET | List API keys |
/api/user/keys | POST | Create API key |
/api/user/keys/:id | DELETE | Revoke API key |
Troubleshootingโ
| Error | Cause | Solution |
|---|---|---|
INVALID_NONCE | Nonce expired/reused | Request fresh nonce |
INVALID_SIGNATURE | Signature doesn't match | Verify message format |
EXPIRED_MESSAGE | Message too old | Set recent issuedAt |
TOKEN_EXPIRED | 24h session ended | Sign in again |
DOMAIN_MISMATCH | Wrong domain in message | Use api.web3identity.com |
Security Best Practicesโ
- Always use fresh nonces - Request before each sign-in
- Validate token server-side if passing to backend
- Store tokens securely - httpOnly cookies or secure storage
- Handle expiration gracefully - Auto re-auth or prompt user
- Clear tokens on disconnect - Don't leave stale sessions
Next Stepsโ
- Combine with x402 payments for seamless paid access
- Build an ENS Profile Card with auth status
- Create API keys for server-to-server auth