or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

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

0

# SIWE (Sign-In with Ethereum)

1

2

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.

3

4

## Package Information

5

6

This Knowledge Tile documents two related packages:

7

8

**Main Package:**

9

- **Package Name**: siwe

10

- **Package Type**: npm

11

- **Language**: TypeScript

12

- **Installation**: `npm install siwe`

13

- **Peer Dependencies**: `ethers` (v5.6.8+ or v6.0.8+)

14

15

**Parser Package:**

16

- **Package Name**: @spruceid/siwe-parser

17

- **Package Type**: npm

18

- **Language**: TypeScript

19

- **Installation**: `npm install @spruceid/siwe-parser`

20

- **Dependencies**: `@noble/hashes`, `apg-js`

21

22

## Core Imports

23

24

```typescript

25

import { SiweMessage } from "siwe";

26

```

27

28

For additional utilities and types:

29

30

```typescript

31

import {

32

SiweMessage,

33

SiweError,

34

SiweErrorType,

35

generateNonce,

36

checkContractWalletSignature,

37

isValidISO8601Date,

38

checkInvalidKeys,

39

VerifyParamsKeys,

40

VerifyOptsKeys,

41

type VerifyParams,

42

type VerifyOpts,

43

type SiweResponse

44

} from "siwe";

45

```

46

47

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

48

49

```typescript

50

import {

51

ParsedMessage,

52

isUri,

53

isEIP55Address,

54

parseIntegerNumber

55

} from "@spruceid/siwe-parser";

56

```

57

58

CommonJS:

59

60

```javascript

61

const {

62

SiweMessage,

63

generateNonce,

64

SiweError,

65

SiweErrorType,

66

checkContractWalletSignature,

67

isValidISO8601Date,

68

checkInvalidKeys,

69

VerifyParamsKeys,

70

VerifyOptsKeys

71

} = require("siwe");

72

```

73

74

## Basic Usage

75

76

```typescript

77

import { SiweMessage } from "siwe";

78

79

// Create a SIWE message

80

const message = new SiweMessage({

81

domain: "example.com",

82

address: "0x1234567890123456789012345678901234567890",

83

uri: "https://example.com/auth",

84

version: "1",

85

chainId: 1,

86

nonce: "12345678" // or let it auto-generate

87

});

88

89

// Get message string for signing

90

const messageString = message.prepareMessage();

91

92

// After user signs the message with their wallet

93

const signature = "0x..."; // from wallet

94

95

// Verify the signature

96

try {

97

const result = await message.verify({

98

signature: signature

99

});

100

101

if (result.success) {

102

console.log("Authentication successful!");

103

}

104

} catch (error) {

105

console.error("Verification failed:", error);

106

}

107

```

108

109

## Architecture

110

111

SIWE is built around several key components:

112

113

- **SiweMessage Class**: Core class handling message creation, formatting, and verification

114

- **EIP-4361 Compliance**: Full implementation of the Sign-In with Ethereum specification

115

- **Signature Verification**: Supports both EOA (Externally Owned Account) and smart contract wallets via EIP-1271

116

- **Ethers Compatibility**: Works with both ethers v5 and v6 through a compatibility layer

117

- **Cryptographic Security**: Uses secure nonce generation and proper message formatting

118

119

## Capabilities

120

121

### Message Creation and Management

122

123

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

124

125

```typescript { .api }

126

class SiweMessage {

127

// Message properties

128

scheme?: string;

129

domain: string;

130

address: string;

131

statement?: string;

132

uri: string;

133

version: string;

134

chainId: number;

135

nonce: string;

136

issuedAt?: string;

137

expirationTime?: string;

138

notBefore?: string;

139

requestId?: string;

140

resources?: Array<string>;

141

142

constructor(param: string | Partial<SiweMessage>);

143

toMessage(): string;

144

prepareMessage(): string;

145

verify(params: VerifyParams, opts?: VerifyOpts): Promise<SiweResponse>;

146

}

147

```

