Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Sign-In With Ethereum (SIWE)

Authenticate users with their signature. When users connect, your frontend knows their account, but your backend doesn't - SIWE bridges that gap by providing cryptographic proof of account ownership.

What you can do:
  • Wallet Login - Users sign in with their JAW account
  • Backend Auth - Verify signatures server-side
  • Secure Sessions - Issue JWTs or session cookies
  • Link Accounts - Associate wallets with existing user accounts

1. Integration Path

ApproachBest For
WagmiReact apps using wagmi connectors
Core SDKCustom implementations or non-React apps

2. Backend Endpoints

Set up three endpoints on your server to handle the SIWE flow:

EndpointPurpose
GET /api/siwe/nonceGenerate a fresh nonce to prevent replay attacks
POST /api/siwe/verifyValidate signature and issue session token
POST /api/siwe/logoutClear user session

Nonce Endpoint

// Server: GET /api/siwe/nonce
import { generateSiweNonce } from 'viem/siwe';
 
export async function GET() {
  const nonce = generateSiweNonce();
 
  // Store nonce in session/cache for verification
  // (e.g., Redis, memory cache, or database)
 
  return Response.json({ nonce });
}

Verify Endpoint

// Server: POST /api/siwe/verify
import { parseSiweMessage, verifySiweMessage } from 'viem/siwe';
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
 
const client = createPublicClient({
  chain: mainnet,
  transport: http(),
});
 
export async function POST(request: Request) {
  const { message, signature } = await request.json();
 
  // Parse the SIWE message
  const siweMessage = parseSiweMessage(message);
 
  // Verify the signature
  const isValid = await verifySiweMessage(client, {
    message,
    signature,
  });
 
  if (!isValid) {
    return Response.json({ error: 'Invalid signature' }, { status: 401 });
  }
 
  // Create session/JWT for the verified address
  const token = await createSessionToken(siweMessage.address);
 
  // Set HTTP-only cookie for security
  return new Response(JSON.stringify({ success: true }), {
    headers: {
      'Set-Cookie': `auth_token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/`,
    },
  });
}

Logout Endpoint

// Server: POST /api/siwe/logout
export async function POST() {
  return new Response(JSON.stringify({ success: true }), {
    headers: {
      'Set-Cookie': 'auth_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0',
    },
  });
}

3. Client-Side Sign-In

Request a SIWE signature during connection using the signInWithEthereum capability in wallet_connect.

Wagmi
import { useConnect } from '@jaw.id/wagmi';
 
function SignInButton() {
  const { mutate: connect } = useConnect();
 
  const handleSignIn = async () => {
    // 1. Fetch nonce from your backend
    const nonceRes = await fetch('/api/siwe/nonce');
    const { nonce } = await nonceRes.json();
 
    // 2. Connect with SIWE capability
    connect({
      connector: config.connectors[0],
      capabilities: {
        signInWithEthereum: {
          nonce,
          chainId: '0x1',
          domain: window.location.host,
          uri: window.location.origin,
          statement: 'Sign in to My App',
          expirationTime: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour
        },
      },
    }, {
      onSuccess: async (data) => {
        const siweResponse = data.accounts[0].capabilities?.signInWithEthereum;
 
        if (siweResponse && 'message' in siweResponse) {
          // 3. Send to backend for verification
          await fetch('/api/siwe/verify', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              message: siweResponse.message,
              signature: siweResponse.signature,
            }),
          });
        }
      },
    });
  };
 
  return <button onClick={handleSignIn}>Sign In</button>;
}

4. SIWE Parameters

ParameterRequiredDescription
nonceYesRandom string from your backend to prevent replay attacks
chainIdYesChain ID in hex format (e.g., '0x1' for mainnet)
domainNoYour app's domain (e.g., 'myapp.com')
uriNoYour app's full URI (e.g., 'https://myapp.com')
statementNoHuman-readable message the user is signing
expirationTimeNoISO 8601 timestamp when signature expires
issuedAtNoISO 8601 timestamp when signature was issued
resourcesNoArray of resource URIs the user is authorizing

5. Security Best Practices

  • Always verify signatures on your backend - Never trust client-side verification alone
  • Use nonces - Generate a fresh nonce for each sign-in attempt to prevent replay attacks
  • Set expiration times - Limit signature validity to a reasonable window (e.g., 1 hour)
  • Store sessions securely - Use HTTP-only cookies or encrypted tokens
  • Validate the domain - Ensure the signed domain matches your application

Related