Connecting a wallet gives a website an address. The server still needs evidence that the person requesting a session controls that account. An address submitted in an HTTP request is only a claim.

Sign-In with Ethereum, or SIWE, turns that claim into a signed challenge. EIP-4361 standardizes the message so a wallet can recognize a login request and a server can verify its scope.

Challenge → signature → session
BrowserWalletServer
Generate nonce · bind session
Check origin + account
Verify → consume nonce

A recognizable request

Before a shared format, each application invented its own login text. Some included a domain and expiry; others asked users to sign an opaque nonce. A wallet could render the text, but could not reliably distinguish authentication from another authorization.

SIWE uses a strict text grammar. This example has a five-minute authentication window:

SIWE text → EIP-191 bytes
Message · exact UTF-8 textexample.com wants you to sign in with your Ethereum account: 0x1111111111111111111111111111111111111111 Sign in to your account. URI: https://example.com/login Version: 1 Chain ID: 1 Nonce: aB9cD2eF3gH4jK5m Issued At: 2026-09-29T08:00:00Z Expiration Time: 2026-09-29T08:05:00Z
Expected server values
domain
example.com
URI
https://example.com/login
chainId
1
nonce
aB9cD2eF3gH4jK5m
window
08:00–08:05 UTC
Version 1 · original line breaks preserved
\x19Ethereum Signed Message:\n278SIWE text · 278 B
Signing preimage · prefix + UTF-8 text0x19457468657265756d205369676e6564204d6573736167653a0a3237386578616d706c652e636f6d2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078313131313131313131313131313131313131313131313131313131313131313131313131313131310a0a5369676e20696e20746f20796f7572206163636f756e742e0a0a5552493a2068747470733a2f2f6578616d706c652e636f6d2f6c6f67696e0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a206142396344326546336748346a4b356d0a4973737565642041743a20323032362d30392d32395430383a30303a30305a0a45787069726174696f6e2054696d653a20323032362d30392d32395430383a30353a30305a
Keccak-256
0x08ea8ac6dc7ec3891c05468afb84484c88cd5de235f702e6ee2d3dedee8b7346

The lab colors the domain and URI purple, the account cyan, and the nonce rose. Switching the server-generated challenge changes the nonce in the exact message text and its signing digest; the two sample nonces have equal byte length. The complete EIP-191 preimage stays visible below the message.

The domain identifies the requesting authority, including a port when relevant. An optional scheme can precede it; without one, HTTPS is assumed. The URI identifies the subject of the authentication request. The address identifies the account, while chain ID identifies the session's chain context and where contract accounts are resolved.

The nonce is at least eight alphanumeric characters. This is a grammar minimum, not a recommendation to use a predictable counter. Generate a fresh challenge with a cryptographically secure random source and bind it to the browser's pre-login session.

Issued At is required. Expiration Time and Not Before are optional in the format; a service can require a short expiry as policy. All use RFC 3339 timestamps. Optional Request ID identifies a sign-in request, not necessarily a user. Optional Resources lists referenced URIs; their presence is not a blanket grant of spending permission.

The signature layer

For EOAs, SIWE uses the EIP-191 ⁠ personal-message envelope. The exact SIWE text is its message payload. SIWE adds a grammar and authentication semantics without inventing a new signing primitive.

EIP-712 ⁠ structures arbitrary typed messages. SIWE selects a recognizable plaintext format for login. Both can be implemented with readable wallet interfaces; the 2023 comparison of wallet display support is not a permanent distinction between the standards.

Verify the challenge

A valid signature is only one condition for login. The server must compare the parsed message with independently expected values.

  • Request context: expected domain, scheme policy, URI, and supported chain.
  • Challenge: nonce belongs to this pre-login session, remains unused, and has not expired.
  • Time: issued-at freshness, allowed clock skew, not-before, and expiry satisfy server policy.
  • Account: signature verification succeeds for the address claimed in the message.

The expected domain must come from trusted configuration, not from the submitted message. The expected nonce must come from stored challenge state, not from the body being verified.

