Sign In With Ethereum
Add ERC-4361 wallet sign-in, optional email collection, and wallet account settings.
The SIWE plugin adds an Ethereum wallet button to the sign-in form. It requests a nonce, builds an ERC-4361 message, signs it, and verifies it with Better Auth.
You can also add a wallet settings card. The card lists connected wallets, changes the primary wallet, and removes wallets.
Setup
Configure Better Auth
Add siwe() to the server. Supply a secure nonce generator and an ERC-4361 message verifier.
import { betterAuth } from "better-auth"
import { siwe } from "better-auth/plugins"
import { verifyMessage } from "viem"
import { generateSiweNonce } from "viem/siwe"
export const auth = betterAuth({
plugins: [
siwe({
domain: "app.example.com",
getNonce: async () => generateSiweNonce(),
verifyMessage: async ({ message, signature, address }) =>
verifyMessage({
address: address as `0x${string}`,
message,
signature: signature as `0x${string}`
})
})
]
})Apply the SIWE schema before using the plugin. See the Better Auth SIWE guide.
Add the client plugin
import { createAuthClient } from "better-auth/react"
import { siweClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({ plugins: [siweClient()] })Register the UI plugin
The included connector uses an injected EIP-1193 wallet. You can provide another connector for Wagmi or another wallet library.
import { createEip1193WalletConnector } from "@better-auth-ui/core/plugins/siwe"
import { AuthProvider } from "@better-auth-ui/heroui"
import { siwePlugin } from "@better-auth-ui/heroui/plugins/siwe"
<AuthProvider
authClient={authClient}
navigate={navigate}
plugins={[
siwePlugin({
connector: createEip1193WalletConnector(),
domain: "app.example.com",
uri: "https://app.example.com",
email: "optional",
statement: "Sign in to Example"
})
]}
>
{children}
</AuthProvider>Wallet settings
Better Auth does not expose browser endpoints for listing or changing SIWE wallets. Add authenticated server routes, then connect them with SiweWalletManager.
Authorize on the server
Never trust a user ID from the browser. Resolve the user from the server session for every wallet operation.
import type {
SiweWalletAccount,
SiweWalletLinkChallenge,
SiweWalletManager
} from "@better-auth-ui/core/plugins/siwe"
const assertOk = async (response: Response) => {
if (!response.ok) {
throw new Error(`Wallet request failed with status ${response.status}.`)
}
}
const get = async <TResponse>(
url: string,
signal?: AbortSignal
): Promise<TResponse> => {
const response = await fetch(url, { signal })
await assertOk(response)
return response.json() as Promise<TResponse>
}
const post = async <TResponse = void>(
url: string,
body: unknown
): Promise<TResponse> => {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
})
await assertOk(response)
if (response.status === 204) return undefined as TResponse
return response.json() as Promise<TResponse>
}
export const walletManager: SiweWalletManager = {
list: (signal) => get<SiweWalletAccount[]>("/api/wallets", signal),
createLinkChallenge: (wallet) =>
post<SiweWalletLinkChallenge>("/api/wallets/link-challenge", wallet),
link: (proof) => post("/api/wallets/link", proof),
unlink: (walletId) => fetch(`/api/wallets/${walletId}`, { method: "DELETE" }).then(assertOk),
setPrimary: (walletId) => fetch(`/api/wallets/${walletId}/primary`, { method: "POST" }).then(assertOk)
}Pass walletManager to siwePlugin(). BAUI then adds connect, list, primary, and remove controls to security settings.
The link challenge must contain a single-use nonce. Verify its domain, chain, address, nonce, and signature before attaching the wallet.
The server must delete a wallet's walletAddress row and matching SIWE account in one transaction.
Email modes
"optional"asks for an email but allows an empty value. This is the default."required"requires an email before the wallet opens."none"opens the wallet immediately.
The signature proves control of the wallet. It does not verify the supplied email address.
Last updated on