Verifies the authenticity of incoming webhook requests from Increase.
Increase signs every webhook using the Standard
Webhooks specification: each request
carries webhook-id, webhook-timestamp, and webhook-signature
headers, and the signature is an HMAC-SHA256 of "#{id}.#{timestamp}.#{raw_body}"
using your endpoint's signing secret, Base64-encoded and prefixed with
v1,. See https://increase.com/documentation/webhooks#securing-your-webhook-endpoint
for the full specification.
You can find your endpoint's signing secret in the dashboard, on the Event Subscription you created.
Usage
Verify a webhook as early as possible in your endpoint, using the raw, unparsed request body -- not a body that's already been decoded into a map, since the signature is computed over the exact bytes Increase sent:
def webhook_controller_action(conn, _params) do
{:ok, raw_body, conn} = Plug.Conn.read_body(conn)
headers = %{
"webhook-id" => List.first(Plug.Conn.get_req_header(conn, "webhook-id")),
"webhook-timestamp" => List.first(Plug.Conn.get_req_header(conn, "webhook-timestamp")),
"webhook-signature" => List.first(Plug.Conn.get_req_header(conn, "webhook-signature"))
}
case Increase.Webhook.verify(raw_body, headers, signing_secret()) do
:ok ->
event = Jason.decode!(raw_body)
handle_event(event)
Plug.Conn.send_resp(conn, 200, "")
{:error, reason} ->
Plug.Conn.send_resp(conn, 400, "invalid signature: #{reason}")
end
endverify/3 returns :ok or {:error, reason} rather than raising, since
an invalid signature is an expected outcome (anyone can hit your
endpoint) rather than a programming error.
Summary
Functions
Computes the expected v1,<base64> signature for a webhook payload, the
same way Increase does. Exposed mainly for testing your own webhook
handler against fixtures; verify/4 is what you want for actually
verifying incoming requests.
@spec verify(String.t(), map(), String.t(), keyword()) :: :ok | {:error, :missing_headers | :invalid_timestamp | :timestamp_out_of_tolerance | :signature_mismatch}
Verifies a webhook request's signature and timestamp.
Arguments
raw_body- the exact, raw request body bytes Increase sent, as a string. Must not be re-encoded JSON or otherwise modified -- the signature covers the literal bytes received.headers- a map (or anything withAccessbehaviour, like aPlug.Conn's header list converted to a map) containing the"webhook-id","webhook-timestamp", and"webhook-signature"header values as strings. Header name lookups are case-sensitive; lowercase them yourself first if your web framework preserves original casing.signing_secret- your Event Subscription's signing secret, from the Increase dashboard.
Options
:tolerance- the maximum allowed age (in seconds, either direction) betweenwebhook-timestampand now, to protect against replay attacks. Defaults to 300 (5 minutes), matching Increase's own recommendation. Passfalseto disable timestamp checking entirely (not recommended).
Examples
iex> Increase.Webhook.verify(raw_body, headers, secret)
:ok
iex> Increase.Webhook.verify(tampered_body, headers, secret)
{:error, :signature_mismatch}