Parsing alone establishes neither account control nor freshness. A helper named detectSIWE can select a wallet interface; it cannot authorize a server session.

A local round trip

This executable Node.js example uses siwe 3.0.0 and ethers 6.17.0. It creates a disposable EOA, signs a message, and checks the expected domain and nonce. It also confirms that a wrong nonce is rejected.

JS
import assert from 'node:assert/strict';
import { Wallet } from 'ethers';
import { SiweMessage, generateNonce } from 'siwe';
 
const wallet = Wallet.createRandom();
const nonce = generateNonce();
const now = new Date();
const domain = 'example.com';
const uri = 'https://example.com/login';
const message = new SiweMessage({
  domain,
  address: wallet.address,
  statement: 'Sign in to your account.',
  uri,
  version: '1',
  chainId: 1,
  nonce,
  issuedAt: now.toISOString(),
  expirationTime: new Date(now.getTime() + 300_000).toISOString(),
}).prepareMessage();
 
const signature = await wallet.signMessage(message);
const parsed = new SiweMessage(message);
assert.equal(parsed.uri, uri);
assert.equal(parsed.chainId, 1);
const result = await parsed.verify({
  signature, domain, nonce, time: now.toISOString(),
});
assert.equal(result.success, true);
assert.equal(result.data.address, wallet.address);
await assert.rejects(() => parsed.verify({
  signature, domain, nonce: nonce + 'x', time: now.toISOString(),
}));
console.log(result.data.address);

A browser application replaces the disposable wallet with the user's signer and sends the original message plus signature to its backend. This local example does not implement HTTP endpoints, challenge storage, or sessions.

The library checks signature and several message constraints, but it cannot know the service's URI policy, allowed chains, maximum challenge age, or whether a nonce has been consumed. Those checks remain application responsibilities.

Consume once

After validation, consume the challenge atomically. Two concurrent requests can both pass signature verification; only one may turn an unused challenge into a session.

The storage operation should combine the conditions and update:

ConditionCompare against
Pre-login sessionServer-bound challenge session
NonceStored random challenge
UnusedStored consumption flag
UnexpiredServer time before challenge expiry
One challenge → one session
same noncesame pre-login session
Request AReady · valid signature
Request BReady · valid signature
Atomic compare-and-set
unused = truesession: —

The two requests use the same challenge. The model assumes that signature, context, and time checks have passed. Submit either one first: it creates the session and consumes the challenge. The second is rejected. Both signature verifications may succeed; the atomic state transition can succeed only once.

A database transaction or equivalent atomic operation enforces this transition. A separate “read unused” followed later by “mark used” leaves a race. Prefer consuming the challenge and creating the authenticated session in one transaction when storage permits.

Rotate the session identifier after authentication. Bind the session to the verified address, and preserve the verified chain context. Use secure session cookies and protect authentication endpoints against CSRF; the signed challenge does not replace HTTP session security.

The message expiry bounds authentication with that message. Session lifetime and revocation are separate service policies. Disconnecting the browser wallet does not, by itself, revoke a session already created on the server.

The wallet's view

A wallet can parse SIWE and show a dedicated login interface. It must compare the claimed origin with the actual request origin obtained through a trusted channel. Merely displaying the domain supplied by the requesting page provides no such protection.

The displayed account should match the account being asked to sign. Parsed labels may be localized, but the bytes being signed must remain the original message. Translating the signed text changes the digest and can also break the grammar.

Wallet-side checks protect the signing interaction. Server-side checks protect session creation. Each side needs its own trusted view of the request.

Contract accounts

A contract wallet cannot always be verified by recovering an EOA address. SIWE recommends ERC-1271 for contract accounts, resolving the verifier on the message's chain.

That verification can depend on contract state. An owner change may alter whether the same signature remains valid. A service supporting contract accounts therefore needs a session policy for relevant state changes, as well as a provider for the correct chain. The EOA-only local example above does not cover that path.

References