or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.md
tile.json

tessl/npm-siwe

TypeScript library implementing the Sign-In with Ethereum (EIP-4361) specification for decentralized authentication

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/siwe@3.0.x

To install, run

npx @tessl/cli install tessl/npm-siwe@3.0.0

index.mddocs/

SIWE (Sign-In with Ethereum)

SIWE is a TypeScript library implementing the EIP-4361 specification for Sign-In with Ethereum. It enables Ethereum-based authentication by creating, parsing, and verifying signed messages that provide a self-custodial alternative to centralized identity providers.

Package Information

This Knowledge Tile documents two related packages:

Main Package:

  • Package Name: siwe
  • Package Type: npm
  • Language: TypeScript
  • Installation: npm install siwe
  • Peer Dependencies: ethers (v5.6.8+ or v6.0.8+)

Parser Package:

  • Package Name: @spruceid/siwe-parser
  • Package Type: npm
  • Language: TypeScript
  • Installation: npm install @spruceid/siwe-parser
  • Dependencies: @noble/hashes, apg-js

Core Imports

import { SiweMessage } from "siwe";

For additional utilities and types:

import { 
  SiweMessage, 
  SiweError, 
  SiweErrorType, 
  generateNonce,
  checkContractWalletSignature,
  isValidISO8601Date,
  checkInvalidKeys,
  VerifyParamsKeys,
  VerifyOptsKeys,
  type VerifyParams,
  type VerifyOpts,
  type SiweResponse
} from "siwe";

For parser functionality (from @spruceid/siwe-parser):

import { 
  ParsedMessage,
  isUri,
  isEIP55Address,
  parseIntegerNumber
} from "@spruceid/siwe-parser";

CommonJS:

const { 
  SiweMessage, 
  generateNonce, 
  SiweError, 
  SiweErrorType,
  checkContractWalletSignature,
  isValidISO8601Date,
  checkInvalidKeys,
  VerifyParamsKeys,
  VerifyOptsKeys
} = require("siwe");

Basic Usage

import { SiweMessage } from "siwe";

// Create a SIWE message
const message = new SiweMessage({
  domain: "example.com",
  address: "0x1234567890123456789012345678901234567890",
  uri: "https://example.com/auth",
  version: "1",
  chainId: 1,
  nonce: "12345678" // or let it auto-generate
});

// Get message string for signing
const messageString = message.prepareMessage();

// After user signs the message with their wallet
const signature = "0x..."; // from wallet

// Verify the signature
try {
  const result = await message.verify({
    signature: signature
  });
  
  if (result.success) {
    console.log("Authentication successful!");
  }
} catch (error) {
  console.error("Verification failed:", error);
}

Architecture

SIWE is built around several key components:

  • SiweMessage Class: Core class handling message creation, formatting, and verification
  • EIP-4361 Compliance: Full implementation of the Sign-In with Ethereum specification
  • Signature Verification: Supports both EOA (Externally Owned Account) and smart contract wallets via EIP-1271
  • Ethers Compatibility: Works with both ethers v5 and v6 through a compatibility layer
  • Cryptographic Security: Uses secure nonce generation and proper message formatting

Capabilities

Message Creation and Management

Create and manage SIWE messages according to the EIP-4361 specification.

class SiweMessage {
  // Message properties
  scheme?: string;
  domain: string;
  address: string;
  statement?: string;
  uri: string;
  version: string;
  chainId: number;
  nonce: string;
  issuedAt?: string;
  expirationTime?: string;
  notBefore?: string;
  requestId?: string;
  resources?: Array<string>;

  constructor(param: string | Partial<SiweMessage>);
  toMessage(): string;
  prepareMessage(): string;
  verify(params: VerifyParams, opts?: VerifyOpts): Promise<SiweResponse>;
}

The SiweMessage class properties follow the EIP-4361 specification:

  • scheme - RFC 3986 URI scheme for the authority (optional)
  • domain - RFC 4501 DNS authority requesting the signing (required)
  • address - Ethereum address with EIP-55 checksum (required)
  • statement - Human-readable assertion, no newlines (optional)
  • uri - RFC 3986 URI referring to the resource (required)
  • version - Message version, currently "1" (required)
  • chainId - EIP-155 Chain ID (required)
  • nonce - Randomized token, minimum 8 alphanumeric characters (required)
  • issuedAt - ISO 8601 datetime of current time (auto-generated if not provided)
  • expirationTime - ISO 8601 datetime when message expires (optional)
  • notBefore - ISO 8601 datetime when message becomes valid (optional)
  • requestId - System-specific identifier (optional)
  • resources - Array of RFC 3986 URIs (optional)

