Awarizon
DOCS/@awarizon/auth — Sign-In with Ethereum

@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.

INSTALL
$npm install @awarizon/auth

How SIWE Works

Three-step flow — nonce, sign, verify.

TS
// ─── 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.

OPTIONS
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.
RETURNS
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().
TSX
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).

TS
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 })