Web3 authentication replaces traditional username/password login with cryptographic wallet signatures. This provides a more secure, privacy-preserving way to authenticate users without centralized servers.
Understanding Web3 Auth
Traditional vs Web3 Auth
Traditional authentication:
- Email + password
- Server stores credentials
- Single point of failure
- Password breaches
Web3 authentication:
- Wallet signature
- No password storage
- User-controlled identity
- Cryptographic security
The Flow
- 1User connects wallet
- 2App generates random message
- 3User signs message with wallet
- 4App verifies signature
- 5User is authenticated
Implementing Web3 Auth
Challenge-Response Authentication
javascript
import { ethers } from 'ethers';
class Web3Auth {
constructor() {
this.provider = null;
this.signer = null;
this.address = null;
}
// Generate random challenge
generateChallenge() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return ethers.utils.hexlify(array);
}
// Connect wallet
async connect() {
if (!window.ethereum) {
throw new Error('No wallet found');
}
this.provider = new ethers.providers.Web3Provider(
window.ethereum
);
await this.provider.send('eth_requestAccounts', []);
this.signer = this.provider.getSigner();
this.address = await this.signer.getAddress();
return this.address;
}
// Sign challenge
async signChallenge(challenge) {
if (!this.signer) {
throw new Error('Wallet not connected');
}
const signature = await this.signer.signMessage(
ethers.utils.arrayify(challenge)
);
return {
address: this.address,
challenge,
signature
};
}
// Verify signature
static verifySignature(address, challenge, signature) {
const recoveredAddress = ethers.utils.verifyMessage(
ethers.utils.arrayify(challenge),
signature
);
return recoveredAddress.toLowerCase() === address.toLowerCase();
}
}Server-Side Verification
javascript
// Node.js server
const express = require('express');
const { ethers } = require('ethers');
const app = express();
app.use(express.json());
// Store nonces (in production, use Redis)
const nonces = new Map();
// Generate nonce endpoint
app.post('/api/auth/nonce', (req, res) => {
const { address } = req.body;
// Generate random nonce
const nonce = ethers.utils.randomBytes(32);
const nonceHex = ethers.utils.hexlify(nonce);
// Store nonce with timestamp
nonces.set(address.toLowerCase(), {
nonce: nonceHex,
timestamp: Date.now()
});
// Expire after 5 minutes
setTimeout(() => {
nonces.delete(address.toLowerCase());
}, 5 * 60 * 1000);
res.json({ nonce: nonceHex });
});
// Verify signature endpoint
app.post('/api/auth/verify', (req, res) => {
const { address, signature } = req.body;
// Get stored nonce
const stored = nonces.get(address.toLowerCase());
if (!stored) {
return res.status(400).json({ error: 'Invalid or expired nonce' });
}
// Verify signature
const recoveredAddress = ethers.utils.verifyMessage(
ethers.utils.arrayify(stored.nonce),
signature
);
if (recoveredAddress.toLowerCase() !== address.toLowerCase()) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Generate JWT token
const token = generateJWT(address);
// Remove used nonce
nonces.delete(address.toLowerCase());
res.json({ token, address });
});
app.listen(3000);Session Management
JWT Tokens
javascript
const jwt = require('jsonwebtoken');
function generateJWT(address) {
return jwt.sign(
{ address },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
}
function verifyJWT(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
return null;
}
}
// Middleware to protect routes
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const decoded = verifyJWT(token);
if (!decoded) {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = decoded;
next();
}SIWE (Sign-In with Ethereum)
Standardized Ethereum authentication.
SIWE Implementation
javascript
import { SiweMessage } from 'siwe';
class SIWEAuth {
constructor(domain) {
this.domain = domain;
}
createMessage(address, statement) {
const message = new SiweMessage({
domain: this.domain,
address: address,
statement: statement,
uri: window.location.origin,
version: '1',
chainId: 1,
nonce: this.generateNonce()
});
return message.prepareMessage();
}
async signIn(message, signature) {
const siweMessage = new SiweMessage(message);
try {
const fields = await siweMessage.validate(signature);
return {
success: true,
address: fields.address
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
generateNonce() {
return Array.from(crypto.getRandomValues(new Uint8Array(16)))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
}
// Usage
const auth = new SIWEAuth('myapp.com');
// Create message
const message = auth.createMessage(
'0x...',
'Sign in to My App'
);
// User signs with wallet
const signature = await signer.signMessage(message);
// Verify
const result = await auth.signIn(message, signature);Frontend Integration
React Hook
jsx
import { useState, useCallback } from 'react';
import { ethers } from 'ethers';
export const useWeb3Auth = () => {
const [address, setAddress] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const connect = useCallback(async () => {
if (!window.ethereum) {
setError('Please install MetaMask');
return;
}
setLoading(true);
setError(null);
try {
const provider = new ethers.providers.Web3Provider(
window.ethereum
);
await provider.send('eth_requestAccounts', []);
const signer = provider.getSigner();
const addr = await signer.getAddress();
setAddress(addr);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
const signIn = useCallback(async () => {
if (!address) {
await connect();
}
// Get challenge from server
const response = await fetch('/api/auth/nonce', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address })
});
const { nonce } = await response.json();
// Sign challenge
const provider = new ethers.providers.Web3Provider(
window.ethereum
);
const signer = provider.getSigner();
const signature = await signer.signMessage(
ethers.utils.arrayify(nonce)
);
// Send signature to server
const verifyResponse = await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, signature })
});
const { token } = await verifyResponse.json();
// Store token
localStorage.setItem('token', token);
return token;
}, [address, connect]);
const signOut = useCallback(() => {
setAddress(null);
localStorage.removeItem('token');
}, []);
return {
address,
loading,
error,
connect,
signIn,
signOut
};
};Next.js Integration
API Routes
javascript
// pages/api/auth/nonce.js
export default function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { address } = req.body;
const nonce = crypto.randomBytes(32).toString('hex');
// Store nonce (use database in production)
// ...
res.status(200).json({ nonce });
}Authentication Utility
javascript
// lib/auth.js
export async function signInWithEthereum() {
if (!window.ethereum) {
throw new Error('No wallet found');
}
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send('eth_requestAccounts', []);
const signer = provider.getSigner();
const address = await signer.getAddress();
// Get nonce
const nonceRes = await fetch('/api/auth/nonce', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address })
});
const { nonce } = await nonceRes.json();
// Sign nonce
const signature = await signer.signMessage(
ethers.utils.arrayify(nonce)
);
// Verify
const verifyRes = await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, signature })
});
if (!verifyRes.ok) {
throw new Error('Authentication failed');
}
return await verifyRes.json();
}Best Practices
- 1Always use nonce/timestamp for challenges
- 2Expire nonces quickly (5 minutes)
- 3Use HTTPS only
- 4Implement rate limiting
- 5Store tokens securely (httpOnly cookies)
- 6Handle chain switching
- 7Provide clear UI feedback
- 8Support multiple wallet providers
Security Considerations
- Never store private keys
- Validate all signatures server-side
- Use proper nonce management
- Implement rate limiting
- Monitor for suspicious activity
- Keep dependencies updated
Conclusion
Web3 authentication provides a secure, user-controlled alternative to traditional auth. By leveraging cryptographic signatures, you can build authentication systems that respect user privacy while maintaining security.