Usage Examples:

// Create from object parameters
const message = new SiweMessage({
  domain: "example.com",
  address: "0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5",
  uri: "https://example.com/login",
  version: "1",
  chainId: 1,
  statement: "Welcome to our dApp!"
});

// Create from existing message string
const existingMessage = `example.com wants you to sign in with your Ethereum account:
0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5

Welcome to our dApp!

URI: https://example.com/login
Version: 1
Chain ID: 1
Nonce: abcd1234
Issued At: 2024-01-01T00:00:00.000Z`;

const parsedMessage = new SiweMessage(existingMessage);

Message Verification

Verify SIWE message signatures with comprehensive validation.

interface VerifyParams {
  signature: string;
  scheme?: string;
  domain?: string;
  nonce?: string;
  time?: string;
}

interface VerifyOpts {
  // Compatible with both ethers v5 (providers.Provider) and v6 (Provider)  
  provider?: any; // ethers.Provider | ethers.providers.Provider
  suppressExceptions?: boolean;
  verificationFallback?: (
    params: VerifyParams,
    opts: VerifyOpts,
    message: SiweMessage,
    EIP1271Promise: Promise<SiweResponse>
  ) => Promise<SiweResponse>;
}

interface SiweResponse {
  success: boolean;
  error?: SiweError;
  data: SiweMessage;
}

const VerifyParamsKeys: Array<keyof VerifyParams>;
const VerifyOptsKeys: Array<keyof VerifyOpts>;

Usage Examples:

// Basic verification
const result = await message.verify({
  signature: "0x..."
});

// Verification with domain binding
const result = await message.verify({
  signature: "0x...",
  domain: "example.com"
});

// Verification with custom time for testing
const result = await message.verify({
  signature: "0x...",
  time: "2024-01-01T12:00:00.000Z"
});

// EIP-1271 smart contract wallet verification
const provider = new ethers.providers.JsonRpcProvider("...");
const result = await message.verify({
  signature: "0x..."
}, {
  provider: provider
});

// Suppress exceptions (returns error in response instead of throwing)
const result = await message.verify({
  signature: "0x..."
}, {
  suppressExceptions: true
});

Error Handling

Comprehensive error types for different failure scenarios.

class SiweError {
  constructor(
    type: SiweErrorType | string,
    expected?: string,
    received?: string
  );
  
  type: SiweErrorType | string;
  expected?: string;
  received?: string;
}

enum SiweErrorType {
  EXPIRED_MESSAGE = 'Expired message.',
  INVALID_DOMAIN = 'Invalid domain.',
  SCHEME_MISMATCH = 'Scheme does not match provided scheme for verification.',
  DOMAIN_MISMATCH = 'Domain does not match provided domain for verification.',
  NONCE_MISMATCH = 'Nonce does not match provided nonce for verification.',
  INVALID_ADDRESS = 'Invalid address.',
  INVALID_URI = 'URI does not conform to RFC 3986.',
  INVALID_NONCE = 'Nonce size smaller then 8 characters or is not alphanumeric.',
  NOT_YET_VALID_MESSAGE = 'Message is not valid yet.',
  INVALID_SIGNATURE = 'Signature does not match address of the message.',
  INVALID_TIME_FORMAT = 'Invalid time format.',
  INVALID_MESSAGE_VERSION = 'Invalid message version.',
  UNABLE_TO_PARSE = 'Unable to parse the message.'
}

Utility Functions

Helper functions for nonce generation and validation.

/**
 * Generates cryptographically secure 96-bit nonce
 * @returns Random alphanumeric string with 96 bits of entropy
 */
function generateNonce(): string;

/**
 * Validates ISO-8601 date format
 * @param inputDate - Date string to validate
 * @returns True if valid ISO-8601 format
 */
function isValidISO8601Date(inputDate: string): boolean;

/**
 * Validates object keys against allowed keys
 * @param obj - Object to validate
 * @param keys - Array of allowed keys
 * @returns Array of invalid keys found in obj
 */
function checkInvalidKeys<T>(obj: T, keys: Array<keyof T>): Array<keyof T>;

