ConvergerClient.Webhook (converger_client v0.1.1)

Copy Markdown View Source

Verification of Converger outbound webhook signatures.

Every webhook request carries

x-converger-signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>

In a Phoenix or Plug app, read the raw body before JSON parsing (for example with a body_reader that keeps it in conn.assigns) and call:

case ConvergerClient.Webhook.verify_signature(conn.req_headers, raw_body, secret) do
  :ok -> handle(Jason.decode!(raw_body))
  {:error, reason} -> send_resp(conn, 401, Atom.to_string(reason))
end

The rules and the test vectors are in packages/conformance/webhook-signatures.json.

Summary

Functions

The header that carries the signature.

Builds an x-converger-signature value (useful for tests).

Types

reason()

@type reason() ::
  :missing_signature
  | :no_secret
  | :malformed_signature
  | :timestamp_out_of_tolerance
  | :invalid_signature

Functions

header()

The header that carries the signature.

sign(secret, body, timestamp \\ System.system_time(:second))

@spec sign(String.t(), iodata(), integer()) :: String.t()

Builds an x-converger-signature value (useful for tests).

valid_signature?(headers, body, secret, opts \\ [])

@spec valid_signature?(Enumerable.t(), iodata(), String.t() | [String.t()], keyword()) ::
  boolean()

Like verify_signature/4, but returns a boolean.

verify_signature(headers, body, secret, opts \\ [])

@spec verify_signature(Enumerable.t(), iodata(), String.t() | [String.t()], keyword()) ::
  :ok | {:error, reason()}

Verifies a webhook request.

headers is a list of {name, value} (as in conn.req_headers) or a map; names are matched case-insensitively. body is the raw body. secret is the channel secret, or a list of secrets while rotating.

Options: :tolerance (seconds, default 300), :now (unix seconds, for tests).