Phoenix
There is no Elixir package yet; the check is a few lines with :crypto. The one thing
Phoenix needs is the raw body, which Plug.Parsers consumes, so keep a copy for the
webhook route.
Keep the raw body
Section titled “Keep the raw body”defmodule MyAppWeb.CacheBodyReader do @moduledoc "Keeps the raw body of webhook requests for signature checks."
def read_body(%Plug.Conn{request_path: "/webhooks"} = conn, opts) do with {:ok, body, conn} <- Plug.Conn.read_body(conn, opts) do {:ok, body, update_in(conn.assigns[:raw_body], &[body | &1 || []])} end end
def read_body(conn, opts), do: Plug.Conn.read_body(conn, opts)endplug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], body_reader: {MyAppWeb.CacheBodyReader, :read_body, []}, json_decoder: Phoenix.json_library()Verify in the controller
Section titled “Verify in the controller”defmodule MyAppWeb.WebhookController do use MyAppWeb, :controller
def create(conn, params) do raw_body = conn.assigns |> Map.get(:raw_body, []) |> Enum.reverse() |> IO.iodata_to_binary() secret = Application.fetch_env!(:my_app, :ricochet_webhook_secret)
if verify(raw_body, conn, secret) do [id] = get_req_header(conn, "webhook-id") # Skip `id` if it was already handled; then handle `params`. send_resp(conn, 200, "") else send_resp(conn, 401, "") end end
defp verify(raw_body, conn, "whsec_" <> secret) do key = Base.decode64!(secret)
with [id] <- get_req_header(conn, "webhook-id"), [timestamp] <- get_req_header(conn, "webhook-timestamp"), [signatures] <- get_req_header(conn, "webhook-signature"), {seconds, ""} <- Integer.parse(timestamp), true <- abs(System.system_time(:second) - seconds) <= 300 do expected = Base.encode64(:crypto.mac(:hmac, :sha256, key, [id, ".", timestamp, ".", raw_body]))
signatures |> String.split(" ") |> Enum.any?(fn "v1," <> received -> Plug.Crypto.secure_compare(received, expected) _ -> false end) else _ -> false end endendscope "/", MyAppWeb do pipe_through :api post "/webhooks", WebhookController, :createendTry it locally
Section titled “Try it locally”mix phx.serverricochet listen --forward localhost:4000/webhooksDeliveries through the CLI carry no webhook-signature (a CLI destination has no
signing secret), so turn the check off in local development, for example with an
environment variable. Never skip it just because the header is missing: that would
accept unsigned requests in production too.