Lettermint.Webhook (Lettermint v2.1.0)

Copy Markdown View Source

Verifies Lettermint webhook deliveries.

A delivery is genuine when its HMAC-SHA256 signature over "<t>." <> raw body, keyed with the endpoint's signing secret (whsec_..., used as is), matches, and its timestamp is within the tolerance:

webhook = Lettermint.Webhook.new(System.fetch_env!("LETTERMINT_WEBHOOK_SECRET"))

case Lettermint.Webhook.verify(webhook, raw_body, conn.req_headers) do
  {:ok, %Lettermint.WebhookPayload{event: event, data: data}} -> handle(event, data)
  {:error, %Lettermint.WebhookVerificationError{}} -> send_resp(conn, 400, "")
end

Pass the raw request body: the signature covers the exact bytes, so decoding and re-encoding the JSON breaks it. In Phoenix or Plug, keep the raw body with a :body_reader (see the README).

inspect/1 and Jason.encode/1 show only the tolerance, never the secret.

Summary

Types

Request headers: a map (any case; a value or a list of values), a list of {name, value} (such as conn.req_headers), or a Plug.Conn.

t()

Functions

Creates a verifier for a signing secret.

Verifies a delivery from its raw body and request headers, and returns the decoded payload.

Verifies the raw body against an X-Lettermint-Signature value, for setups where the headers are not at hand.

Types

headers()

@type headers() :: map() | [{String.t() | atom(), String.t()}] | struct()

Request headers: a map (any case; a value or a list of values), a list of {name, value} (such as conn.req_headers), or a Plug.Conn.

t()

@type t() :: %Lettermint.Webhook{
  secret: (-> String.t()),
  tolerance: non_neg_integer()
}

Functions

new(secret, options \\ [])

@spec new(String.t(), [{:tolerance, non_neg_integer()}]) :: t()

Creates a verifier for a signing secret.

Options: :tolerance, the maximum difference between the signed timestamp and now, in seconds, in either direction (default 300; 0 accepts only the current second). An empty secret or a negative tolerance raises Lettermint.ConfigError.

verify(webhook, raw_body, headers, options \\ [])

@spec verify(t(), binary(), headers(), [{:now, integer()}]) ::
  {:ok, Lettermint.WebhookPayload.t()}
  | {:error, Lettermint.WebhookVerificationError.t()}

Verifies a delivery from its raw body and request headers, and returns the decoded payload.

Requires X-Lettermint-Signature (t=<unix>,v1=<hex>; any matching v1 is accepted) and X-Lettermint-Delivery, which must equal the signed timestamp. Header names are case-insensitive.

Options: :now, the current time in Unix seconds (for tests).

verify_signature(webhook, raw_body, signature_header, options \\ [])

@spec verify_signature(t(), binary(), String.t(),
  timestamp: String.t() | integer(),
  now: integer()
) ::
  {:ok, Lettermint.WebhookPayload.t()}
  | {:error, Lettermint.WebhookVerificationError.t()}

Verifies the raw body against an X-Lettermint-Signature value, for setups where the headers are not at hand.

Options: :timestamp, the X-Lettermint-Delivery value, which must equal the signed timestamp when given; :now, the current time in Unix seconds.