Packages

Elixir client library for the Paymob payment gateway (Egypt, UAE, KSA, Oman)

Current section

Files

Jump to
ex_paymob lib ex_paymob webhook.ex
Raw

lib/ex_paymob/webhook.ex

defmodule ExPaymob.Webhook do
@moduledoc """
Webhook HMAC-SHA512 verification and parsing for Paymob callbacks.
Paymob sends transaction callbacks with an HMAC signature in the query parameter `hmac`.
The signature is computed by concatenating 20 specific fields (no separator) and
computing HMAC-SHA512 with your HMAC secret key.
## HMAC Fields (in order)
1. `amount_cents`
2. `created_at`
3. `currency`
4. `error_occured`
5. `has_parent_transaction`
6. `id`
7. `integration_id`
8. `is_3d_secure`
9. `is_auth`
10. `is_capture`
11. `is_refunded`
12. `is_standalone_payment`
13. `is_voided`
14. `order.id`
15. `owner`
16. `pending`
17. `source_data.pan`
18. `source_data.sub_type`
19. `source_data.type`
20. `success`
"""
@hmac_fields [
{:amount_cents, ["obj", "amount_cents"]},
{:created_at, ["obj", "created_at"]},
{:currency, ["obj", "currency"]},
{:error_occured, ["obj", "error_occured"]},
{:has_parent_transaction, ["obj", "has_parent_transaction"]},
{:id, ["obj", "id"]},
{:integration_id, ["obj", "integration_id"]},
{:is_3d_secure, ["obj", "is_3d_secure"]},
{:is_auth, ["obj", "is_auth"]},
{:is_capture, ["obj", "is_capture"]},
{:is_refunded, ["obj", "is_refunded"]},
{:is_standalone_payment, ["obj", "is_standalone_payment"]},
{:is_voided, ["obj", "is_voided"]},
{:order_id, ["obj", "order", "id"]},
{:owner, ["obj", "owner"]},
{:pending, ["obj", "pending"]},
{:source_data_pan, ["obj", "source_data", "pan"]},
{:source_data_sub_type, ["obj", "source_data", "sub_type"]},
{:source_data_type, ["obj", "source_data", "type"]},
{:success, ["obj", "success"]}
]
@doc """
Verifies the HMAC signature of a webhook payload.
Uses timing-safe comparison via `Plug.Crypto.secure_compare/2`.
## Parameters
* `payload` - The decoded JSON payload from the webhook body
* `received_hmac` - The HMAC from the `hmac` query parameter
* `opts` - Options, can include `hmac_secret` override
"""
@spec verify_hmac(map(), String.t(), keyword()) :: :ok | {:error, :invalid_hmac}
def verify_hmac(payload, received_hmac, opts \\ []) do
computed = compute_hmac(payload, opts)
if Plug.Crypto.secure_compare(computed, received_hmac) do
:ok
else
{:error, :invalid_hmac}
end
end
@doc """
Computes the HMAC-SHA512 hex digest for a webhook payload.
"""
@spec compute_hmac(map(), keyword()) :: String.t()
def compute_hmac(payload, opts \\ []) do
hmac_secret = ExPaymob.Config.resolve(:hmac_secret, opts)
message = build_hmac_message(payload)
:crypto.mac(:hmac, :sha512, hmac_secret, message)
|> Base.encode16(case: :lower)
end
@doc """
Verifies HMAC and parses the webhook payload in one step.
Returns the transaction object on success.
"""
@spec verify_and_parse(map(), String.t(), keyword()) ::
{:ok, map()} | {:error, :invalid_hmac}
def verify_and_parse(payload, received_hmac, opts \\ []) do
case verify_hmac(payload, received_hmac, opts) do
:ok -> {:ok, payload["obj"]}
error -> error
end
end
@doc """
Verifies the HMAC for subscription webhook events.
Subscription HMACs use the format: `"{trigger_type}for{subscription_id}"`
"""
@spec verify_subscription_hmac(map(), String.t(), keyword()) ::
:ok | {:error, :invalid_hmac}
def verify_subscription_hmac(payload, received_hmac, opts \\ []) do
hmac_secret = ExPaymob.Config.resolve(:hmac_secret, opts)
trigger_type = payload["type"] || ""
subscription_id = get_in(payload, ["obj", "id"]) || ""
message = "#{trigger_type}for#{subscription_id}"
computed =
:crypto.mac(:hmac, :sha512, hmac_secret, message)
|> Base.encode16(case: :lower)
if Plug.Crypto.secure_compare(computed, received_hmac) do
:ok
else
{:error, :invalid_hmac}
end
end
defp build_hmac_message(payload) do
@hmac_fields
|> Enum.map(fn {_name, path} -> extract_field(payload, path) end)
|> Enum.join()
end
defp extract_field(payload, path) do
value = get_in(payload, path)
to_hmac_string(value)
end
defp to_hmac_string(true), do: "true"
defp to_hmac_string(false), do: "false"
defp to_hmac_string(nil), do: ""
defp to_hmac_string(value) when is_integer(value), do: Integer.to_string(value)
defp to_hmac_string(value) when is_float(value), do: Float.to_string(value)
defp to_hmac_string(value) when is_binary(value), do: value
defp to_hmac_string(value), do: to_string(value)
end