MetaMask Integration for Website Authorization (Web3 Login)
Web3 Login via MetaMask allows users to authorize on a website by signing a message with their wallet's cryptographic key. No passwords — proof of address ownership through signature.
Authorization Mechanism
- Frontend requests
noncefrom server for wallet address - MetaMask shows user a message to sign
- User signs — MetaMask returns signature
- Server verifies signature and issues JWT
Frontend: Connecting MetaMask
import { ethers } from 'ethers';
async function loginWithMetaMask(): Promise<void> {
// 1. Check for MetaMask
if (!window.ethereum) {
throw new Error('MetaMask not installed');
}
// 2. Request account access
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
const address = await signer.getAddress();
// 3. Get nonce from server
const nonceResponse = await fetch(`/api/auth/nonce?address=${address}`);
const { nonce } = await nonceResponse.json();
// 4. Sign message
const message = `Sign in to example.com\n\nNonce: ${nonce}\nTime: ${new Date().toISOString()}`;
const signature = await signer.signMessage(message);
// 5. Send signature to server
const authResponse = await fetch('/api/auth/web3', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, signature, message })
});
const { token } = await authResponse.json();
localStorage.setItem('auth_token', token);
}
Backend: Signature Verification
// Node.js + ethers.js
import { ethers } from 'ethers';
import { randomBytes } from 'crypto';
// Nonce storage (Redis with 5 min TTL)
async function getNonce(address: string): Promise<string> {
const normalized = address.toLowerCase();
const existing = await redis.get(`nonce:${normalized}`);
if (existing) return existing;
const nonce = randomBytes(16).toString('hex');
await redis.setex(`nonce:${normalized}`, 300, nonce);
return nonce;
}
// Verification
async function verifyWeb3Auth(req, res) {
const { address, signature, message } = req.body;
const normalized = address.toLowerCase();
// Check nonce in message
const storedNonce = await redis.get(`nonce:${normalized}`);
if (!storedNonce || !message.includes(storedNonce)) {
return res.status(401).json({ error: 'Invalid or expired nonce' });
}
// Recover address from signature
const recoveredAddress = ethers.verifyMessage(message, signature).toLowerCase();
if (recoveredAddress !== normalized) {
return res.status(401).json({ error: 'Signature verification failed' });
}
// Delete used nonce
await redis.del(`nonce:${normalized}`);
// Find or create user
let user = await userRepo.findByWalletAddress(normalized);
if (!user) {
user = await userRepo.create({ walletAddress: normalized });
}
const token = jwt.sign(
{ sub: user.id, walletAddress: normalized },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({ token, userId: user.id });
}
Supporting Multiple Wallets
// Link additional wallet to account
async function linkWallet(userId: string, address: string, signature: string) {
const existing = await walletRepo.findByAddress(address.toLowerCase());
if (existing) throw new Error('Wallet already linked to another account');
await walletRepo.create({
userId,
address: address.toLowerCase(),
linkedAt: new Date()
});
}
Timeline
MetaMask Login with nonce verification and JWT — 2–3 days.







