Skip to main content

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