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.
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:
example.com wants you to sign in with your Ethereum account:
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:00Z0x19457468657265756d205369676e6564204d6573736167653a0a3237380x08ea8ac6dc7ec3891c05468afb84484c88cd5de235f702e6ee2d3dedee8b7346The 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.
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.
A valid signature is only one condition for login. The server must compare the parsed message with independently expected values.
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.
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.
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.
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 |
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.
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.
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.