@awarizon/auth — Sign-In with Ethereum
Add wallet-based authentication to any app using EIP-4361 (SIWE). Users sign a human-readable message — no passwords, no OAuth. Verification works identically on client and server.
How SIWE Works
Three-step flow — nonce, sign, verify.
// ─── Step 1: Server generates a nonce ──────────────────────────────────────── // GET /api/auth/nonce const nonce = generateNonce() // random 32-char hex req.session.nonce = nonce res.send(nonce) // ─── Step 2: Client builds and signs the SIWE message ──────────────────────── import { AwarizonAuth } from "@awarizon/auth" const auth = new AwarizonAuth(awarizon, { domain: "myapp.com", uri: "https://myapp.com", statement: "Sign in to MyApp.", }) const nonce = await fetch("/api/auth/nonce").then(r => r.text()) const { message, signature } = await auth.signIn({ nonce }) // ─── Step 3: Server verifies the signature ──────────────────────────────────── // POST /api/auth/verify { message, signature } const session = await auth.verify({ message, signature, nonce: req.session.nonce, // must match step 1 }) // session.address is the cryptographically verified wallet address
SIWE is phishing-resistant. The message binds the domain, nonce, and expiry — so a signature obtained on one site cannot be replayed on another.
useSiwe — React hook
Drop-in SIWE flow for React / Next.js. Handles nonce fetching, wallet signing, and server verification in one signIn() call. Import from @awarizon/react.
domainstringREQYour app's hostname, e.g. "myapp.com". Embedded in every SIWE message.uristringREQFull URI of the resource, e.g. "https://myapp.com".statementstringHuman-readable prompt shown to the user inside their wallet.expiresInnumberSession validity in seconds. Default: 3600 (1 hour).getNonce() => Promise<string>Return a nonce before signing. Omit to generate one locally. Fetch server-side for replay protection.onVerify(msg, sig) => Promise<unknown>POST to your backend for server-side verification. Whatever you return is stored in session.serverData.onSignOut() => Promise<void>Called when signOut() is invoked. Use to clear the server-side session or cookie.isAuthenticatedbooleanTrue after a successful signIn().addressstring | nullVerified Ethereum address, or null if not signed in.sessionSiweSession | nullFull session: address, chainId, issuedAt, expiresAt, message, signature, serverData.isLoadingbooleanTrue during signIn / signOut.errorError | nullLast error. Cleared on the next signIn attempt.signIn() => Promise<void>Trigger the full SIWE flow: get nonce → sign → verify.signOut() => Promise<void>Clear the session and call onSignOut().import { useSiwe } from "@awarizon/react" function AuthButton() { const { isAuthenticated, address, signIn, signOut, isLoading, error } = useSiwe({ domain: "myapp.com", uri: "https://myapp.com", statement: "Sign in to access your account.", getNonce: () => fetch("/api/auth/nonce").then(r => r.text()), onVerify: async ({ message, signature }) => { const res = await fetch("/api/auth/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message, signature }), }) if (!res.ok) throw new Error("Server rejected the signature") return res.json() // stored as session.serverData }, onSignOut: () => fetch("/api/auth/signout", { method: "POST" }), }) if (isAuthenticated) { return <button onClick={signOut}>Sign out ({address?.slice(0,6)}…)</button> } return ( <> <button onClick={signIn} disabled={isLoading}> {isLoading ? "Signing…" : "Sign in with Ethereum"} </button> {error && <p style={{ color: "red" }}>{error.message}</p>} </> ) }
Server-side Verification
Use verifySiweSignature() on your server to confirm the wallet signed the message. Works in Node.js, Next.js, Express, Edge runtime, and Deno.
// ── Next.js App Router (route.ts) ──────────────────────────────────────────── import { verifySiweSignature } from "@awarizon/auth" import { cookies } from "next/headers" export async function POST(req: Request) { const { message, signature } = await req.json() const nonce = cookies().get("siwe_nonce")?.value if (!nonce) return Response.json({ error: "Missing nonce" }, { status: 400 }) const result = await verifySiweSignature({ message, signature, options: { domain: "myapp.com", nonce } }) // result.address is the cryptographically verified wallet address cookies().set("user_address", result.address, { httpOnly: true, secure: true }) cookies().delete("siwe_nonce") // invalidate — one-time use only return Response.json({ address: result.address }) } // ── Express ─────────────────────────────────────────────────────────────────── app.post("/api/auth/verify", async (req, res) => { const { message, signature } = req.body const result = await verifySiweSignature({ message, signature, options: { domain: "myapp.com", nonce: req.session.nonce }, }) req.session.address = result.address req.session.nonce = undefined // invalidate after use res.json({ address: result.address }) })
Always invalidate the nonce after a successful verification. Leaving it reusable allows the same signed message to authenticate multiple times (replay attack).
AwarizonAuth class
High-level class that wraps the full SIWE flow. Useful outside of React (Node scripts, CLI tools, Electron).
import { AwarizonAuth, SiweMessage, parseSiweMessage, verifySiweSignature, generateNonce } from "@awarizon/auth" const auth = new AwarizonAuth(awarizon, { domain: "myapp.com", uri: "https://myapp.com", statement: "Sign in to MyApp.", expiresIn: 3600, // seconds }) // Client: sign in const nonce = await fetch("/api/auth/nonce").then(r => r.text()) const { message, signature } = await auth.signIn({ nonce }) // Server: verify const session = await auth.verify({ message, signature, nonce }) // → session: { address, chainId, issuedAt, expiresAt, message, signature } auth.isValid(session) // → true if not expired auth.isExpired(session) // → true if past expiresAt // Low-level building blocks const nonce2 = generateNonce() // random 32-char hex const msg = new SiweMessage({ domain: "myapp.com", address: "0x...", uri: "https://myapp.com", chainId: 8453, nonce: nonce2, version: "1" }) const text = msg.prepare() const sig = await awarizon.signMessage(text) const fields = parseSiweMessage(text) // → { domain, address, uri, chainId, nonce, ... } const result = await verifySiweSignature({ message: text, signature: sig })