Sign In With Ethereum
Install ERC-4361 wallet sign-in and wallet account settings.
The SIWE registry item adds wallet sign-in, optional email collection, and a security settings card for connected wallets.
Setup
Configure Better Auth
Add siwe() to the server. Provide a secure nonce generator and an ERC-4361 verifier. Apply the plugin schema before sign-in.
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}`
})
})
]
})See the Better Auth SIWE guide for the complete server setup.
Add the client plugin
import { createAuthClient } from "better-auth/react"
import { siweClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({ plugins: [siweClient()] })Install the registry item
npx shadcn@latest add @better-auth-ui/siweRegister the UI plugin
import { createEip1193WalletConnector } from "@better-auth-ui/core/plugins/siwe"
import { siwePlugin } from "@/lib/auth/siwe-plugin"
<AuthProvider
authClient={authClient}
navigate={navigate}
plugins={[
siwePlugin({
connector: createEip1193WalletConnector(),
domain: "app.example.com",
uri: "https://app.example.com",
email: "optional"
})
]}
>
{children}
</AuthProvider>Wallet settings
Better Auth does not expose browser endpoints for SIWE wallet management. Create authenticated server routes and connect them through SiweWalletManager.
Authorize on the server
Resolve the user from the server session. Do not accept a user ID from the browser for wallet operations.
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(). The registry item then adds connect, list, primary, and remove controls.
The link challenge must use a single-use nonce. Verify its domain, chain, address, nonce, and signature before attaching the wallet.
Use email: "required" to require an email. Use email: "none" to open the wallet without an email dialog.
The wallet signature does not verify the supplied email address.
Last updated on