/**
 * Validates EIP-1271 smart contract wallet signature
 * @param message - SIWE message instance
 * @param signature - Signature to verify
 * @param provider - Ethers provider or signer for contract interaction (v5/v6 compatible)
 * @returns True if signature is valid according to EIP-1271
 */
function checkContractWalletSignature(
  message: SiweMessage,
  signature: string,
  provider?: any // ethers.Provider | ethers.providers.Provider | ethers.Signer
): Promise<boolean>;

Usage Examples:

import { generateNonce, isValidISO8601Date } from "siwe";

// Generate secure nonce
const nonce = generateNonce();
console.log(nonce); // e.g., "Qm9fJ2KxN8Lp"

// Validate date format
const isValid = isValidISO8601Date("2024-01-01T00:00:00.000Z");
console.log(isValid); // true

const invalid = isValidISO8601Date("2024-13-01T00:00:00.000Z");
console.log(invalid); // false

// Validate parameter keys (used internally for error checking)
const validKeys = checkInvalidKeys({ signature: "0x..." }, VerifyParamsKeys);
console.log(validKeys); // [] (empty array means all keys are valid)

Ethers Compatibility

Cross-version compatibility utilities for ethers v5 and v6. These functions are used internally by SIWE to provide compatibility between different ethers versions.

// Note: These functions are internal compatibility utilities
// They are not directly exported for external use

/**
 * Internal: Verify message signature (compatible with ethers v5/v6)
 * Used internally by SiweMessage.verify()
 */
// function verifyMessage(message: Uint8Array | string, signature: string): string;

/**
 * Internal: Hash message for signing (compatible with ethers v5/v6)  
 * Used internally for EIP-1271 verification
 */
// function hashMessage(message: Uint8Array | string): string;

/**
 * Internal: Get normalized address (compatible with ethers v5/v6)
 * Used internally for address validation
 */
// function getAddress(address: string): string;

These functions provide an internal compatibility layer that works with both ethers v5 (ethers.utils.*) and ethers v6 (ethers.*) APIs. They are automatically used by SIWE when you have either version of ethers installed as a peer dependency.

Common Patterns

Complete Authentication Flow

import { SiweMessage } from "siwe";
import { ethers } from "ethers";

// 1. Create message on server
const message = new SiweMessage({
  domain: "myapp.com",
  address: userAddress,
  uri: "https://myapp.com/login",
  version: "1",
  chainId: 1,
  statement: "Sign in to MyApp"
});

// 2. Send message to client for signing
const messageString = message.prepareMessage();

// 3. Client signs message (in frontend)
const signature = await wallet.signMessage(messageString);

// 4. Verify signature on server
try {
  const result = await message.verify({
    signature: signature,
    domain: "myapp.com"
  });
  
  if (result.success) {
    // Authentication successful
    // Create session, JWT, etc.
  }
} catch (error) {
  // Handle verification error
}

Time-Based Validation

// Create message with expiration
const message = new SiweMessage({
  domain: "example.com",
  address: userAddress,
  uri: "https://example.com/login",
  version: "1",
  chainId: 1,
  expirationTime: new Date(Date.now() + 5 * 60 * 1000).toISOString(), // 5 minutes
  notBefore: new Date().toISOString()
});

// Verify at specific time
const result = await message.verify({
  signature: signature,
  time: new Date().toISOString()
});

Smart Contract Wallet Support

import { ethers } from "ethers";

// For ethers v5
const provider = new ethers.providers.JsonRpcProvider("https://mainnet.infura.io/v3/...");

// For ethers v6  
// const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/...");

const result = await message.verify({
  signature: signature
}, {
  provider: provider
});

// This will automatically attempt EIP-1271 verification for smart contract wallets
// Falls back to standard signature verification for EOA wallets

Parser Package (@spruceid/siwe-parser)

The SIWE parser package provides low-level parsing and validation utilities for working with SIWE messages and related data formats.

Installation

npm install @spruceid/siwe-parser

Message Parsing

Parse raw SIWE message strings into structured objects using ABNF grammar validation.

class ParsedMessage {
  scheme: string | undefined;
  domain: string;
  address: string;
  statement: string | undefined;
  uri: string;
  version: string;
  chainId: number;
  nonce: string;
  issuedAt: string;
  expirationTime: string | undefined;
  notBefore: string | undefined;
  requestId: string | undefined;
  resources: Array<string> | undefined;
  uriElements: {
    scheme: string;
    userinfo: string | undefined;
    host: string | undefined;
    port: string | undefined;
    path: string;
    query: string | undefined;
    fragment: string | undefined;
  };

