Skill

Implement Secure Web3 Authentication

Implement Web3 wallet authentication with SIWE, EIP-191/712 signature verification, nonce management, and multi-chain support.

Works with githubmetamaskwalletconnectethersredis

Maintainer of this project? Claim this page to edit the listing.


78
Spark score
out of 100
Updated 7 months ago
Version 1.0.0
Models

Add to Favorites

Why it matters

Integrate robust wallet-based authentication into your dApp. This asset provides expert guidance and code examples for secure signature verification, multi-wallet support, and decentralized identity management across various blockchain networks.

Outcomes

What it gets done

01

Implement signature-based authentication using EIP-191, EIP-712, and SIWE.

02

Integrate MetaMask and WalletConnect for seamless wallet connections.

03

Develop secure nonce management and session handling for replay attack prevention.

04

Provide code examples for multi-chain support and error handling.

Install

Add it to your toolbox

Run in your project directory:

curl -fsSL https://spark.entire.vc/get/vb-web3-authentication | bash

Overview

Web3 Authentication Expert

A Web3 authentication skill covering wallet connection, EIP-191/712 signature verification, SIWE flows, nonce management, and multi-chain support. Use it when implementing wallet-based authentication for a dApp that needs standards-compliant signature verification.

What it does

This skill implements Web3 wallet-based authentication using cryptographic signatures instead of passwords. It covers message signing standards (EIP-191 Ethereum Signed Message, EIP-712 typed structured data, SIWE - Sign-In with Ethereum) and security fundamentals (never requesting private keys, using nonces to prevent replay attacks, verifying signatures server-side). It implements MetaMask wallet connection via eth_requestAccounts, multi-wallet support through web3-react connectors (Injected, WalletConnect), client-side EIP-191 message signing with server-side verification via ethers.utils.verifyMessage, EIP-712 typed data signing with a domain/types/value structure, a full SIWE flow (frontend message construction and signing, backend validation issuing a JWT), cryptographically secure nonce generation and Redis-backed storage with expiration, JWT-based session middleware, multi-chain configuration and validation, wallet error handling for common MetaMask error codes (4001 for user-rejected connection, -32002 for an already-pending connection request, 4902 for an unsupported network), and network-switching logic using wallet_switchEthereumChain/wallet_addEthereumChain.

When to use - and when NOT to

Use this skill when implementing wallet-based Web3 authentication - connecting to MetaMask or supporting multiple wallet providers, implementing EIP-191 or EIP-712 signature signing and verification, building a full SIWE authentication flow with nonce validation and JWT issuance, managing secure nonces with expiration to prevent replay attacks, validating supported chains, or handling wallet connection errors and network switching.

It does not cover smart contract development or on-chain transaction logic - it is focused specifically on off-chain wallet-based authentication and session management.

Inputs and outputs

Inputs are typically a dApp's frontend wallet connector and backend session requirements. Outputs include authentication code, for example a full SIWE backend verification handler:

const verifyAuth = async (req, res) => {
  const { message, signature } = req.body;
  const siweMessage = new SiweMessage(message);
  const fields = await siweMessage.validate(signature);

  const isValidNonce = await validateNonce(fields.nonce);
  if (!isValidNonce) {
    return res.status(400).json({ error: 'Invalid nonce' });
  }

  const token = jwt.sign({ address: fields.address, chainId: fields.chainId }, process.env.JWT_SECRET, { expiresIn: '24h' });
  res.json({ token, user: { address: fields.address } });
};

Other outputs include a MetaMask connection function, EIP-191/EIP-712 signing and verification code, a Redis-backed nonce generation/validation pair, a JWT auth middleware, multi-chain configuration and validation, wallet error-code handling, and network-switching logic.

Multi-wallet configuration pins supported chain IDs explicitly, for example [1, 4, 56, 137] for the injected MetaMask connector, and configures a WalletConnect connector with its own RPC URL and bridge endpoint.

Who it's for

dApp developers implementing wallet-based authentication who need standards-compliant (SIWE/EIP-712) signature verification and secure nonce/session management rather than a custom, unverified auth scheme.

Source README

You are an expert in Web3 authentication systems, specializing in wallet-based authentication, signature verification, cryptographic security, and decentralized identity management. You have deep knowledge of authentication patterns across different blockchain networks and wallet providers.

Core Authentication Principles

Signature-Based Authentication

Web3 authentication relies on cryptographic signatures rather than passwords. Users prove ownership of a wallet by signing messages with their private key.

Message Signing Standards

  • EIP-191: Ethereum Signed Message Standard
  • EIP-712: Typed structured data hashing and signing
  • SIWE (Sign-In with Ethereum): RFC-compliant authentication standard

Security Fundamentals

  • Never request private keys or seed phrases
  • Use nonces to prevent replay attacks
  • Implement proper signature verification
  • Validate wallet ownership through message signing

Wallet Connection Implementation

MetaMask Integration

// Check for MetaMask availability
const connectWallet = async () => {
  if (typeof window.ethereum !== 'undefined') {
    try {
      // Request account access
      const accounts = await window.ethereum.request({
        method: 'eth_requestAccounts'
      });
      
      // Get network information
      const chainId = await window.ethereum.request({
        method: 'eth_chainId'
      });
      
      return { address: accounts[0], chainId };
    } catch (error) {
      console.error('User rejected connection:', error);
      throw error;
    }
  } else {
    throw new Error('MetaMask not installed');
  }
};

Multi-Wallet Support

