CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-siwe

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

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

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
});
Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/siwe@3.0.x
Publish Source
CLI
Badge
tessl/npm-siwe badge