TypeScript library implementing the Sign-In with Ethereum (EIP-4361) specification for decentralized authentication
npx @tessl/cli install tessl/npm-siwe@3.0.00
# 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
```