TypeScript library implementing the Sign-In with Ethereum (EIP-4361) specification for decentralized authentication
npx @tessl/cli install tessl/npm-siwe@3.0.0SIWE 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.
This Knowledge Tile documents two related packages:
Main Package:
npm install siweethers (v5.6.8+ or v6.0.8+)Parser Package:
npm install @spruceid/siwe-parser@noble/hashes, apg-jsimport { 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");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);
}SIWE is built around several key components:
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);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
});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.'
}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)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.
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
}// 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()
});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@spruceid/siwe-parser)The SIWE parser package provides low-level parsing and validation utilities for working with SIWE messages and related data formats.
npm install @spruceid/siwe-parserParse 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"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)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")); // falseParse 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);
}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
});