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 anEthers.Siwe.Messagegenerate_nonce/0- generate a cryptographically random nonceto_message/1- render the EIP-4361 string for the wallet to signparse/1- parse a message string received from a clientvalidate/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
@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.
@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}
@spec new!(Keyword.t() | map()) :: Ethers.Siwe.Message.t() | no_return()
Same as new/1 but raises ArgumentError on error.
@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}
@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.
@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- theDateTimeto check the validity window against. Defaults toDateTime.utc_now/0. The message is invalid at and afterexpiration_time(exclusive upper bound) and beforenot_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.
@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-parsedEthers.Siwe.Message. When given a string, the signature is verified over that exact string.signature: The signature as a0x-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/validatedEthers.Siwe.Messageon success — trustmessage.addressafterwards.{:error, :invalid_signature}if the signature does not verify formessage.address.{:error, reason}for parse errors,validate/2errors or RPC transport failures.