SIWEを安全に実装する:nonce・domain・URI・chain IDとsession設計
Sign-In with Ethereumを署名確認だけで終わらせず、ERC-4361 field、single-use nonce、EOA・ERC-1271、cookie、CSRF、logoutまでlocal再現で検証します。

SIWE(Sign-In with Ethereum)を安全に実装する中心は、署名の真偽だけでなく「このsiteが、今、1回だけ発行したlogin challengeか」を検証することです。ERC-4361 messageを厳密に解析し、serverが固定したscheme、domain、URI、chain ID、時刻と照合し、nonceを1回だけ消費してからEOAまたはERC-1271署名を確認します。
成功後も終わりではありません。sessionをaddressへ結び付け、cookie、CSRF、logout、walletのaddress / chain変更、contract accountのstate変更まで設計して、ようやくweb authenticationの一連の流れになります。
SIWEはmessage署名からweb sessionを作る規則
ERC-4361は、Ethereum accountでoff-chain serviceへ認証し、sessionを確立するための標準です。EOAでは人が読めるmessageをERC-191形式で署名し、serverはmessageとsignatureの両方を受け取ります。
ここで確認する対象は2層あります。
| 層 | 確認するもの | 確認しないまま成功にすると起きること |
|---|---|---|
| challenge policy | 誰が、何のURIで、どのchainへ、いつ、どのnonceを発行したか | 別site・別request・過去signatureの使い回し |
| signer proof | messageに書かれたaddressが本当に署名したか | 別key・別contract stateのsignatureを受理 |
signatureがvalidでも、challenge policyが違えばloginは失敗です。反対に、fieldが正しくてもsignatureが違えばsessionを発行しません。
ERC-4361 fieldを役割ごとに読む
ERC-4361のABNFは、fieldの名前だけでなく改行、順序、文字種を定義します。独自のsplit("\n")で部分的に読むのではなく、準拠parserで全体を解析してからapplicationの期待値を比較します。
| field | 必須 | 意味 | serverでの扱い |
|---|---|---|---|
scheme |
optional | request originのscheme | 未指定時の既定はHTTPS。公開環境では期待schemeを固定する |
domain |
required | 署名を要求するauthority。hostと任意portを含む | 設定済みdomainと完全一致させる |
address |
required | 認証するEthereum address | signatureの期待signer、sessionのbinding先にする |
statement |
optional | 人が読む署名目的 | 改行を入れず、認証以上の同意を曖昧に混ぜない |
URI |
required | signing subjectとなるresource | login endpointなど、serverで許可したURIと一致させる |
version |
required | ERC-4361 version | 現行は正確に1 |
chain ID |
required | sessionを結ぶchain。contract accountを解決するchain | allowlistした値と、そのchain専用clientへ対応させる |
nonce |
required | replayを防ぐchallenge | 8文字以上の英数字。高entropy、短期限、1回限りにする |
issued-at |
required | message発行時刻 | server clockと許容skewを決める |
expiration-time |
optional | message失効時刻 | この実装では必須policyにして5分後へ固定する |
not-before |
optional | 使用開始時刻 | 未来なら開始前として拒否する |
request-id |
optional | application固有request ID | logやtrace用途を決め、権限として推測しない |
resources |
optional | 関連URI一覧 | 解釈はERC-4361の範囲外。列挙だけで認可しない |
domainとURIは同じ文字列を重複して置くfieldではありません。前者は署名要求元のauthority、後者は署名対象resourceです。chain IDも「現在walletで選ばれているchainを何となく記録する値」ではなく、特にERC-1271 contractをどのchainで解決するかを決めます。
expected originをHost headerから作らない
expected valueは、受信requestから自由に組み立てずserver設定へ置きます。たとえば公開URLがhttps://login.exampleなら、scheme=https、domain=login.example、URI=https://login.example/auth/siweをdeployment configへ固定します。
reverse proxyの背後でも、HostやX-Forwarded-Hostを無条件に信用しません。公開sampleのresolveRequestOrigin()は次の順で扱います。
- 通常はcanonical originを使う
- forwarded headerがあれば、接続元peerがtrusted proxy allowlist内か確認する
protoとhostは単一値だけを受け取る- 復元したoriginが最終allowlist内かもう一度確認する
これにより、攻撃者がHost: evil.exampleを送り、その値をSIWE messageへ反映させる経路を閉じます。wallet側のorigin表示・検証と、server側のexpected-field検証は両方必要です。
nonceは生成・期限・消費を一つのstateとして扱う
ERC-4361はnonceを8文字以上の英数字と定義しますが、具体的な生成・保存方法までは決めません。公開sampleではNode.js crypto.randomBytes(16)の128-bit値をhexへ変換し、保存時はSHA-256 hashをkeyにして5分のTTLを持たせます。
重要なのは「照合してから後で削除」ではなく、未使用から使用済みへの変更を1回の原子的操作にすることです。
take(nonce, now) {
const key = sha256(nonce);
const record = records.get(key);
if (!record) throw new AuthError('unknown-or-used-nonce');
records.delete(key); // async signature verificationより前に消費
if (now > record.expiresAt) throw new AuthError('expired-nonce');
return record;
}
これは1つのNode.js process内を説明するsampleです。複数instanceで動かす場合は、shared databaseで条件付きDELETEやcompare-and-setを1 transactionとして実行します。先にSELECTして、署名検証後に別requestでDELETEすると、同時に届いた2件が両方成功する時間差が残ります。
signatureがinvalidだった場合もnonceは復活させません。攻撃者が失敗を繰り返せるchallengeを残すより、clientへ新しいchallengeの取得を求める方がstateは単純です。
検証順は解析、field、nonce、signature、session
backendは次の順序を固定します。
- message sizeを制限し、ERC-4361 ABNFとして全体を解析する
scheme、domain、URI、version、chain IDをserver policyと比較するissued-atのclock skew、expiration-time、not-beforeをserver clockで確認する- nonceを期限内・未使用から使用済みへ原子的に移す
- parsed addressを期待signerとしてEOAまたはERC-1271 signatureを検証する
- freshなsession IDを作り、address、chain ID、signer typeへ結び付ける
fieldと時刻は署名検証より安いので先に落とせます。ただしnonce consumeは非同期のRPC / signature処理より前です。同じnonceの並行requestが来ても、2件目はunknown-or-used-nonceになり、sessionを2つ作れません。
EOAとERC-1271を同じ入口で検証する
EOAはERC-191 message hashからsignerをrecoverします。deployed contract accountは、messageのchain IDに対応するclientでcodeを読み、ERC-1271のisValidSignature結果を確認します。
公開sampleはviem 2.56.0のpublicClient.verifyMessageを使います。
const code = (await publicClient.getCode({ address })) ?? '0x';
const signerType = code === '0x' ? 'eoa' : 'erc1271';
const valid = await publicClient.verifyMessage({
address,
message: originalSiweMessage,
signature,
});
verifyMessageへ到達する前に、applicationがchain clientとexpected fieldsを決めていることが前提です。「libraryがtrueを返したから、どのdomain・URI・nonceでもよい」にはなりません。
ERC-1271の署名検証で説明した通り、contract signatureは現在stateに依存します。owner、threshold、module、pause状態が変われば、同じmessageとsignatureでも結果が変わり得ます。
SIWEはEIP-712のdomain separatorへ置き換える形式ではなく、現行ERC-4361はERC-191の人が読めるmessageを使います。また、この記事のlocal再現はdeployed ERC-1271までです。未デプロイsmart accountを扱う場合は、ERC-6492 wrapperとtrusted factory policyを別途reviewします。
local再現で成功系と20の拒否理由を固定した
公開再現データは、Node.js 24.x、npm 11.9.0、siwe 3.0.0、ethers peer 6.17.0、viem 2.56.0、Foundry / Anvil 1.8.0、Solidity 0.8.36、OpenZeppelin Contracts 5.6.1、local chain ID 31337へ固定しています。public RPC、実在wallet、個人key、署名prompt、実資産は使っていません。
| success case | result | 確認したstate |
|---|---|---|
| EOA ERC-191 | true |
parsed addressとsession addressが一致 |
| deployed ERC-1271 | true |
local chain上のcontract accountで検証 |
| 同一nonceの並行2要求 | 成功1、拒否1 | atomic consumeでsession成功を1件に限定 |
| trusted proxy経由 | true |
peerと最終originの両allowlistを通過 |
| Origin + CSRF | true |
address-bound sessionでstate changeを許可 |
| logout | revoked | server sessionを削除 |
| address / chain change | revoked | wallet contextとsession bindingの不一致 |
| ERC-1271 owner削除後 | signature false |
matching contract sessionを1件失効 |
ERC-1271 contract自体のFoundry 14 testsも通し、EOA、1-of-1、2-of-3、insufficient threshold、wrong owner、wrong digest、wrong magic、revert、期限、owner / pause state changeを分けています。
失敗は「何となくlogin失敗」ではなく、次の理由へ分類しました。
| category | local再現で拒否したcase | session |
|---|---|---|
| format / origin fields | malformed ABNF、wrong version、scheme、domain、URI | 発行しない |
| chain | wrong chain ID、policyとclientのchain不一致 | 発行しない |
| nonce | unknown、expired、replay、bad signature後のreuse | 発行しない |
| time | expired message、future issued-at、not-before前 | 発行しない |
| signer | wrong EOA signer、wrong ERC-1271 owner | 発行しない |
| proxy | untrusted forwarded host、trusted proxyからのdisallowed origin | 発行しない |
| session action | wrong Origin、wrong CSRF token | state changeを実行しない |
wrong versionは現行ABNF自体に合わないため、parser段階のmalformed-messageとして落ちます。signatureを確かめる前にrejectして構いません。
session cookieにはaddressやsignatureを入れない
signatureやaddressをそのままbearer cookieにせず、認証成功ごとに256-bitのopaque session IDを新しく作ります。server側はIDのhashをkeyに、少なくとも次を保存します。
- checksum済みaddress
- chain ID
- signer type(EOA / ERC-1271)
- created / expires timestamp
- session-bound CSRF tokenのhash
公開sampleのcookieは次の形です。
Set-Cookie: __Host-siwe=<opaque>; Path=/; Max-Age=28800; Secure; HttpOnly; SameSite=Lax
__Host- prefixを使うため、SecureとPath=/を付け、Domain属性は付けません。HttpOnlyはJavaScriptからsession IDを読めなくし、SameSite=Laxはcross-site送信を減らします。ただし、SameSiteだけをCSRF対策の全部にはしません。
state-changing requestはOriginとCSRF tokenを確認する
cookieはbrowserが自動送信します。そのため、profile変更やlogoutなどstate-changing endpointでは、sessionが有効なだけでなく次も確認します。
- HTTP methodを
POSTなどの変更系へ限定する Originがcanonical originと一致する- request bodyまたはcustom headerのCSRF tokenが、sessionに結び付くhashと一致する
- server-side authorizationをresourceごとに実行する
CSRF token自体をsession cookieと同じHttpOnly cookieだけに置くと、request側から別値として提示できません。login responseから安全にclient stateへ渡す、専用endpointで取得するなど、frameworkの推奨patternに合わせます。tokenをURLへ置くとreferrerやlogへ残り得るため避けます。
XSSがあると同一originからCSRF tokenを使われる可能性があります。output escaping、Content Security Policy、dependency管理などのXSS対策は別の必須layerです。
logoutとaccount変更でserver sessionを失効する
logoutはwallet接続を切るだけではありません。POST /logoutでOriginとCSRFを確認し、server recordを削除してcookieを期限切れにします。
frontendがwalletのaccountsChangedまたはchainChangedを受け取った場合も、画面表示だけを変えず、現在sessionを失効させて再認証します。server sessionがaddress Aへbindingされているのに、wallet表示だけaddress Bへ変わる状態を残しません。
ERC-1271ではさらに、owner、threshold、moduleなどsignature validityへ影響するstate changeを検知したとき、matching sessionを失効するpolicyが必要です。webhook、event監視、重要操作前の再検証、短いsession TTLを組み合わせます。local再現ではownerを削除し、同じsignatureがtrue → falseになったことを確認してcontract accountのsessionを削除しました。
SIWE authenticationとauthorizationを分離する
ERC-4361の範囲はauthenticationです。SIWE成功から分かるのは、期待したchallengeに対してaddressのsigner proofが通り、web sessionを作れることです。
次の判断は別です。
- 管理画面を使えるroleか
- private documentを読めるmembershipか
- tokenやNFTを現在保有しているか
- transactionを送ってよいか
- allowance、asset移動、message公開へ同意したか
これらをresourcesやstatementに書いただけでserver authorizationへ変換しません。role database、resource ownership、policy engine、step-up authenticationなどをresourceごとに判定します。特にSIWE login signatureを、token approvalや資産移動への包括同意として再利用してはいけません。
利用者がwallet prompt上でSIWEをPermit、一般EIP-712、EIP-7702 delegationと分け、domain・URI・chain ID・nonceを確認する入口は、wallet署名要求の見分け方と9項目チェックリストにまとめています。
walletのdisconnectとserver logoutを同じ操作だと考えず、token allowance、Permit2、smart account module、EIP-7702 delegationまで残存権限を確認する場合は、DApp切断後の六つの権限層を参照してください。
実装前のチェックリスト
- ERC-4361 ABNF全体を準拠parserで読み、field sizeも制限した
- scheme、domain、URI、version、chain IDをserver-fixed policyと比較した
- request Hostやforwarded headerをexpected valueへ無条件に採用していない
- trusted proxy peerと最終allowed originを両方確認した
- nonceはcryptographic random、短いTTL、hash保存、1回限りにした
- 同時requestでもnonceの成功を1件にするtransactional consumeを実装した
- issued-at skew、expiration-time、not-beforeをserver clockで確認した
- EOA ERC-191と、指定chain上のdeployed ERC-1271を検証した
- signature failure後に同じnonceを復活させていない
- session IDを新規random値にし、address / chain / signer typeへbindingした
-
__Host-、Secure、HttpOnly、Path、SameSite、TTLをreviewした - state-changing requestでOriginとsession-bound CSRF tokenを確認した
- logout、session expiry、address / chain changeでserver sessionを失効した
- ERC-1271 state change時の再検証・失効方法を決めた
- authenticationとresource authorization、transaction / asset consentを分離した
- rate limit、TLS、logのsecret除外、XSS対策、shared storeの障害挙動をreviewした
- local再現結果を全wallet・provider・chain対応やsecurity auditの証拠にしていない
SIWE実装の完了条件は「signature libraryがtrueを返した」ではありません。期待originとrequestを固定し、single-use challengeを先に消費し、signerの現在状態を確認して、失効できるaddress-bound sessionへ移すことです。
この記事にスポンサー、affiliate、wallet接続、署名要求、transaction、資産移動のCTAはありません。将来、wallet、auth provider、RPC、session service、audit serviceの広告を置く場合も広告であることを明示し、field policy、nonce state、signature verification、session / CSRF、failure matrix、security reviewから独立させます。
確認した一次情報
- ERC-4361 Sign-In with Ethereum確認日: 2026/08/30
- ERC-191 Signed Data Standard確認日: 2026/08/30
- ERC-1271 Standard Signature Validation Method for Contracts確認日: 2026/08/30
- Sign-In with Ethereum TypeScript library確認日: 2026/08/30
- SIWE backend quickstart確認日: 2026/08/30
- SIWE security considerations確認日: 2026/08/30
- viem publicClient.verifyMessage確認日: 2026/08/30
- OWASP Session Management Cheat Sheet確認日: 2026/08/30
- OWASP CSRF Prevention Cheat Sheet確認日: 2026/08/30
- MDN Using HTTP cookies確認日: 2026/08/30
- Node.js crypto.randomBytes確認日: 2026/08/30



