Sign-In with Ethereum: A Simple Guide for Traditional Web Developers

22 Aug 2025

22 Aug 2025 by Luke Puplett - Founder

Luke Puplett Founder

Sign-In with Ethereum: A Simple Guide for Traditional Web Developers

If you're a traditional web developer looking to add Ethereum authentication to your app, you've probably heard of "Sign-In with Ethereum" (SIWE). But what exactly is it, and how does it differ from just "connecting a wallet"?

As the world moves increasingly onchain, SIWE is growing in importance. Wallet addresses are becoming more than just payment identifiers—they carry important information such as attestations via services like Ethereum Attestation Service and other similar public utilities on other chains. These attestations can include verified credentials, reputation scores, and other trust signals that make wallet-based authentication increasingly valuable.

Here's the key difference: connecting a wallet just gets you an address—like asking someone for their phone number. Signing in with Ethereum proves they actually own that address—like having them sign a document with their fingerprint.

In this guide, I'll walk you through the high-level flow first, then dive into the technical details. By the end, you'll understand how to implement SIWE in your traditional web application while protecting your users' privacy.

The High-Level Flow: What Actually Happens

Think of SIWE as a three-step dance between your server, the user's wallet, and your database:

  1. Your server creates a message - A structured text that says "Sign this to prove you own wallet 0x123..."

  2. The user's wallet signs it - Their private key creates a cryptographic signature

  3. Your server verifies the signature - Confirms the signature matches the wallet address

That's it. No passwords, no email verification, no magic links. Just cryptographic proof that someone controls a specific Ethereum wallet.

But here's where it gets interesting for traditional web developers: you probably want to tie this wallet address to a user account in your database. And that's where privacy becomes crucial.

The Privacy Responsibility

Wallet addresses are pseudonymous until linked to personal data. If your database is compromised, you expose someone's entire blockchain history—every transaction, NFT, and token transfer linked to their real identity.

Privacy-first design options:

  • Don't store the address - Use it only for what you need (attestations, balances) and never persist it.

  • Don't collect personal data - Many modern web3 apps have no backend database—everything is onchain.

  • Hash for authentication only - Never store plaintext addresses alongside personal data.

See our wallet privacy guide for more details.

Understanding the Signing Process

Now let's get technical. When a user signs a message with their Ethereum wallet, several things happen:

EIP-191: The Standard for Personal Message Signing

Ethereum wallets don't just sign your message as-is. They follow a standard called EIP-191 that adds a prefix to prevent replay attacks:

"\x19Ethereum Signed Message:\n" + length + message

This prefix ensures that a signature created for your app can't be reused on another app. It's like adding a watermark to a document.

SIWE Message Structure

Originally, you could ask users to sign any text. But SIWE (EIP-4361) introduced a structured format that includes important security fields:

example.com wants you to sign in with your Ethereum account:
0x1234567890123456789012345678901234567890

Sign in with Ethereum to prove you own this wallet.

URI: https://example.com
Version: 1
Chain ID: 1
Nonce: 32891756
Issued At: 2021-01-27T17:09:38.578Z
Expiration Time: 2021-01-27T17:09:38.578Z

This structured format includes:

  • Domain - Which website is requesting the signature

  • Wallet Address - The address being authenticated

  • Statement - What the user is signing for

  • Nonce - A unique number to prevent replay attacks

  • Timestamps - When the message was created and expires

  • Chain ID - Which blockchain network this is for

Traditional Website Implementation Flow

Here's how to implement SIWE in a traditional web application with a backend database. I'll use JavaScript examples for the backend code, but you can adapt this to Python, Go, Java, or any other language:

Step 1: Backend Creates the SIWE Message

Your server generates a structured SIWE message with a cryptographically secure nonce:

// Backend JavaScript (Node.js) - adapt to your language
// Node.js built-in crypto module for secure random generation
const crypto = require('crypto');

function createSignInMessage(uri, walletAddress) {
    // Generate cryptographically secure nonce
    const nonce = generateSecureNonce(process.env.NONCE_SECRET);
    
    // Create SIWE message structure
    const message = {
        domain: new URL(uri).hostname,
        address: walletAddress,
        statement: "Sign in with Ethereum to prove you own this wallet.",
        uri: uri,
        version: "1",
        chainId: "1", // or your target chain
        nonce: nonce,
        issuedAt: new Date().toISOString(),
        expirationTime: new Date(Date.now() + 3 * 60 * 1000).toISOString()
    };
    
    // Format as SIWE message string
    return formatSiweMessage(message);
}
Step 2: Frontend Requests Message and Signs

Your JavaScript gets the message from the server and asks the wallet to sign it:

// 1. Get SIWE message from server
const makeForm = new FormData();
makeForm.append('address', walletAddress);

const makeResponse = await fetch('/make-siwe-message', {
    method: 'POST',
    body: makeForm
});

let siweMessage = await makeResponse.text();
siweMessage = siweMessage.trim();

// 2. User signs with wallet
const signature = await provider.request({
    method: 'personal_sign',
    params: [siweMessage, walletAddress]
});
Step 3: Backend Verifies the Signature

Your server verifies the signature and creates the user session:

// Backend JavaScript (Node.js) - adapt to your language
// ethers.js is a popular Ethereum library that handles signature verification
// npm install ethers
const { ethers } = require('ethers');

