X402.Extensions.SIWX.Server (X402 v0.9.0)

Copy Markdown View Source

Server-side Sign-In-With-X: challenge issuance, proof verification, and payer records.

Bundles the pieces a resource server needs to let a wallet that already paid skip payment: it builds the challenge advertised on 402 responses (challenge/1), verifies SIGN-IN-WITH-X proofs (verify/2), records which address paid for which resource after settlement (record_payment/4), and combines verification with that history (authenticate/3). X402.Plug.PaymentGate drives it through its :siwx option; use it directly from other frameworks.

{:ok, siwx} =
  X402.Extensions.SIWX.Server.new(
    domain: "api.example.com",
    uri: "https://api.example.com",
    supported_chains: [%{chain_id: "eip155:8453"}],
    nonce_cache: {X402.Extensions.PaymentIdentifier.ETSCache, MyApp.SIWXNonces}
  )

# on every 402
{:ok, challenge} = X402.Extensions.SIWX.Server.challenge(siwx)

# on a request carrying SIGN-IN-WITH-X
case X402.Extensions.SIWX.Server.authenticate(siwx, header, resource_url) do
  {:ok, %{address: address}} -> serve(conn)
  {:error, :not_authorized} -> respond_402(conn)
  {:error, code} -> respond_402(conn, error: code)
end

# after a successful settlement
:ok = X402.Extensions.SIWX.Server.record_payment(siwx, payer, resource_url, settle_response)

Nonces

Each challenge carries a fresh nonce. With a :nonce_cache (any X402.Extensions.PaymentIdentifier.Cache adapter) issued nonces are recorded and consumed on verification, so a proof authenticates exactly once and only against a challenge this server issued. Without a cache — the default — a proof is only bound by its issuedAt window (:max_age_seconds) and can be replayed within it; configure a cache in production.

Storage

Payer records live in an X402.Extensions.SIWX.Storage adapter keyed by {address, resource}; X402.Extensions.SIWX.ETSStorage (its default process) is used unless :storage is set. Pass {module, server} to address a specific storage process; the module must then expose the server-taking get/3, put/5, and delete/3 like ETSStorage does. EVM addresses are lowercased for storage and lookup; case-sensitive addresses such as Solana public keys are left unchanged. Verified identities retain the address spelling from the proof.

Summary

Types

A verified identity together with the payment record that authorizes it.

A storage module, or a module paired with the server it should address.

t()

Validated server configuration, built by new/1.

Functions

Verifies a proof and checks that its address has a payment record for resource.

Looks up the payment record for an address and resource.

Builds a fresh challenge and, with a :nonce_cache, records its nonce as issued.

Validates server options.

Validates server options, raising NimbleOptions.ValidationError on failure.

Records that address paid for resource, granting access for :ttl_ms.

Records a challenge nonce as issued in the :nonce_cache.

Revokes the payment record for an address and resource.

Verifies a SIGN-IN-WITH-X proof against the server configuration.

Types

session()

@type session() :: %{
  address: String.t(),
  chain_id: String.t(),
  fields: X402.Extensions.SIWX.fields(),
  access: X402.Extensions.SIWX.Storage.access_record()
}

A verified identity together with the payment record that authorizes it.

storage()

@type storage() :: module() | {module(), term()}

A storage module, or a module paired with the server it should address.

t()

@type t() :: %{
  domain: String.t(),
  uri: String.t(),
  supported_chains: [map()],
  statement: String.t() | nil,
  resources: [String.t()],
  expiration_seconds: pos_integer(),
  max_age_seconds: pos_integer(),
  clock_skew_seconds: non_neg_integer(),
  nonce_cache: X402.Extensions.PaymentIdentifier.Cache.adapter() | nil,
  storage: storage(),
  ttl_ms: pos_integer(),
  verifier: module(),
  ed25519_verifier: module()
}

Validated server configuration, built by new/1.

Functions

authenticate(config, proof, resource)

(since 0.9.0)
@spec authenticate(t(), X402.Extensions.SIWX.decoded() | String.t(), String.t()) ::
  {:ok, session()}
  | {:error, X402.Extensions.SIWX.verify_error() | :not_authorized}

Verifies a proof and checks that its address has a payment record for resource.

Returns the identity with the access record under :access, {:error, :not_authorized} when the proof is valid but no record exists (respond with a fresh challenge and payment requirements), or the verification error otherwise.

authorized(map, address, resource)

(since 0.9.0)
@spec authorized(t(), String.t(), String.t()) ::
  {:ok, X402.Extensions.SIWX.Storage.access_record()}
  | {:error, :not_authorized}

Looks up the payment record for an address and resource.

challenge(config)

(since 0.9.0)
@spec challenge(t()) :: {:ok, map()} | {:error, term()}

Builds a fresh challenge and, with a :nonce_cache, records its nonce as issued.

Returns the map to advertise under PaymentRequired.extensions["sign-in-with-x"]. Fails only when the nonce could not be recorded; a challenge whose nonce is unknown to the server would never verify, so it is better not to advertise one.

Examples

iex> siwx = X402.Extensions.SIWX.Server.new!(
...>   domain: "api.example.com",
...>   uri: "https://api.example.com",
...>   supported_chains: [%{chain_id: "eip155:8453"}]
...> )
iex> {:ok, challenge} = X402.Extensions.SIWX.Server.challenge(siwx)
iex> {challenge["info"]["domain"], challenge["supportedChains"]}
{"api.example.com", [%{"chainId" => "eip155:8453", "type" => "eip191"}]}

new(opts)

(since 0.9.0)
@spec new(keyword()) :: {:ok, t()} | {:error, NimbleOptions.ValidationError.t()}

Validates server options.

Options

Examples

iex> {:ok, siwx} = X402.Extensions.SIWX.Server.new(
...>   domain: "api.example.com",
...>   uri: "https://api.example.com",
...>   supported_chains: [%{chain_id: "eip155:8453"}]
...> )
iex> {siwx.storage, siwx.nonce_cache, siwx.ttl_ms}
{X402.Extensions.SIWX.ETSStorage, nil, 86_400_000}

iex> {:error, %NimbleOptions.ValidationError{}} =
...>   X402.Extensions.SIWX.Server.new(domain: "api.example.com")

new!(opts)

(since 0.9.0)
@spec new!(keyword()) :: t()

Validates server options, raising NimbleOptions.ValidationError on failure.

record_payment(map, address, resource, payment_proof)

(since 0.9.0)
@spec record_payment(t(), String.t(), String.t(), term()) :: :ok | {:error, term()}

Records that address paid for resource, granting access for :ttl_ms.

payment_proof is stored alongside the record (the facilitator's settle response, typically) and returned by authenticate/3 under access.payment_proof.

remember_nonce(map, nonce)

(since 0.9.0)
@spec remember_nonce(t(), String.t()) :: :ok | {:error, term()}

Records a challenge nonce as issued in the :nonce_cache.

A no-op (:ok) without a cache. challenge/1 calls this for you; use it when advertising a challenge built some other way.

revoke(map, address, resource)

(since 0.9.0)
@spec revoke(t(), String.t(), String.t()) :: :ok

Revokes the payment record for an address and resource.

verify(config, proof)

(since 0.9.0)

Verifies a SIGN-IN-WITH-X proof against the server configuration.

Accepts the raw header value or the tuple X402.Extensions.SIWX.decode_signed/1 returns; see X402.Extensions.SIWX.verify/2 for the checks and error codes. With a :nonce_cache the nonce is consumed on success.