  constructor(msg: string);
}

Usage Examples:

import { ParsedMessage } from "@spruceid/siwe-parser";

// Parse a raw SIWE message string
const messageString = `example.com wants you to sign in with your Ethereum account:
0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5

Welcome to our dApp!

URI: https://example.com/login
Version: 1
Chain ID: 1
Nonce: abcd1234
Issued At: 2024-01-01T00:00:00.000Z`;

const parsed = new ParsedMessage(messageString);

console.log(parsed.domain); // "example.com"
console.log(parsed.address); // "0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5"
console.log(parsed.chainId); // 1
console.log(parsed.statement); // "Welcome to our dApp!"
console.log(parsed.uriElements.scheme); // "https"
console.log(parsed.uriElements.host); // "example.com"

URI Validation

Validate URI strings according to RFC 3986 specification.

/**
 * Validates URI format according to RFC 3986
 * @param uri - URI string to validate
 * @returns True if URI is valid according to RFC 3986
 */
function isUri(uri: string): boolean;

Usage Examples:

import { isUri } from "@spruceid/siwe-parser";

console.log(isUri("https://example.com/path")); // true
console.log(isUri("ftp://files.example.com")); // true
console.log(isUri("not-a-uri")); // false
console.log(isUri("http://[::1]:8080")); // true (IPv6)

EIP-55 Address Validation

Validate Ethereum addresses according to EIP-55 checksum encoding.

/**
 * Validates Ethereum address EIP-55 checksum encoding
 * @param address - Ethereum address to validate
 * @returns True if address conforms to EIP-55 format
 */
function isEIP55Address(address: string): boolean;

Usage Examples:

import { isEIP55Address } from "@spruceid/siwe-parser";

// Valid EIP-55 addresses
console.log(isEIP55Address("0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5")); // true
console.log(isEIP55Address("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")); // true

// Invalid addresses
console.log(isEIP55Address("0x742c3cf9af45f91b109a81efeaf11535ecde24c5")); // false (wrong case)
console.log(isEIP55Address("0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C")); // false (wrong length)
console.log(isEIP55Address("not-an-address")); // false

Safe Integer Parsing

Parse string numbers safely with proper error handling.

/**
 * Safely parse string to integer with validation
 * @param number - String number to parse
 * @returns Parsed integer
 * @throws Error if string is not a valid number or is infinite
 */
function parseIntegerNumber(number: string): number;

Usage Examples:

import { parseIntegerNumber } from "@spruceid/siwe-parser";

console.log(parseIntegerNumber("123")); // 123
console.log(parseIntegerNumber("0")); // 0
console.log(parseIntegerNumber("1337")); // 1337

// These will throw errors
try {
  parseIntegerNumber("abc"); // Error: Invalid number.
} catch (e) {
  console.error(e.message);
}

try {
  parseIntegerNumber("Infinity"); // Error: Invalid number.  
} catch (e) {
  console.error(e.message);
}

Parser Integration Pattern

The parser package is typically used internally by the main SIWE package, but can be used directly for low-level operations:

import { ParsedMessage, isUri, isEIP55Address } from "@spruceid/siwe-parser";
import { SiweMessage } from "siwe";

// Validate components before creating SIWE message
const uri = "https://example.com/login";
const address = "0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5";

if (!isUri(uri)) {
  throw new Error("Invalid URI format");
}

if (!isEIP55Address(address)) {
  throw new Error("Invalid address checksum");
}

// Create SIWE message (this internally uses ParsedMessage for validation)
const message = new SiweMessage({
  domain: "example.com",
  address: address,
  uri: uri,
  version: "1",
  chainId: 1
});

// Alternatively, parse an existing message string directly
const existingMessage = "..."; // some SIWE message string
const parsed = new ParsedMessage(existingMessage);

// Convert parsed message back to SiweMessage for verification
const siweFromParsed = new SiweMessage({
  scheme: parsed.scheme,
  domain: parsed.domain,
  address: parsed.address,
  statement: parsed.statement,
  uri: parsed.uri,
  version: parsed.version,
  chainId: parsed.chainId,
  nonce: parsed.nonce,
  issuedAt: parsed.issuedAt,
  expirationTime: parsed.expirationTime,
  notBefore: parsed.notBefore,
  requestId: parsed.requestId,
  resources: parsed.resources
});