Skip to main content

Documentation Index

Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt

Use this file to discover all available pages before exploring further.

Try it Live

Run SIWE examples in the interactive playground

Usage Patterns

Production patterns for SIWE authentication.

Complete Authentication Flow

Frontend Implementation

// 1. Request nonce from backend
async function startAuth() {
  const response = await fetch('/auth/start', {
    method: 'POST',
  });
  const { nonce } = await response.json();
  return nonce;
}

// 2. Create SIWE message
async function createMessage(address: string, nonce: string) {
  const chainId = await ethereum.request({ method: 'eth_chainId' });

  return Siwe.create({
    domain: window.location.host,
    address: Address(address),
    uri: window.location.origin,
    chainId: Number(chainId),
    statement: 'Sign in to Example App',
    nonce,
  });
}

// 3. Sign message
async function signMessage(message: BrandedMessage) {
  const text = Siwe.format(message);
  const signature = await ethereum.request({
    method: 'personal_sign',
    params: [text, message.address],
  });
  return signature;
}

// 4. Complete auth flow
async function authenticate() {
  try {
    // Get user address
    const [address] = await ethereum.request({
      method: 'eth_requestAccounts',
    });

    // Start authentication
    const nonce = await startAuth();

    // Create and sign message
    const message = await createMessage(address, nonce);
    const signature = await signMessage(message);

    // Send to backend
    const response = await fetch('/auth/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        message: Siwe.format(message),
        signature,
      }),
    });

    if (!response.ok) {
      throw new Error('Authentication failed');
    }

    const { token } = await response.json();
    localStorage.setItem('authToken', token);

    return { success: true };
  } catch (err) {
    console.error('Auth failed:', err);
    return { success: false, error: err.message };
  }
}

Backend Implementation

import { Redis } from 'ioredis';
import { Siwe, Address } from 'tevm';

const redis = new Redis();

// 1. Start authentication endpoint
app.post('/auth/start', async (req, res) => {
  try {
    // Generate nonce
    const nonce = Siwe.generateNonce();
    const expiresAt = Date.now() + 300000; // 5 minutes

    // Store nonce
    await redis.set(
      `nonce:${nonce}`,
      JSON.stringify({
        createdAt: Date.now(),
        expiresAt,
      }),
      'EX',
      300
    );

    res.json({ nonce });
  } catch (err) {
    res.status(500).json({ error: 'Failed to start authentication' });
  }
});

// 2. Verify authentication endpoint
app.post('/auth/verify', async (req, res) => {
  try {
    const { message: messageText, signature: signatureHex } = req.body;

    // Parse message
    const message = Siwe.parse(messageText);

    // Verify domain
    if (message.domain !== req.hostname) {
      return res.status(400).json({ error: 'Domain mismatch' });
    }

    // Verify nonce
    const nonceData = await redis.get(`nonce:${message.nonce}`);
    if (!nonceData) {
      return res.status(400).json({ error: 'Invalid or expired nonce' });
    }

    const { expiresAt } = JSON.parse(nonceData);
    if (Date.now() > expiresAt) {
      return res.status(400).json({ error: 'Nonce expired' });
    }

    // Consume nonce (single use)
    await redis.del(`nonce:${message.nonce}`);

    // Validate message
    const validationResult = Siwe.validate(message);
    if (!validationResult.valid) {
      return res.status(400).json({ error: validationResult.error.message });
    }

    // Verify signature
    const signature = hexToBytes(signatureHex);
    const verifyResult = Siwe.verifyMessage(message, signature);
    if (!verifyResult.valid) {
      return res.status(401).json({ error: verifyResult.error.message });
    }

    // Create session
    const sessionToken = generateSessionToken();
    await redis.set(
      `session:${sessionToken}`,
      JSON.stringify({
        address: Address.toHex(message.address),
        chainId: message.chainId,
        createdAt: Date.now(),
      }),
      'EX',
      86400 // 24 hours
    );

    res.json({ token: sessionToken });
  } catch (err) {
    console.error('Auth verification failed:', err);
    res.status(400).json({ error: 'Authentication failed' });
  }
});

Session Management

Expiring Sessions

const message = Siwe.create({
  domain: "example.com",
  address: userAddress,
  uri: "https://example.com",
  chainId: 1,
  expirationTime: new Date(Date.now() + 3600000).toISOString(), // 1 hour
});

// Middleware to check session validity
app.use((req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    return res.status(401).json({ error: 'No token' });
  }

  redis.get(`session:${token}`).then(data => {
    if (!data) {
      return res.status(401).json({ error: 'Invalid session' });
    }

    const session = JSON.parse(data);
    req.session = session;
    next();
  });
});

Sliding Sessions

// Extend session on each request
app.use(async (req, res, next) => {
  if (req.session) {
    const token = req.headers.authorization?.replace('Bearer ', '');
    await redis.expire(`session:${token}`, 86400); // Reset to 24 hours
  }
  next();
});

Multi-Chain Authentication

const supportedChains = [1, 137, 42161]; // Ethereum, Polygon, Arbitrum

async function authenticateMultiChain(address: string) {
  const nonce = await startAuth();

  const messages = await Promise.all(
    supportedChains.map(async (chainId) => {
      return Siwe.create({
        domain: window.location.host,
        address: Address(address),
        uri: window.location.origin,
        chainId,
        nonce,
      });
    })
  );

  // User selects chain and signs
  const selectedChainId = await promptChainSelection();
  const message = messages.find(m => m.chainId === selectedChainId);
  const signature = await signMessage(message);
}

Resource-Based Authorization

const ADMIN_RESOURCES = [
  "https://example.com/api/admin/users",
  "https://example.com/api/admin/settings",
];

function createAuthMessage(address: string, role: 'admin' | 'user') {
  const resources = role === 'admin' ? ADMIN_RESOURCES : USER_RESOURCES;

  return Siwe.create({
    domain: "example.com",
    address,
    uri: "https://example.com",
    chainId: 1,
    statement: `Grant ${role} access`,
    resources,
  });
}

// Middleware
app.use('/api/admin/*', (req, res, next) => {
  const requestedResource = `https://example.com${req.path}`;

  if (!hasResourceAccess(req.session.message, requestedResource)) {
    return res.status(403).json({ error: 'Insufficient permissions' });
  }

  next();
});

Security Best Practices

Clock Skew Handling

const CLOCK_SKEW_MS = 30000; // 30 seconds

function validateWithSkew(message: BrandedMessage): ValidationResult {
  const now = new Date(Date.now() - CLOCK_SKEW_MS);
  return Siwe.validate(message, { now });
}

HTTPS Only

// Middleware to enforce HTTPS
app.use((req, res, next) => {
  if (!req.secure && process.env.NODE_ENV === 'production') {
    return res.status(403).json({ error: 'HTTPS required' });
  }
  next();
});

Audit Logging

async function logAuthAttempt(
  address: string,
  success: boolean,
  reason?: string
) {
  await db.auditLog.create({
    type: 'auth_attempt',
    address,
    success,
    reason,
    timestamp: new Date(),
    ip: req.ip,
    userAgent: req.get('user-agent'),
  });
}

See Also