Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions docs/base-account/guides/authenticate-users.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -178,20 +188,33 @@ export async function verifySig(req, res) {
regardless of where it originated.
</Note>

<Warning>
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.
</Warning>

### Example Express Server

```ts title="server/auth.ts" expandable
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());

// Simple in-memory nonce store (swap for Redis or DB in production)
const nonces = new Set<string>();

// 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);
Expand All @@ -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
Expand Down