docs/content/docs/plugins/siwe.mdx
The Sign in with Ethereum (SIWE) plugin allows users to authenticate using their Ethereum wallets following the ERC-4361 standard. This plugin provides flexibility by allowing you to implement your own message verification and nonce generation logic.
Add the SIWE plugin to your auth configuration:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { siwe } from "better-auth/plugins"; // [!code highlight]
export const auth = betterAuth({
plugins: [
siwe({
domain: "example.com",
emailDomainName: "example.com", // optional
anonymous: false, // optional, default is true
getNonce: async () => {
// Implement your nonce generation logic here
return "your-secure-random-nonce";
},
verifyMessage: async (args) => {
// Implement your SIWE message verification logic here
// This should verify the signature against the message
return true; // return true if signature is valid
},
ensLookup: async (args) => {
// Optional: Implement ENS lookup for user names and avatars
return {
name: "user.eth",
avatar: "https://example.com/avatar.png"
};
},
}),
],
});
```
Run the migration or generate the schema to add the necessary fields and tables to the database.
<Tabs items={["migrate", "generate"]}>
<Tab value="migrate">
```package-install
npx auth migrate
```
</Tab>
<Tab value="generate">
```package-install
npx auth generate
```
</Tab>
</Tabs>
See the [Schema](#schema) section to add the fields manually.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins"; // [!code highlight]
export const authClient = createAuthClient({
plugins: [
siweClient() // [!code highlight]
]
});
```
Before signing a SIWE message, you need to generate a nonce for the wallet address:
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.nonce({
walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
chainId: 1, // optional for Ethereum mainnet, required for other chains. Defaults to 1
});
if (data) {
console.log("Nonce:", data.nonce);
}
After generating a nonce and creating a SIWE message, verify the signature to authenticate:
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.verify({
message: "Your SIWE message string",
signature: "0x...", // The signature from the user's wallet
walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
chainId: 1, // optional for Ethereum mainnet, required for other chains. Must match Chain ID in SIWE message
email: "[email protected]", // optional, required if anonymous is false
});
if (data) {
console.log("Authentication successful:", data.user);
}
Here are examples for different blockchain networks:
// Ethereum Mainnet (chainId can be omitted, defaults to 1)
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
// chainId: 1 (default)
});
// Polygon (chainId REQUIRED)
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 137, // Required for Polygon
});
// Arbitrum (chainId REQUIRED)
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 42161, // Required for Arbitrum
});
// Base (chainId REQUIRED)
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 8453, // Required for Base
});
The SIWE plugin accepts the following configuration options:
truePromise<string>verifyMessage) and return Promise<boolean> — the plugin independently validates the message's nonce, domain, address, Chain ID, and time bounds before creating a sessionThe SIWE client plugin doesn't require any configuration options, but you can pass them if needed for future extensibility:
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
siweClient({
// Optional client configuration can go here
}),
],
});
The SIWE plugin adds a walletAddress table to store user wallet associations:
| Field | Type | Description |
|---|---|---|
| id | string | Primary key |
| userId | string | Reference to user.id |
| address | string | Ethereum wallet address |
| chainId | number | Chain ID (e.g., 1 for Ethereum mainnet) |
| isPrimary | boolean | Whether this is the user's primary wallet |
| createdAt | date | Creation timestamp |
Here's a complete example showing how to implement SIWE authentication:
import { betterAuth } from "better-auth";
import { siwe } from "better-auth/plugins";
import { generateRandomString } from "better-auth/crypto";
import { verifyMessage, createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
export const auth = betterAuth({
database: {
// your database configuration
},
plugins: [
siwe({
domain: "myapp.com",
emailDomainName: "myapp.com",
anonymous: false,
getNonce: async () => {
// Generate a cryptographically secure random nonce
return generateRandomString(32, "a-z", "A-Z", "0-9");
},
verifyMessage: async ({ message, signature, address }) => {
try {
// Verify the signature using viem (recommended)
const isValid = await verifyMessage({
address: address as `0x${string}`,
message,
signature: signature as `0x${string}`,
});
return isValid;
} catch (error) {
console.error("SIWE verification failed:", error);
return false;
}
},
ensLookup: async ({ walletAddress }) => {
try {
// Optional: lookup ENS name and avatar using viem
// You can use viem's ENS utilities here
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
const ensName = await client.getEnsName({
address: walletAddress as `0x${string}`,
});
const ensAvatar = ensName
? await client.getEnsAvatar({
name: ensName,
})
: null;
return {
name: ensName || walletAddress,
avatar: ensAvatar || "",
};
} catch {
return {
name: walletAddress,
avatar: "",
};
}
},
}),
],
});