148

149

The `SiweMessage` class properties follow the EIP-4361 specification:

150

151

- `scheme` - RFC 3986 URI scheme for the authority (optional)

152

- `domain` - RFC 4501 DNS authority requesting the signing (required)

153

- `address` - Ethereum address with EIP-55 checksum (required)

154

- `statement` - Human-readable assertion, no newlines (optional)

155

- `uri` - RFC 3986 URI referring to the resource (required)

156

- `version` - Message version, currently "1" (required)

157

- `chainId` - EIP-155 Chain ID (required)

158

- `nonce` - Randomized token, minimum 8 alphanumeric characters (required)

159

- `issuedAt` - ISO 8601 datetime of current time (auto-generated if not provided)

160

- `expirationTime` - ISO 8601 datetime when message expires (optional)

161

- `notBefore` - ISO 8601 datetime when message becomes valid (optional)

162

- `requestId` - System-specific identifier (optional)

163

- `resources` - Array of RFC 3986 URIs (optional)

164

165

**Usage Examples:**

166

167

```typescript

168

// Create from object parameters

169

const message = new SiweMessage({

170

domain: "example.com",

171

address: "0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5",

172

uri: "https://example.com/login",

173

version: "1",

174

chainId: 1,

175

statement: "Welcome to our dApp!"

176

});

177

178

// Create from existing message string

179

const existingMessage = `example.com wants you to sign in with your Ethereum account:

180

0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5

181

182

Welcome to our dApp!

183

184

URI: https://example.com/login

185

Version: 1

186

Chain ID: 1

187

Nonce: abcd1234

188

Issued At: 2024-01-01T00:00:00.000Z`;

189

190

const parsedMessage = new SiweMessage(existingMessage);

191

```

192

193

### Message Verification

194

195

Verify SIWE message signatures with comprehensive validation.

196

197

```typescript { .api }

198

interface VerifyParams {

199

signature: string;

200

scheme?: string;

201

domain?: string;

202

nonce?: string;

203

time?: string;

204

}

205

206

interface VerifyOpts {

207

// Compatible with both ethers v5 (providers.Provider) and v6 (Provider)

208

provider?: any; // ethers.Provider | ethers.providers.Provider

209

suppressExceptions?: boolean;

210

verificationFallback?: (

211

params: VerifyParams,

212

opts: VerifyOpts,

213

message: SiweMessage,

214

EIP1271Promise: Promise<SiweResponse>

215

) => Promise<SiweResponse>;

216

}

217

218

interface SiweResponse {

219

success: boolean;

220

error?: SiweError;

221

data: SiweMessage;

222

}

223

224

const VerifyParamsKeys: Array<keyof VerifyParams>;

225

const VerifyOptsKeys: Array<keyof VerifyOpts>;

226

```

227

228

**Usage Examples:**

229

230

```typescript

231

// Basic verification

232

const result = await message.verify({

233

signature: "0x..."

234

});

235

236

// Verification with domain binding

237

const result = await message.verify({

238

signature: "0x...",

239

domain: "example.com"

240

});

241

242

// Verification with custom time for testing

243

const result = await message.verify({

244

signature: "0x...",

245

time: "2024-01-01T12:00:00.000Z"

246

});

247

248

// EIP-1271 smart contract wallet verification

249

const provider = new ethers.providers.JsonRpcProvider("...");

250

const result = await message.verify({

251

signature: "0x..."

252

}, {

253

provider: provider

254

});

255

256

// Suppress exceptions (returns error in response instead of throwing)

257

const result = await message.verify({

258

signature: "0x..."

259

}, {

260

suppressExceptions: true

261

});

262

```

263

264

### Error Handling

265

266

Comprehensive error types for different failure scenarios.

267

268

