Skip to main content

Creating a SIWE Message

In this first tutorial, you'll learn how to create Sign in with Ethereum (SIWE) messages using the official TypeScript library. This is the foundation of SIWE authentication - generating properly formatted messages that users will sign with their wallets.

note

Other supported libraries can be found at here

Learning Objectives

By the end of this tutorial, you'll understand:

  • How to install and import the SIWE library
  • The anatomy of a SIWE message
  • How to generate secure nonces
  • Best practices for message creation

Installation

First, let's set up a new Node.js project and install the required dependencies:

# Create a new project directory
mkdir siwe-tutorial
cd siwe-tutorial

# Initialize a new Node.js project
npm init -y

# Install SIWE library and ethers for Ethereum utilities
npm install siwe ethers

# Install development dependencies
npm install -D typescript @types/node ts-node

Basic Message Creation

Let's start by creating our first SIWE message. Create a new file called create-message.js:

const { SiweMessage } = require('siwe')

// Basic SIWE message configuration
const domain = 'localhost:3000'
const origin = 'http://localhost:3000'
const address = '0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890'

function createBasicMessage() {
const message = new SiweMessage({
domain: domain,
address: address,
statement: 'Sign in to our awesome Web3 app!',
uri: origin,
version: '1',
chainId: 1, // Ethereum mainnet
})

// Generate the formatted message string
const messageString = message.prepareMessage()
console.log('Generated SIWE Message:')
console.log(messageString)

return message
}

// Run the function
createBasicMessage()

Run this script:

node create-message.js

You should see output similar to:

Generated SIWE Message:
localhost:3000 wants you to sign in with your Ethereum account:
0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890

Sign in to our awesome Web3 app!

URI: http://localhost:3000
Version: 1
Chain ID: 1
Nonce: 32891756
Issued At: 2023-10-31T16:25:24Z

Understanding Message Components

Let's break down what each part of the message does:

Required Fields

const message = new SiweMessage({
// The domain requesting the signature
domain: 'localhost:3000',

// User's Ethereum address (EIP-55 checksum format)
address: '0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890',

// The URI being signed (usually your app's login endpoint)
uri: 'http://localhost:3000',

// SIWE specification version (always "1")
version: '1',

// Blockchain network (1 = Ethereum mainnet, 5 = Goerli, etc.)
chainId: 1,
})

Optional Fields

You can enhance messages with additional security and context:

const enhancedMessage = new SiweMessage({
domain: 'localhost:3000',
address: address,
uri: origin,
version: '1',
chainId: 1,

// Optional human-readable statement
statement:
'Welcome to our decentralized application. By signing this message, you agree to our terms of service.',

// Custom nonce (if not provided, one is generated automatically)
nonce: 'custom-nonce-12345',

// Message expiration (1 hour from now)
expirationTime: new Date(Date.now() + 60 * 60 * 1000).toISOString(),

// Message valid from (current time)
notBefore: new Date().toISOString(),

// Request identifier for tracking
requestId: 'auth-request-001',

// Resources the user is requesting access to
resources: ['https://api.example.com/user-data', 'ipfs://QmHash123...'],
})

Secure Nonce Generation

Nonces prevent replay attacks by ensuring each signature is unique. The SIWE library provides a secure nonce generator:

const { SiweMessage, generateNonce } = require('siwe')

function createMessageWithSecureNonce() {
// Generate a cryptographically secure nonce
const nonce = generateNonce()
console.log('Generated nonce:', nonce)

const message = new SiweMessage({
domain: 'localhost:3000',
address: '0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890',
statement: 'Secure authentication with unique nonce',
uri: 'http://localhost:3000',
version: '1',
chainId: 1,
nonce: nonce, // Use the generated nonce
})

return message
}

TypeScript Implementation

For better type safety, let's create a TypeScript version. Create create-message.ts:

import { SiweMessage, generateNonce } from 'siwe'

interface MessageOptions {
domain: string
address: string
uri: string
chainId: number
statement?: string
expirationTime?: string
}

function createSiweMessage(options: MessageOptions): SiweMessage {
const message = new SiweMessage({
domain: options.domain,
address: options.address,
statement: options.statement || 'Sign in with Ethereum to authenticate',
uri: options.uri,
version: '1',
chainId: options.chainId,
nonce: generateNonce(),
issuedAt: new Date().toISOString(),
expirationTime: options.expirationTime,
})

return message
}