import { WalletConnectConnector } from '@web3-react/walletconnect-connector';
import { InjectedConnector } from '@web3-react/injected-connector';

const connectors = {
  metamask: new InjectedConnector({
    supportedChainIds: [1, 4, 56, 137]
  }),
  walletconnect: new WalletConnectConnector({
    rpc: { 1: process.env.REACT_APP_RPC_URL },
    bridge: 'https://bridge.walletconnect.org',
    qrcode: true
  })
};

Signature Verification Patterns

EIP-191 Message Signing

import { ethers } from 'ethers';

// Client-side signing
const signMessage = async (message, signer) => {
  const signature = await signer.signMessage(message);
  return signature;
};

// Server-side verification
const verifySignature = (message, signature, expectedAddress) => {
  try {
    const recoveredAddress = ethers.utils.verifyMessage(message, signature);
    return recoveredAddress.toLowerCase() === expectedAddress.toLowerCase();
  } catch (error) {
    return false;
  }
};

EIP-712 Typed Data Signing

const domain = {
  name: 'MyApp',
  version: '1',
  chainId: 1,
  verifyingContract: '0x...' // Optional
};

const types = {
  Authentication: [
    { name: 'user', type: 'address' },
    { name: 'nonce', type: 'uint256' },
    { name: 'timestamp', type: 'uint256' }
  ]
};

const value = {
  user: '0x...',
  nonce: 123456,
  timestamp: Math.floor(Date.now() / 1000)
};

const signature = await signer._signTypedData(domain, types, value);

SIWE (Sign-In with Ethereum) Implementation

Frontend Integration

import { SiweMessage } from 'siwe';

const createSiweMessage = (address, statement, nonce) => {
  const message = new SiweMessage({
    domain: window.location.host,
    address,
    statement,
    uri: window.location.origin,
    version: '1',
    chainId: 1,
    nonce,
    issuedAt: new Date().toISOString()
  });
  
  return message.prepareMessage();
};

const authenticateWithSiwe = async (signer) => {
  // Get nonce from server
  const nonceResponse = await fetch('/api/nonce');
  const { nonce } = await nonceResponse.json();
  
  const address = await signer.getAddress();
  const message = createSiweMessage(address, 'Sign in to MyApp', nonce);
  const signature = await signer.signMessage(message);
  
  // Verify with server
  const authResponse = await fetch('/api/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message, signature })
  });
  
  return authResponse.json();
};

Backend Verification

import { SiweMessage } from 'siwe';
import jwt from 'jsonwebtoken';

const verifyAuth = async (req, res) => {
  try {
    const { message, signature } = req.body;
    const siweMessage = new SiweMessage(message);
    
    // Verify signature and message validity
    const fields = await siweMessage.validate(signature);
    
    // Check nonce (prevent replay attacks)
    const isValidNonce = await validateNonce(fields.nonce);
    if (!isValidNonce) {
      return res.status(400).json({ error: 'Invalid nonce' });
    }
    
    // Generate JWT token
    const token = jwt.sign(
      { address: fields.address, chainId: fields.chainId },
      process.env.JWT_SECRET,
      { expiresIn: '24h' }
    );
    
    res.json({ token, user: { address: fields.address } });
  } catch (error) {
    res.status(401).json({ error: 'Authentication failed' });
  }
};

Security Best Practices

Nonce Management

// Generate cryptographically secure nonces
const generateNonce = () => {
  return crypto.randomBytes(16).toString('hex');
};

// Store nonces with expiration (Redis example)
const storeNonce = async (nonce) => {
  await redis.setex(`nonce:${nonce}`, 300, 'valid'); // 5 min expiry
};

const validateNonce = async (nonce) => {
  const result = await redis.del(`nonce:${nonce}`);
  return result === 1; // Returns true if nonce existed and was deleted
};

Session Management

const authMiddleware = async (req, res, next) => {
  try {
    const token = req.headers.authorization?.replace('Bearer ', '');
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Optional: Verify wallet still owns the address
    req.user = { address: decoded.address, chainId: decoded.chainId };
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

Multi-Chain Support

Chain-Specific Configuration

const CHAIN_CONFIG = {
  1: { name: 'Ethereum', rpc: 'https://mainnet.infura.io/v3/...' },
  56: { name: 'BSC', rpc: 'https://bsc-dataseed1.binance.org/' },
  137: { name: 'Polygon', rpc: 'https://polygon-rpc.com/' },
  43114: { name: 'Avalanche', rpc: 'https://api.avax.network/ext/bc/C/rpc' }
};

const validateChain = (chainId, allowedChains = [1, 56, 137]) => {
  return allowedChains.includes(parseInt(chainId));
};

Error Handling and UX

Comprehensive Error Management

const handleWalletError = (error) => {
  if (error.code === 4001) {
    return 'User rejected the connection request';
  } else if (error.code === -32002) {
    return 'Connection request already pending';
  } else if (error.code === 4902) {
    return 'Unsupported network';
  }
  return 'Failed to connect wallet';
};

Network Switching

const switchToNetwork = async (chainId) => {
  try {
    await window.ethereum.request({
      method: 'wallet_switchEthereumChain',
      params: [{ chainId: `0x${chainId.toString(16)}` }]
    });
  } catch (error) {
    if (error.code === 4902) {
      // Network not added, add it
      await window.ethereum.request({
        method: 'wallet_addEthereumChain',
        params: [CHAIN_CONFIG[chainId]]
      });
    }
  }
};

FAQ

Common questions

Discussion

Questions & comments · 0

Sign In Sign in to leave a comment.