```typescript { .api }

269

class SiweError {

270

constructor(

271

type: SiweErrorType | string,

272

expected?: string,

273

received?: string

274

);

275

276

type: SiweErrorType | string;

277

expected?: string;

278

received?: string;

279

}

280

281

enum SiweErrorType {

282

EXPIRED_MESSAGE = 'Expired message.',

283

INVALID_DOMAIN = 'Invalid domain.',

284

SCHEME_MISMATCH = 'Scheme does not match provided scheme for verification.',

285

DOMAIN_MISMATCH = 'Domain does not match provided domain for verification.',

286

NONCE_MISMATCH = 'Nonce does not match provided nonce for verification.',

287

INVALID_ADDRESS = 'Invalid address.',

288

INVALID_URI = 'URI does not conform to RFC 3986.',

289

INVALID_NONCE = 'Nonce size smaller then 8 characters or is not alphanumeric.',

290

NOT_YET_VALID_MESSAGE = 'Message is not valid yet.',

291

INVALID_SIGNATURE = 'Signature does not match address of the message.',

292

INVALID_TIME_FORMAT = 'Invalid time format.',

293

INVALID_MESSAGE_VERSION = 'Invalid message version.',

294

UNABLE_TO_PARSE = 'Unable to parse the message.'

295

}

296

```

297

298

### Utility Functions

299

300

Helper functions for nonce generation and validation.

301

302

```typescript { .api }

303

/**

304

* Generates cryptographically secure 96-bit nonce

305

* @returns Random alphanumeric string with 96 bits of entropy

306

*/

307

function generateNonce(): string;

308

309

/**

310

* Validates ISO-8601 date format

311

* @param inputDate - Date string to validate

312

* @returns True if valid ISO-8601 format

313

*/

314

function isValidISO8601Date(inputDate: string): boolean;

315

316

/**

317

* Validates object keys against allowed keys

318

* @param obj - Object to validate

319

* @param keys - Array of allowed keys

320

* @returns Array of invalid keys found in obj

321

*/

322

function checkInvalidKeys<T>(obj: T, keys: Array<keyof T>): Array<keyof T>;

323

324

/**

325

* Validates EIP-1271 smart contract wallet signature

326

* @param message - SIWE message instance

327

* @param signature - Signature to verify

328

* @param provider - Ethers provider or signer for contract interaction (v5/v6 compatible)

329

* @returns True if signature is valid according to EIP-1271

330

*/

331

function checkContractWalletSignature(

332

message: SiweMessage,

333

signature: string,

334

provider?: any // ethers.Provider | ethers.providers.Provider | ethers.Signer

335

): Promise<boolean>;

336

```

337

338

**Usage Examples:**

339

340

```typescript

341

import { generateNonce, isValidISO8601Date } from "siwe";

342

343

// Generate secure nonce

344

const nonce = generateNonce();

345

console.log(nonce); // e.g., "Qm9fJ2KxN8Lp"

346

347

// Validate date format

348

const isValid = isValidISO8601Date("2024-01-01T00:00:00.000Z");

349

console.log(isValid); // true

350

351

const invalid = isValidISO8601Date("2024-13-01T00:00:00.000Z");

352

console.log(invalid); // false

353

354

// Validate parameter keys (used internally for error checking)

355

const validKeys = checkInvalidKeys({ signature: "0x..." }, VerifyParamsKeys);

356

console.log(validKeys); // [] (empty array means all keys are valid)

357

```

358

359

### Ethers Compatibility

360

361

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

362

363

```typescript { .api }

364

// Note: These functions are internal compatibility utilities

365

// They are not directly exported for external use

366

367

/**

368

* Internal: Verify message signature (compatible with ethers v5/v6)

369

* Used internally by SiweMessage.verify()

370

*/

371

// function verifyMessage(message: Uint8Array | string, signature: string): string;

372

373

/**

374

* Internal: Hash message for signing (compatible with ethers v5/v6)

375

* Used internally for EIP-1271 verification

376

*/

377

// function hashMessage(message: Uint8Array | string): string;

378

379

/**

380

* Internal: Get normalized address (compatible with ethers v5/v6)

381

* Used internally for address validation

382

*/

383

// function getAddress(address: string): string;

384

```

