Skip to content

Sign-In With Ethereum (SIWE)

TODO:
- Switch example over to sign via Secp256k1 instead of JSON-RPC
- Outline JSON-RPC approach for signing
- Outline ERC-6492 verify

Overview

Sign-In with Ethereum (SIWE) is a standard described by EIP-4361 for how Ethereum accounts authenticate with off-chain services by signing a standard message format. This guide details how to perform SIWE with Ox in a client-server architecture.

Generate Nonce

Using Siwe.generateNonce, you can generate a random nonce that can be used to prevent replay attacks.

Server
import { Siwe } from 'ox'
 
function handler() {
  const nonce = Siwe.generateNonce()
  return nonce
}

Your server should generate a new nonce for each SIWE process it performs. The nonce should be stored for later use (e.g. in the session or database) to validate the signature.

Create SIWE Message

Before you can create a SIWE message, you need to source the following information:

  • address: The Ethereum address performing the signing.
  • chainId: The EIP-155 Chain ID to which the session is bound,
  • domain: RFC 3986 authority that is requesting the signing.
  • uri: RFC 3986 URI referring to the resource that is the subject of the signing (as in the subject of a claim).
  • version: The current version of the SIWE Message.
Client
function onClick() {
  const data = {
    address: '<connected address>', // e.g. Wagmi `useAccount()`/`getAccount()`
    chainId: 1,
    domain: window.location.host,
    nonce: '<fetched from server>', // e.g. `await getNonceFromServer()`
    uri: window.location.origin,
    version: '1',
  } as const
}

Once you have the information, you can create the SIWE message with Siwe.createMessage.

Client
import { Siwe } from 'ox'
 
function onClick() {
  const data = {
    address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
    chainId: 1,
    domain: 'example.com',
    nonce: '65ed4681d4efe0270b923ff5f4b097b1c95974dc33aeebecd5724c42fd86dfd25dc70b27ef836b2aa22e68f19ebcccc1',
    uri: 'https://example.com/path',
    version: '1',
  } as const satisfies Siwe.Message
  const message = Siwe.createMessage(data)
}

In addition, to the required fields, there are some optional fields you can include:

  • expirationTime: Time when the signed authentication message is no longer valid.
  • issuedAt: Time when the message was generated, typically the current time.
  • notBefore: Time when the signed authentication message will become valid.
  • requestId: A system-specific identifier that may be used to uniquely refer to the sign-in request.
  • resources: A list of information or references to information the user wishes to have resolved as part of authentication by the relying party.
  • scheme: RFC 3986 URI scheme of the origin of the request.
  • statement: A human-readable ASCII assertion that the user will sign.

Sign SIWE Message

Once the message is created, the next step is to sign it. In this guide, we will assume a browser wallet will sign over JSON-RPC, but you could also sign with Secp256k1.sign if you have access to the private key directly.

Client
import { Hex, Provider, Siwe } from 'ox'
 
async function onClick() {
  const data = {
    address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
    chainId: 1,
    domain: 'example.com',
    nonce: '65ed4681d4efe0270b923ff5f4b097b1c95974dc33aeebecd5724c42fd86dfd25dc70b27ef836b2aa22e68f19ebcccc1',
    uri: 'https://example.com/path',
    version: '1',
  } as const satisfies Siwe.Message
  const message = Siwe.createMessage(data)
  const provider = Provider.from(window.ethereum)
  const signature = await provider.request({  
    method: 'personal_sign',  
    params: [Hex.fromString(message), data.address],  
  })
}

Validate Signature

Now that we have our message and signature, we can validate them to confirm the address did indeed sign the message. This validation must be performed in such a way that it cannot be tampered with. In our case, we will use the server.

Server
import { Address, Hash, Hex, PersonalMessage, Secp256k1, Siwe } from 'ox'
 
function handler() {
  const payload = {
    //  ↑ Sent by client
    message: 'example.com wants you to sign in with your Ethereum account: ...',
    signature: '0x...',
  } as const
 
  // Parse message string into structured object
  const parsed = Siwe.parseMessage(payload.message)
  if (!parsed.address) return false
 
  // Validate message fields (e.g. `now >= expirationTime`), nonce, etc.
  const isValid = Siwe.validateMessage({
    message: parsed,
    nonce: '<nonce from session, database, etc.>',
  })
  if (!isValid) return false
 
  const personalMessage = PersonalMessage.encode(Hex.fromString(payload.message))
  const hash = Hash.keccak256(personalMessage)
  const verified = Address.isEqual(
    Address.from(parsed.address),
    Secp256k1.recoverAddress({ hash, signature: payload.signature }),
Type 'string' is not assignable to type '{ r: bigint; s: bigint; yParity: number; }'.
) return verified }

Related Modules

ModuleDescription
SiweUtility functions for working with Sign-In with Ethereum (EIP-4361).