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.
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
Sign-in message
example.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
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. Keccak-256 hashes the complete prefixed byte sequence into the signing digest. 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:
Condition
Compare against
Pre-login session
Server-bound challenge session
Nonce
Stored random challenge
Unused
Stored consumption flag
Unexpired
Server time before challenge expiry
One challenge → one session
Same nonce · same pre-login session
AValid signature
BValid signature
One-use nonceunused
Atomic consume
0 sessionsWaiting for a request
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.