// Example usage
const messageOptions: MessageOptions = {
domain: 'myapp.com',
address: '0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890',
uri: 'https://myapp.com/login',
chainId: 1,
statement: 'Welcome to MyApp! Sign this message to authenticate.',
expirationTime: new Date(Date.now() + 10 * 60 * 1000).toISOString(), // 10 minutes
}

const siweMessage = createSiweMessage(messageOptions)
console.log(siweMessage.prepareMessage())

Production Best Practices

Server-Side Message Creation

In production applications, always create SIWE messages on the server:

// ❌ DON'T: Client-side message creation
// This allows clients to manipulate security-critical fields

// ✅ DO: Server-side message creation
function createServerSideMessage(userAddress, clientDomain) {
// Verify the domain matches your application
if (clientDomain !== 'myapp.com') {
throw new Error('Invalid domain')
}

const message = new SiweMessage({
domain: 'myapp.com', // Use server-controlled domain
address: userAddress,
statement: 'Authenticate with MyApp',
uri: 'https://myapp.com/auth',
version: '1',
chainId: 1,
nonce: generateNonce(), // Server-generated nonce
issuedAt: new Date().toISOString(), // Server timestamp
})

return message
}

Nonce Management

Store and validate nonces to prevent replay attacks:

// Simple in-memory nonce storage (use Redis/database in production)
const usedNonces = new Set()

function createMessageWithNonceTracking(address) {
const nonce = generateNonce()

// Store nonce as pending
usedNonces.add(nonce)

const message = new SiweMessage({
domain: 'localhost:3000',
address: address,
uri: 'http://localhost:3000',
version: '1',
chainId: 1,
nonce: nonce,
issuedAt: new Date().toISOString(),
expirationTime: new Date(Date.now() + 5 * 60 * 1000).toISOString(), // 5 minutes
})

return { message, nonce }
}

function validateNonce(nonce) {
if (!usedNonces.has(nonce)) {
throw new Error('Invalid or expired nonce')
}

// Remove nonce after use to prevent replay
usedNonces.delete(nonce)
return true
}

Error Handling

Always handle potential errors when creating messages:

function safeCreateMessage(options) {
try {
// Validate required fields
if (!options.domain || !options.address || !options.uri) {
throw new Error('Missing required fields')
}

// Validate Ethereum address format
if (!/^0x[a-fA-F0-9]{40}$/.test(options.address)) {
throw new Error('Invalid Ethereum address format')
}

// Create message
const message = new SiweMessage({
domain: options.domain,
address: options.address,
statement: options.statement,
uri: options.uri,
version: '1',
chainId: options.chainId || 1,
nonce: generateNonce(),
issuedAt: new Date().toISOString(),
})

return { success: true, message }
} catch (error) {
console.error('Error creating SIWE message:', error.message)
return { success: false, error: error.message }
}
}

Testing Your Implementation

Create a test script to verify your message creation:

const { SiweMessage, generateNonce } = require('siwe')

function testMessageCreation() {
console.log('Testing SIWE message creation...\n')

// Test 1: Basic message
const basicMessage = new SiweMessage({
domain: 'test.com',
address: '0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890',
uri: 'http://test.com',
version: '1',
chainId: 1,
})

console.log('✅ Basic message created successfully')
console.log('Message preview:')
console.log(basicMessage.prepareMessage().substring(0, 100) + '...\n')

// Test 2: Enhanced message with all optional fields
const enhancedMessage = new SiweMessage({
domain: 'test.com',
address: '0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890',
statement: 'Test authentication message',
uri: 'http://test.com/auth',
version: '1',
chainId: 1,
nonce: generateNonce(),
issuedAt: new Date().toISOString(),
expirationTime: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
resources: ['https://api.test.com/data'],
})

console.log('✅ Enhanced message created successfully')
console.log('Nonce:', enhancedMessage.nonce)
console.log('Issued at:', enhancedMessage.issuedAt)
console.log('Expires at:', enhancedMessage.expirationTime)
}

testMessageCreation()