Verifies that an incoming webhook request really came from Moov, and
parses its payload into a Moov.Webhook.Event.
Moov signs every webhook delivery with an HMAC-SHA512 of
timestamp <> "|" <> nonce <> "|" <> webhookID, keyed with the signing
secret shown in the Moov Dashboard for that webhook
(https://dashboard.moov.io/developers/webhooks), and sends the result
hex-encoded in the X-Signature header alongside the X-Timestamp,
X-Nonce, and X-Webhook-ID headers it was computed from. See
https://docs.moov.io/guides/webhooks/check-webhook-signatures/.
Usage with Plug/Phoenix
def webhook_controller(conn, _params) do
{:ok, raw_body, conn} = Plug.Conn.read_body(conn)
secret = Application.fetch_env!(:my_app, :moov_webhook_secret)
case Moov.Webhook.construct_event(raw_body, conn.req_headers, secret) do
{:ok, %Moov.Webhook.Event{type: "transfer.updated", data: data}} ->
MyApp.Transfers.handle_update(data)
send_resp(conn, 200, "")
{:ok, _event} ->
send_resp(conn, 200, "")
{:error, :invalid_signature} ->
send_resp(conn, 400, "invalid signature")
{:error, reason} ->
send_resp(conn, 400, "bad request: " <> inspect(reason))
end
endNote that for the signature to validate you must use the raw,
unparsed request body - if you're using Phoenix, make sure no JSON
parser has already consumed and re-serialized it (a custom
Plug.Parsers body reader that caches the raw body is the standard
fix).
Summary
Functions
Verifies the signature on a raw webhook request body and, if valid,
JSON-decodes it into a Moov.Webhook.Event.
Computes the expected hex-encoded HMAC-SHA512 signature for the given
header values and signing secret, without comparing it to anything. This
is the exact value Moov puts in X-Signature.
Returns the list of webhook event type identifiers documented by Moov as
of this package's release. This list is informational only - Moov may add
new event types at any time and construct_event/3 will still parse them
successfully (type is always returned as a plain string, never an atom).
The pure, lower-level signature check: given the four header values and
the signing secret, returns true if signature matches the expected
HMAC-SHA512, using a constant-time comparison.
Verifies the HMAC-SHA512 signature of a webhook delivery.
Functions
@spec construct_event(String.t(), Enumerable.t(), String.t()) :: {:ok, Moov.Webhook.Event.t()} | {:error, :missing_signature_headers | :invalid_signature | term()}
Verifies the signature on a raw webhook request body and, if valid,
JSON-decodes it into a Moov.Webhook.Event.
raw_body must be the exact, unmodified bytes Moov sent - decode it
after verifying, never before.
Computes the expected hex-encoded HMAC-SHA512 signature for the given
header values and signing secret, without comparing it to anything. This
is the exact value Moov puts in X-Signature.
@spec known_event_types() :: [String.t()]
Returns the list of webhook event type identifiers documented by Moov as
of this package's release. This list is informational only - Moov may add
new event types at any time and construct_event/3 will still parse them
successfully (type is always returned as a plain string, never an atom).
The pure, lower-level signature check: given the four header values and
the signing secret, returns true if signature matches the expected
HMAC-SHA512, using a constant-time comparison.
@spec verify_signature(Enumerable.t(), String.t()) :: :ok | {:error, :missing_signature_headers | :invalid_signature}
Verifies the HMAC-SHA512 signature of a webhook delivery.
headers may be a list of {key, value} tuples (as returned by
Plug.Conn.req_headers/1) or a map; header name lookups are
case-insensitive either way.
Returns :ok or {:error, :missing_signature_headers | :invalid_signature}.