Plugins

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.

auth.py
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

auth-client.ts
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():

OptionTypeDefaultDescription
domainstr"localhost"Domain embedded in and checked against the SIWE message.
anonymousboolTrueWhen False, a valid email is required on verify.
email_domain_namestrNoneDomain used to synthesize a placeholder email for new wallet users.
get_nonceasync () -> str17-char alphanumericCustom nonce generator.
verify_messageasync (args) -> booleth_account recoveryCustom signature verifier.
enable_ensboolFalseEnable ENS reverse-lookup (see below).
ens_rpc_urlstrNoneRPC endpoint for the built-in web3.py ENS resolver.
ens_resolverasync (address) -> str | NoneNoneFully custom ENS resolver.
ens_lookupasync ({walletAddress}) -> {name, avatar}NoneUpstream-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.

auth.py
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.