Skip to main content

Overview

This guide shows you how to verify message signatures to authenticate users and ensure data integrity. Message verification is essential for secure dApp interactions and user authentication.

Prerequisites

Step 1: Basic Message Verification

Verify a simple message signature:
import { authenticatedEvmClient } from './client';

const evmClient = await authenticatedEvmClient();

const isValid = await evmClient.verifyMessageSignature({
  accountAddress: '0xYourWalletAddress',
  message: 'Hello, World!',
  signature: '0xYourSignature',
});

console.log('Signature valid:', isValid);

Step 2: Authentication with Nonce

Verify a message with a nonce to prevent replay attacks:
export const verifyAuthentication = async ({
  walletAddress,
  message,
  signature,
  expectedNonce,
}: {
  walletAddress: string;
  message: string;
  signature: string;
  expectedNonce: string;
}) => {
  const evmClient = await authenticatedEvmClient();

  // Extract nonce from message
  const nonceMatch = message.match(/nonce: (\d+)/);
  if (!nonceMatch || nonceMatch[1] !== expectedNonce) {
    throw new Error('Invalid or expired nonce');
  }

  // Verify signature
  const isValid = await evmClient.verifyMessageSignature({
    accountAddress: walletAddress,
    message,
    signature,
  });

  return isValid;
};

// Usage
const isValid = await verifyAuthentication({
  walletAddress: '0xYourWalletAddress',
  message: 'Sign this message to authenticate: nonce: 1234567890',
  signature: '0xYourSignature',
  expectedNonce: '1234567890',
});

if (isValid) {
  console.log('User authenticated successfully');
} else {
  console.log('Authentication failed');
}

Step 3: Data Integrity Verification

Verify that data hasn’t been tampered with:
export const verifyDataIntegrity = async ({
  walletAddress,
  originalData,
  signature,
}: {
  walletAddress: string;
  originalData: any;
  signature: string;
}) => {
  const evmClient = await authenticatedEvmClient();

  // Convert data to string (must match what was signed)
  const message = JSON.stringify(originalData);

  const isValid = await evmClient.verifyMessageSignature({
    accountAddress: walletAddress,
    message,
    signature,
  });

  return isValid;
};

// Usage
const data = { userId: 123, action: 'transfer', amount: '100' };
const isValid = await verifyDataIntegrity({
  walletAddress: '0xYourWalletAddress',
  originalData: data,
  signature: '0xYourSignature',
});

if (isValid) {
  console.log('Data integrity verified');
} else {
  console.log('Data has been tampered with');
}

Step 5: Complete Authentication Flow

Here’s a complete example of a secure authentication flow:
export class MessageVerifier {
  private evmClient: any;
  private nonceStore: Map<string, number> = new Map();

  constructor() {
    this.evmClient = await authenticatedEvmClient();
  }

  // Generate a nonce for a user
  generateNonce(userId: string): string {
    const nonce = Date.now();
    this.nonceStore.set(userId, nonce);
    return nonce.toString();
  }

  // Verify user authentication
  async verifyUser({
    walletAddress,
    userId,
    signature,
  }: {
    walletAddress: string;
    userId: string;
    signature: string;
  }): Promise<boolean> {
    const expectedNonce = this.nonceStore.get(userId);
    if (!expectedNonce) {
      throw new Error('No nonce found for user');
    }

    const message = `Sign this message to authenticate user ${userId}: nonce: ${expectedNonce}`;

    const isValid = await this.evmClient.verifyMessageSignature({
      accountAddress: walletAddress,
      message,
      signature,
    });

    if (isValid) {
      // Clear the used nonce
      this.nonceStore.delete(userId);
    }

    return isValid;
  }

  // Verify transaction approval
  async verifyTransactionApproval({
    walletAddress,
    transactionHash,
    signature,
  }: {
    walletAddress: string;
    transactionHash: string;
    signature: string;
  }): Promise<boolean> {
    const message = `I approve transaction: ${transactionHash}`;

    return await this.evmClient.verifyMessageSignature({
      accountAddress: walletAddress,
      message,
      signature,
    });
  }
}

// Usage
const verifier = new MessageVerifier();

// Step 1: Generate nonce for user
const nonce = verifier.generateNonce('user123');
console.log('Nonce generated:', nonce);

// Step 2: User signs message with nonce (on frontend)
// const message = `Sign this message to authenticate user user123: nonce: ${nonce}`;
// const signature = await wallet.signMessage(message);

// Step 3: Verify signature
const isValid = await verifier.verifyUser({
  walletAddress: '0xUserWalletAddress',
  userId: 'user123',
  signature: '0xUserSignature',
});

if (isValid) {
  console.log('User authenticated successfully');
} else {
  console.log('Authentication failed');
}

Best Practices

  1. Nonce Usage: Always use nonces to prevent replay attacks
  2. Message Format: Use consistent message formats across your application
  3. Error Handling: Implement proper error handling for verification failures
  4. Security: Never trust client-side verification - always verify on the server
  5. Nonce Management: Clear used nonces to prevent reuse

Common Use Cases

  • User Authentication: Verify wallet ownership for login
  • Transaction Approval: Verify user approval for transactions
  • Data Integrity: Ensure data hasn’t been modified
  • Access Control: Verify permissions for specific actions

Next Steps

I