385

386

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.

387

388

## Common Patterns

389

390

### Complete Authentication Flow

391

392

```typescript

393

import { SiweMessage } from "siwe";

394

import { ethers } from "ethers";

395

396

// 1. Create message on server

397

const message = new SiweMessage({

398

domain: "myapp.com",

399

address: userAddress,

400

uri: "https://myapp.com/login",

401

version: "1",

402

chainId: 1,

403

statement: "Sign in to MyApp"

404

});

405

406

// 2. Send message to client for signing

407

const messageString = message.prepareMessage();

408

409

// 3. Client signs message (in frontend)

410

const signature = await wallet.signMessage(messageString);

411

412

// 4. Verify signature on server

413

try {

414

const result = await message.verify({

415

signature: signature,

416

domain: "myapp.com"

417

});

418

419

if (result.success) {

420

// Authentication successful

421

// Create session, JWT, etc.

422

}

423

} catch (error) {

424

// Handle verification error

425

}

426

```

427

428

### Time-Based Validation

429

430

```typescript

431

// Create message with expiration

432

const message = new SiweMessage({

433

domain: "example.com",

434

address: userAddress,

435

uri: "https://example.com/login",

436

version: "1",

437

chainId: 1,

438

expirationTime: new Date(Date.now() + 5 * 60 * 1000).toISOString(), // 5 minutes

439

notBefore: new Date().toISOString()

440

});

441

442

// Verify at specific time

443

const result = await message.verify({

444

signature: signature,

445

time: new Date().toISOString()

446

});

447

```

448

449

### Smart Contract Wallet Support

450

451

```typescript

452

import { ethers } from "ethers";

453

454

// For ethers v5

455

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

456

457

// For ethers v6

458

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

459

460

const result = await message.verify({

461

signature: signature

462

}, {

463

provider: provider

464

});

465

466

// This will automatically attempt EIP-1271 verification for smart contract wallets

467

// Falls back to standard signature verification for EOA wallets

468

```

469

470

## Parser Package (`@spruceid/siwe-parser`)

471

472

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

473

474

### Installation

475

476

```bash

477

npm install @spruceid/siwe-parser

478

```

479

480

### Message Parsing

481

482

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

483

484

```typescript { .api }

485

class ParsedMessage {

486

scheme: string | undefined;

487

domain: string;

488

address: string;

489

statement: string | undefined;

490

uri: string;

491

version: string;

492

chainId: number;

493

nonce: string;

494

issuedAt: string;

495

expirationTime: string | undefined;

496

notBefore: string | undefined;

497

requestId: string | undefined;

498

resources: Array<string> | undefined;

499

uriElements: {

500

scheme: string;

501

userinfo: string | undefined;

502

host: string | undefined;

503

port: string | undefined;

504

path: string;

505

query: string | undefined;

506

fragment: string | undefined;

507

};

508

509

constructor(msg: string);

510

}

511

```

512

513

**Usage Examples:**

514

515

```typescript

516

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

517

518

// Parse a raw SIWE message string

519

const messageString = `example.com wants you to sign in with your Ethereum account:

520

0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5

521

522

Welcome to our dApp!

523

524

URI: https://example.com/login

525

Version: 1

526

Chain ID: 1

527

Nonce: abcd1234

528

Issued At: 2024-01-01T00:00:00.000Z`;

529

530

const parsed = new ParsedMessage(messageString);

531

532

console.log(parsed.domain); // "example.com"

533

console.log(parsed.address); // "0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5"

534

console.log(parsed.chainId); // 1

535

console.log(parsed.statement); // "Welcome to our dApp!"

536

console.log(parsed.uriElements.scheme); // "https"

537

console.log(parsed.uriElements.host); // "example.com"

538

```

539

540

