Skip to content
All essays
WebMarch 23, 202411 min

Web3 Authentication and Identity

Implement Web3 authentication using crypto wallets

Ü
Ümit Uz
Mobile & Full Stack Developer

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

  1. 1User connects wallet
  2. 2App generates random message
  3. 3User signs message with wallet
  4. 4App verifies signature
  5. 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

  1. 1Always use nonce/timestamp for challenges
  2. 2Expire nonces quickly (5 minutes)
  3. 3Use HTTPS only
  4. 4Implement rate limiting
  5. 5Store tokens securely (httpOnly cookies)
  6. 6Handle chain switching
  7. 7Provide clear UI feedback
  8. 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.

Enjoyed this essay? Share it.

Related essays

Next essay
Remote Work Best Practices for Development Teams