async function verifySiweMessage(messageText, signature, requestUri) {
    try {
        // Parse and validate SIWE message
        const message = parseSiweMessage(messageText);
        
        // Check if message is expired
        if (new Date(message.expirationTime) < new Date()) {
            return { success: false, error: "Message expired" };
        }
        
        // Verify nonce hasn't been used before
        if (!(await isNonceValid(message.nonce))) {
            return { success: false, error: "Invalid nonce" };
        }
        
        // Verify signature matches wallet address
        if (!verifySignature(messageText, signature, message.address)) {
            return { success: false, error: "Invalid signature" };
        }
        
        // Hash wallet address for privacy protection
        const hashedAddress = hashWalletAddress(message.address);
        
        // Create user session
        const session = createUserSession(hashedAddress);
        
        return { success: true, session: session };
    } catch (error) {
        return { success: false, error: error.message };
    }
}

Key Implementation Details

Nonce Management

Nonces prevent replay attacks. Each SIWE message should have a unique nonce that can only be used once:

// Backend JavaScript (Node.js) - adapt to your language
// Node.js built-in crypto module for secure random generation and HMAC
const crypto = require('crypto');

function generateSecureNonce(secretKey) {
    // Generate 32 bytes of random entropy
    const entropy = crypto.randomBytes(32);
    
    // Create HMAC signature using secret key
    const hmac = crypto.createHmac('sha256', secretKey);
    hmac.update(entropy);
    const signature = hmac.digest();
    
    // Combine entropy + signature and convert to hex
    const combined = Buffer.concat([entropy, signature]);
    return combined.toString('hex');
}

async function isNonceValid(nonce) {
    // Check if nonce has been used before
    if (await nonceExistsInDatabase(nonce)) {
        return false;
    }
    
    // Store nonce in database to prevent reuse
    await storeNonceInDatabase(nonce);
    
    // Clean up old nonces (older than 7 days)
    await cleanupOldNonces();
    
    return true;
}
Signature Verification

Verify signatures using the EIP-191 prefix:

// Backend JavaScript (Node.js) - adapt to your language
// ethers.js handles the complex EIP-191 prefixing and signature recovery
// npm install ethers
const { ethers } = require('ethers');

function verifySignature(messageText, signature, expectedAddress) {
    try {
        // Apply EIP-191 prefix: "\x19Ethereum Signed Message:\n" + length + message
        const messageBytes = ethers.utils.toUtf8Bytes(messageText);
        const prefixedMessage = ethers.utils.hashMessage(messageBytes);
        
        // Recover address from signature
        const recoveredAddress = ethers.utils.verifyMessage(messageText, signature);
        
        // Compare with expected address (case-insensitive)
        return recoveredAddress.toLowerCase() === expectedAddress.toLowerCase();
    } catch (error) {
        return false;
    }
}
Privacy-First Storage

Never store wallet addresses in plaintext. Hash them for authentication:

// Backend JavaScript (Node.js) - adapt to your language
// bcrypt is a popular password hashing library - perfect for wallet addresses too
// npm install bcrypt
const bcrypt = require('bcrypt');

async function hashWalletAddress(address) {
    // Generate cryptographically secure salt
    const saltRounds = 12;
    const salt = await bcrypt.genSalt(saltRounds);
    
    // Hash address with salt using bcrypt
    const hashedAddress = await bcrypt.hash(address, salt);
    
    // Store only the hash and salt, never the plaintext address
    return { hashedAddress: hashedAddress, salt: salt };
}

async function verifyWalletAddress(address, hashedAddress) {
    return await bcrypt.compare(address, hashedAddress);
}

Common Pitfalls to Avoid

  • Storing plaintext addresses - Always hash wallet addresses for authentication

  • Weak nonces - Use cryptographically secure random nonces

  • No expiration - SIWE messages should expire (we use 3 minutes)

  • Ignoring chain ID - Verify the user is on the expected network

  • Reusing nonces - Each message should have a unique nonce

Why This Matters for Traditional Web Apps

For traditional web applications, SIWE offers several advantages:

  • No password management - Users don't need to remember passwords

  • Enhanced security - Private keys are more secure than passwords

  • User control - Users control their own authentication

  • Blockchain integration - Ready for Web3 features

But remember: with great power comes great responsibility. If you're storing wallet addresses alongside personal data, you must implement proper privacy protections.

Getting Started

Ready to implement SIWE in your application? Here's what you need:

  1. Choose a library - Use established SIWE libraries for your platform

  2. Implement the flow - Follow the three-step process outlined above

  3. Add privacy protections - Hash wallet addresses for storage

  4. Test thoroughly - Verify signature validation and nonce management

At Zipwire, we've built this approach into our platform. We hash wallet addresses for authentication and only encrypt the actual address when needed for blockchain operations like attestations. This gives users the benefits of blockchain authentication while protecting their privacy.

For more details on our privacy-first approach to blockchain authentication, check out our wallet privacy guide and our blockchain attestation documentation.

Remember: SIWE is just signing some text. But how you handle that signed data makes all the difference for your users' privacy and security.


That's lovely and everything but what is Zipwire?

Zipwire Collect handles document collection for KYC, KYB, AML, RTW and RTR compliance. Used by recruiters, agencies, landlords, accountants, solicitors and anyone needing to gather and verify ID documents.

Zipwire Approve manages contractor timesheets and payments for recruiters, agencies and people ops. Features WhatsApp time tracking, approval workflows and reporting to cut paperwork, not corners.

Zipwire Attest provides self-service identity verification with blockchain attestations for proof of personhood, proof of age, and selective disclosure of passport details and AML results.

For contractors & temps, Zipwire Approve handles time journalling via WhatsApp, and techies can even use the command line. It pings your boss for approval, reducing friction and speeding up payday. Imagine just speaking what you worked on into your phone or car, and a few days later, money arrives. We've done the first part and now we're working on instant pay.

All three solutions aim to streamline workflows and ensure compliance, making work life easier for all parties involved. It's free for small teams, and you pay only for what you use.

Learn more