SIWE
Sign in with Ethereum using EIP-4361 nonce and signature verification.
The SIWE plugin implements Sign-In with Ethereum. Kernia issues a chain-scoped nonce, verifies the EIP-4361 signed message, consumes the nonce, and signs the user in — auto-creating a wallet-backed account when needed. ENS reverse-lookup is optional.
Installation
Add the plugin to your server
Pass siwe() to your Kernia config. Set domain to the host your frontend
serves from; it is embedded in the signed message and checked on verify.
import os
from kernia import KerniaOptions
from kernia.auth import init
from kernia.plugins.siwe import siwe
from .db import adapter
auth = init(KerniaOptions(
database=adapter,
secret=os.environ["KERNIA_SECRET"],
base_url=os.environ["KERNIA_BASE_URL"],
plugins=[siwe(domain="example.com")],
))Add the client plugin
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: "/api/auth",
plugins: [siweClient()],
});Usage
Request a nonce
Ask Kernia for a nonce for the connected wallet, then build the SIWE message and have the wallet sign it.
const { data } = await authClient.siwe.nonce({
walletAddress: address,
chainId: 1,
});
const nonce = data?.nonce;Verify the signature
Send the signed message back. Kernia validates the domain, nonce, chain id, and signature, then creates the session.
await authClient.siwe.verify({
message, // the EIP-4361 message string the wallet signed
signature, // 0x-prefixed signature
walletAddress: address,
chainId: 1,
});If you configure anonymous=False on the server, pass an email to verify.
Options
Pass these to siwe():
| Option | Type | Default | Description |
|---|---|---|---|
domain | str | "localhost" | Domain embedded in and checked against the SIWE message. |
anonymous | bool | True | When False, a valid email is required on verify. |
email_domain_name | str | None | Domain used to synthesize a placeholder email for new wallet users. |
get_nonce | async () -> str | 17-char alphanumeric | Custom nonce generator. |
verify_message | async (args) -> bool | eth_account recovery | Custom signature verifier. |
enable_ens | bool | False | Enable ENS reverse-lookup (see below). |
ens_rpc_url | str | None | RPC endpoint for the built-in web3.py ENS resolver. |
ens_resolver | async (address) -> str | None | None | Fully custom ENS resolver. |
ens_lookup | async ({walletAddress}) -> {name, avatar} | None | Upstream-shaped ENS lookup. |
ENS reverse-lookup
ENS resolution is opt-in. Pass either ens_rpc_url (uses the built-in web3.py
resolver against your endpoint) or a fully custom ens_resolver, with
enable_ens=True. Lookups are best-effort — a network failure never blocks
sign-in.
from kernia.plugins.siwe import siwe
siwe(
enable_ens=True,
# Either: built-in web3.py resolver
ens_rpc_url="https://mainnet.infura.io/v3/<key>",
# OR: custom resolver. Async callable (address: str) -> str | None.
# ens_resolver=my_resolver,
)The built-in resolver does a forward-resolve confirmation — the ENS name's
address record must resolve back to the same wallet — so a squatted reverse
record is never accepted. ENS is enrichment, not identity proof: the wallet
address remains the durable identifier.
Schema
Adds a walletAddress table (wallet id, user id, address, chain id, primary
flag, created-at) and extends the core user table with two optional fields:
walletAddress— checksummed EIP-55 address (unique), set on first verify.ensName— last resolved ENS name when ENS is enabled, refreshed on each sign-in so a stale name doesn't survive a transfer.
Nonces live in the core verification table, are single-use, and expire after
15 minutes by default.