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({
walletMetadata,
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
- Nonce Usage: Always use nonces to prevent replay attacks
- Message Format: Use consistent message formats across your application
- Error Handling: Implement proper error handling for verification failures
- Security: Never trust client-side verification - always verify on the server
- 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
Last modified on May 21, 2026