From 32bddbac201a24d566b1a607999e7ac38ff30148 Mon Sep 17 00:00:00 2001 From: Dusk1e Date: Tue, 11 Aug 2026 02:47:41 +0300 Subject: [PATCH] fix(base-account): parse the SIWE nonce correctly and bind verification to the app domain The "Authenticate users" guide ships a server example that cannot authenticate anyone, and a verification step that accepts signatures minted for other sites. Nonce extraction used `/at (\w{32})$/`. In an EIP-4361 message the nonce sits on a `Nonce: ` line with `Issued At:` after it, so the regex never matches and `/auth/verify` answers 400 "Invalid or reused nonce" for every valid login. Two other pages in this repo already read the nonce correctly, so the guide was the odd one out. Switching to viem's `parseSiweMessage` removes the regex entirely. Verification called `client.verifyMessage`, which only checks that the signature matches the message. It does not look at the `domain` field, so a signature a user produced on another site verifies against this endpoint too. EIP-4361 requires the relying party to check `domain`. `verifySiweMessage` checks domain, nonce and expiry, and still routes through `verifyHash`, so ERC-6492 and ERC-1271 signatures from undeployed Base Accounts keep working. Verified against a local node with real signatures: a legitimate login returns 200, a replayed signature 400, a signature minted for another domain 401, and an unissued nonce 400. Before the change the first case returned 400 and the third returned 200. Fixes #1502 --- .../guides/authenticate-users.mdx | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/base-account/guides/authenticate-users.mdx b/docs/base-account/guides/authenticate-users.mdx index 7a8332200..65ce02e6d 100644 --- a/docs/base-account/guides/authenticate-users.mdx +++ b/docs/base-account/guides/authenticate-users.mdx @@ -147,12 +147,22 @@ try { ```ts Backend (Viem) import { createPublicClient, http } from 'viem'; import { base } from 'viem/chains'; +import { verifySiweMessage } from 'viem/siwe'; + +// The domain your app is served from. Anything signed for another +// domain must not be accepted here. +const APP_DOMAIN = 'yourapp.com'; const client = createPublicClient({ chain: base, transport: http() }); export async function verifySig(req, res) { const { address, message, signature } = req.body; - const valid = await client.verifyMessage({ address, message, signature }); + const valid = await verifySiweMessage(client, { + address, + message, + signature, + domain: APP_DOMAIN, + }); if (!valid) return res.status(401).json({ error: 'Invalid signature' }); // create session / JWT res.json({ ok: true }); @@ -178,6 +188,14 @@ export async function verifySig(req, res) { regardless of where it originated. + + Always check the `domain` field of the signed message against your own domain. + A signature is valid for whichever domain it was signed for, so a signature a + user produced on another site is cryptographically valid on yours too. + `verifySiweMessage` performs this check when you pass `domain`; a bare + `verifyMessage` call does not, and accepts the signature. + + ### Example Express Server ```ts title="server/auth.ts" expandable @@ -185,6 +203,7 @@ import crypto from "crypto"; import express from "express"; import { createPublicClient, http } from "viem"; import { base } from "viem/chains"; +import { parseSiweMessage, verifySiweMessage } from "viem/siwe"; const app = express(); app.use(express.json()); @@ -192,6 +211,10 @@ app.use(express.json()); // Simple in-memory nonce store (swap for Redis or DB in production) const nonces = new Set(); +// The domain your app is served from. Anything signed for another +// domain must not be accepted here. +const APP_DOMAIN = "yourapp.com"; + app.get("/auth/nonce", (_, res) => { const nonce = crypto.randomBytes(16).toString("hex"); nonces.add(nonce); @@ -203,14 +226,19 @@ const client = createPublicClient({ chain: base, transport: http() }); app.post("/auth/verify", async (req, res) => { const { address, message, signature } = req.body; - // 1. Check nonce hasn\'t been reused - const nonce = message.match(/at (\w{32})$/)?.[1]; + // 1. Check this server issued the nonce and hasn't seen it before + const { nonce } = parseSiweMessage(message); if (!nonce || !nonces.delete(nonce)) { return res.status(400).json({ error: "Invalid or reused nonce" }); } - // 2. Verify signature - const valid = await client.verifyMessage({ address, message, signature }); + // 2. Verify the signature and bind it to your domain + const valid = await verifySiweMessage(client, { + address, + message, + signature, + domain: APP_DOMAIN, + }); if (!valid) return res.status(401).json({ error: "Invalid signature" }); // 3. Create session / JWT here