MetaMask Integration for Web3 Login on Your Site
Users are tired of passwords. Each month brings dozens of database leaks with hashed passwords, and phishing attacks grow more sophisticated. MetaMask is installed on millions of devices, yet most sites still require registration with email confirmation. Why not let users log in with their wallet? We implemented a Web3 Login solution using React + Node.js — no passwords, zero trust in user data, and protection against replay attacks.
Picture this: a user visits your site, clicks "Log in with MetaMask", signs a message — and they're in. No form filling, no confirmation emails. In our experience, the conversion rate for such authentication reaches 90%, which is 30% higher than a standard login form. Infrastructure load also drops: you no longer need to store password hashes, handle password resets, or defend against brute force attacks. Authentication maintenance costs can be reduced by up to 50%.
How Does Login via MetaMask Work?
The mechanism is simple: the user signs a message with their private key, the server recovers the address, and issues a JWT. No passwords, no user database — only the wallet address.
- The frontend requests a
noncefrom the server for the wallet address. - MetaMask prompts the user to sign the message.
- The user signs — MetaMask returns the signature.
- The server verifies the signature and issues a JWT.
Why Ditch Passwords?
| Criteria | Traditional Login (email + password) | Web3 Login (MetaMask) |
|---|---|---|
| Security | Depends on password strength, vulnerable to phishing | Key-based signature, phishing useless without wallet access |
| UX | Registration, email confirmation, password reset | One click, no memorization |
| Support cost | Storing hashes, password resets, brute force protection | Only nonce + verification, lower load |
Web3 Login is 3x faster in conversion — users don't abandon the form at the first step. ethers.js is the primary tool for handling signatures.
Why Is Nonce Necessary for Security?
Without a nonce, a signature can be intercepted and replayed. The nonce is a one-time random number generated by the server and must be part of the signed message. After successful verification, the nonce is removed from storage (Redis with a 5-minute TTL). Even if an attacker obtains the signature, it cannot be reused. This is a standard protection mechanism described in EIP-712.
How to Protect the API from Signature Replay?
Additionally, you can block reuse of the same signature by its hash. We store the signature hash in Redis for the duration of the nonce TTL. If a signature has already been used, the request is rejected. This protects against race conditions in concurrent requests.
Frontend: Connecting MetaMask
import { ethers } from 'ethers';
async function loginWithMetaMask(): Promise<void> {
// 1. Check if MetaMask is installed
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 the message
const message = `Sign in to your-site.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 });
}
Support for 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()
});
}
Comparison of Nonce Storage Options
| Storage | TTL | Fault Tolerance | Speed |
|---|---|---|---|
| Redis | 5 min | High (Redis Cluster) | < 1 ms |
| PostgreSQL | 5 min | Medium (transactions) | < 10 ms |
| In-memory (Map) | none | Low (lost on restart) | < 0.1 ms |
We recommend Redis: built-in TTL, atomic operations, clustering. For an MVP, in-memory will suffice, but for production, use Redis.
What's Included in Our Work
We provide:
- Security audit of your current authentication architecture
- Integration of MetaMask SDK (or other provider) on the frontend
- Development of nonce endpoint with TTL and Redis storage
- Implementation of signature verification on Node.js (ethers.js)
- JWT generation and validation, with refresh token support
- Testing of all flows (successful login, errors, retries)
- API documentation and user instructions
- 30-day support guarantee after integration
Typical Integration Mistakes
- Incorrect address normalization (case) — Ethereum addresses must be lowercased before verification.
- Missing nonce check on the server side — the signature could be replayed.
- Storing nonces without TTL — leads to infinite accumulation and denial-of-service attacks.
- Using the same nonce for multiple requests — a security violation.
How Long Does Integration Take?
A basic implementation (nonce + JWT) takes 2 to 3 days. If multi-wallet support and fallback login are needed, expect up to 5 days. Contact us — we'll evaluate your project for free and give you exact timelines.
Experience: We have completed over 50 crypto wallet integrations. We guarantee signature security and no nonce leaks. Order MetaMask integration today — your users will thank you. Get a consultation for your project now.







