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