### URI Validation

541

542

Validate URI strings according to RFC 3986 specification.

543

544

```typescript { .api }

545

/**

546

* Validates URI format according to RFC 3986

547

* @param uri - URI string to validate

548

* @returns True if URI is valid according to RFC 3986

549

*/

550

function isUri(uri: string): boolean;

551

```

552

553

**Usage Examples:**

554

555

```typescript

556

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

557

558

console.log(isUri("https://example.com/path")); // true

559

console.log(isUri("ftp://files.example.com")); // true

560

console.log(isUri("not-a-uri")); // false

561

console.log(isUri("http://[::1]:8080")); // true (IPv6)

562

```

563

564

### EIP-55 Address Validation

565

566

Validate Ethereum addresses according to EIP-55 checksum encoding.

567

568

```typescript { .api }

569

/**

570

* Validates Ethereum address EIP-55 checksum encoding

571

* @param address - Ethereum address to validate

572

* @returns True if address conforms to EIP-55 format

573

*/

574

function isEIP55Address(address: string): boolean;

575

```

576

577

**Usage Examples:**

578

579

```typescript

580

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

581

582

// Valid EIP-55 addresses

583

console.log(isEIP55Address("0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5")); // true

584

console.log(isEIP55Address("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")); // true

585

586

// Invalid addresses

587

console.log(isEIP55Address("0x742c3cf9af45f91b109a81efeaf11535ecde24c5")); // false (wrong case)

588

console.log(isEIP55Address("0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C")); // false (wrong length)

589

console.log(isEIP55Address("not-an-address")); // false

590

```

591

592

### Safe Integer Parsing

593

594

Parse string numbers safely with proper error handling.

595

596

```typescript { .api }

597

/**

598

* Safely parse string to integer with validation

599

* @param number - String number to parse

600

* @returns Parsed integer

601

* @throws Error if string is not a valid number or is infinite

602

*/

603

function parseIntegerNumber(number: string): number;

604

```

605

606

**Usage Examples:**

607

608

```typescript

609

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

610

611

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

612

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

613

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

614

615

// These will throw errors

616

try {

617

parseIntegerNumber("abc"); // Error: Invalid number.

618

} catch (e) {

619

console.error(e.message);

620

}

621

622

try {

623

parseIntegerNumber("Infinity"); // Error: Invalid number.

624

} catch (e) {

625

console.error(e.message);

626

}

627

```

628

629

### Parser Integration Pattern

630

631

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

632

633

```typescript

634

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

635

import { SiweMessage } from "siwe";

636

637

// Validate components before creating SIWE message

638

const uri = "https://example.com/login";

639

const address = "0x742C3cF9Af45f91B109a81EfEaf11535ECDe24C5";

640

641

if (!isUri(uri)) {

642

throw new Error("Invalid URI format");

643

}

644

645

if (!isEIP55Address(address)) {

646

throw new Error("Invalid address checksum");

647

}

648

649

// Create SIWE message (this internally uses ParsedMessage for validation)

650

const message = new SiweMessage({

651

domain: "example.com",

652

address: address,

653

uri: uri,

654

version: "1",

655

chainId: 1

656

});

657

658

// Alternatively, parse an existing message string directly

659

const existingMessage = "..."; // some SIWE message string

660

const parsed = new ParsedMessage(existingMessage);

661

662

// Convert parsed message back to SiweMessage for verification

663

const siweFromParsed = new SiweMessage({

664

scheme: parsed.scheme,

665

domain: parsed.domain,

666

address: parsed.address,

667

statement: parsed.statement,

668

uri: parsed.uri,

669

version: parsed.version,

670

chainId: parsed.chainId,

671

nonce: parsed.nonce,

672

issuedAt: parsed.issuedAt,

673

expirationTime: parsed.expirationTime,

674

notBefore: parsed.notBefore,

675

requestId: parsed.requestId,

676

resources: parsed.resources

677

});

678

```