Ethers.Siwe (Ethers v0.7.0)

Copy Markdown View Source

Sign-In with Ethereum (EIP-4361) messages.

SIWE is the standard way for a wallet to authenticate to an off-chain service: the backend issues a nonce, the wallet signs a structured plaintext message containing it, and the backend parses, validates and verifies the returned message and signature.

This module covers the message lifecycle:

  • new/1 / new!/1 - build an Ethers.Siwe.Message
  • generate_nonce/0 - generate a cryptographically random nonce
  • to_message/1 - render the EIP-4361 string for the wallet to sign
  • parse/1 - parse a message string received from a client
  • validate/2 - stateless validation (validity window, domain/nonce/address binding)
  • verify/3 - the one-call backend flow: parse, validate and check the signature (including ERC-1271/ERC-6492 smart-contract wallets)

See the Sign-In with Ethereum guide for a complete Phoenix integration recipe.

Example

iex> {:ok, message} =
...>   Ethers.Siwe.new(
...>     domain: "example.com",
...>     address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
...>     statement: "Sign in to Example",
...>     uri: "https://example.com/login",
...>     chain_id: 1,
...>     nonce: "32891756",
...>     issued_at: "2021-09-30T16:25:24.000Z"
...>   )
iex> Ethers.Siwe.to_message(message)
"example.com wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nSign in to Example\n\nURI: https://example.com/login\nVersion: 1\nChain ID: 1\nNonce: 32891756\nIssued At: 2021-09-30T16:25:24.000Z"

Summary

Functions

Generates a cryptographically random alphanumeric nonce with at least 8 characters.

Builds a new Ethers.Siwe.Message from a keyword list or map.

Same as new/1 but raises ArgumentError on error.

Parses an EIP-4361 message string into an Ethers.Siwe.Message.

Renders the message as the EIP-4361 string the wallet will sign.

Statelessly validates a message against the current time and the expected binding values.

Verifies a SIWE message end-to-end: parse (when given a string), validate the fields and check the signature.

Functions

generate_nonce()

@spec generate_nonce() :: String.t()

Generates a cryptographically random alphanumeric nonce with at least 8 characters.

Uses 128 bits of entropy from :crypto.strong_rand_bytes/1 encoded in base 36.

new(params)

@spec new(Keyword.t() | map()) :: {:ok, Ethers.Siwe.Message.t()} | {:error, atom()}

Builds a new Ethers.Siwe.Message from a keyword list or map.

Accepts atom keys (:chain_id) or string keys ("chain_id"). Required fields are :domain, :address, :uri, :chain_id and :nonce. :version defaults to "1" (the only defined version) and :issued_at defaults to the current UTC time.

The address is normalized to its EIP-55 checksummed form. Timestamp fields accept either DateTime structs or RFC 3339 strings (kept verbatim).

Returns {:error, reason} with a descriptive atom (e.g. :missing_domain, :invalid_nonce) when a field is missing or invalid.

Examples

iex> {:ok, message} =
...>   Ethers.Siwe.new(
...>     domain: "example.com",
...>     address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
...>     uri: "https://example.com",
...>     chain_id: 1,
...>     nonce: Ethers.Siwe.generate_nonce()
...>   )
iex> message.address
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
iex> message.version
"1"

iex> Ethers.Siwe.new(domain: "example.com")
{:error, :missing_address}

new!(params)

@spec new!(Keyword.t() | map()) :: Ethers.Siwe.Message.t() | no_return()

Same as new/1 but raises ArgumentError on error.

parse(message)

@spec parse(String.t()) :: {:ok, Ethers.Siwe.Message.t()} | {:error, atom()}

Parses an EIP-4361 message string into an Ethers.Siwe.Message.

Parsing is strict: field order, the EIP-55 checksummed address, the nonce format, RFC 3339 timestamps and RFC 3986 domain/URIs are all enforced. A successfully parsed message re-renders byte-for-byte identical via to_message/1.

Examples

iex> raw =
...>   "example.com wants you to sign in with your Ethereum account:\n" <>
...>     "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\n" <>
...>     "URI: https://example.com\nVersion: 1\nChain ID: 1\n" <>
...>     "Nonce: 32891756\nIssued At: 2021-09-30T16:25:24.000Z"
iex> {:ok, message} = Ethers.Siwe.parse(raw)
iex> {message.domain, message.chain_id, message.statement}
{"example.com", 1, nil}

to_message(message)

@spec to_message(Ethers.Siwe.Message.t()) :: String.t()

Renders the message as the EIP-4361 string the wallet will sign.

The String.Chars protocol is also implemented for Ethers.Siwe.Message, so to_string/1 and string interpolation work as well.

validate(message, opts \\ [])

@spec validate(Ethers.Siwe.Message.t(), Keyword.t()) :: :ok | {:error, atom()}

Statelessly validates a message against the current time and the expected binding values.

This performs no signature verification and no RPC requests - it only checks the message fields themselves.

Options

  • :time - the DateTime to check the validity window against. Defaults to DateTime.utc_now/0. The message is invalid at and after expiration_time (exclusive upper bound) and before not_before (inclusive lower bound).
  • :domain - the domain this backend expects (exact match).
  • :scheme - the scheme this backend expects (exact match).
  • :nonce - the nonce this backend issued for the session (exact match).
  • :address - the expected signing address (case-insensitive comparison).

Omitted options are not checked.

Returns :ok or {:error, reason} where reason is one of :invalid_version, :expired, :not_yet_valid, :invalid_expiration_time, :invalid_not_before, :domain_mismatch, :scheme_mismatch, :nonce_mismatch, :address_mismatch or :invalid_address.

verify(message, signature, opts \\ [])

@spec verify(String.t() | Ethers.Siwe.Message.t(), binary(), Keyword.t()) ::
  {:ok, Ethers.Siwe.Message.t()} | {:error, term()}

Verifies a SIWE message end-to-end: parse (when given a string), validate the fields and check the signature.

This is the one-call backend flow. The signature check uses Ethers.Signature.verify_message/4, so EOA signatures verify locally without any RPC round-trip while smart-contract wallets (ERC-1271, deployed or not — ERC-6492) are verified with a single eth_call.

Parameters

  • message: The raw EIP-4361 message string received from the client, or an already-parsed Ethers.Siwe.Message. When given a string, the signature is verified over that exact string.
  • signature: The signature as a 0x-prefixed hex string or raw binary. ERC-6492-wrapped signatures are supported.
  • opts: Options.

Options

All of validate/2's options (:time, :domain, :scheme, :nonce, :address) plus the RPC options forwarded to Ethers.Signature.verify_hash/4 for smart-wallet verification: :rpc_client, :rpc_opts and :block.

Returns

  • {:ok, message} with the parsed/validated Ethers.Siwe.Message on success — trust message.address afterwards.
  • {:error, :invalid_signature} if the signature does not verify for message.address.
  • {:error, reason} for parse errors, validate/2 errors or RPC transport failures.