wfgilman

wfgilman

How to: Plug which verifies header signature signed using request body

I’m writing up this quick “How to” because what I thought was going to be an easy implementation of a Plug to validate a webhook request turned out to be more complicated than I expected. Hopefully this can save others some time. This is for a JSON API.

Prior discussions here, and here. Recommended approach here by Jose from these discussions.

Goal: Validate a request signature which is a SHA256 HMAC of the request body signed by your secret key.

Challenge: The request body can only be read once from the conn, and it is done by Plug.Parsers before the request even reaches the router.

Solution: Normally, I add a webhook endpoint to my router, and add a controller which does something with the webhook, like store it in a queue for later processing. I can put the webhook route inside a pipeline, or add a Plug to my controller which verifies the header before processing the request.

However, I need the request body (as a string) to sign and compare to the signature in the header. It can only be accessed in this way using Plug.Conn.read_body/2. Unfortunately, that function is called by Plug.Parsers before the request hits the router.

Therefore, I need to create a Plug which is added to the endpoint of my app, before Plug.Parsers, here in endpoint.ex:

...
plug Plug.RequestId
plug Plug.Logger

# --> Add my Plug here <--
plug Plug.Parsers,
  parsers: [:urlencoded, :multipart, :json],
  pass: ["*/*"],
  json_decoder: Poison

plug Plug.MethodOverride
plug Plug.Head
...

Also, the Plug has to actually handle the request and then halt the conn. Once the request body is read, the subsequent plug, Plug.Parsers will fail. This means I can’t use a controller. It also means every request to the endpoint will go through my Plug; so I need a way of only taking action on request to the endpoint I care about.

I used a module plug for this. I copied the format from the recommended approach linked above (credit to https://github.com/hamiltop).

init/1 and call/2 are simple. init/1 just passed along the options with no modifications. I actually need to do modifications here, but I punt to a private function after I know whether the request is to the endpoint I care about. Otherwise every request will go through those modification - unnecessary overhead.

def init(opts), do: opts

call/2 checks the request path. It just returns the connection to continue one if it doesn’t match, otherwise it performs the verification and ultimately halts it.

def call(conn, opts) do
  mount = Keyword.get(opts, :mount)
  case conn.request_path do
    ^mount ->
      verify_signature(conn, opts)
    _ ->
      conn
  end
end

verify_signature/2 does the work of verifying the header signature. If correct, it will handle the request. If not, it will return a 401 error. handle_request/2 here is the equivalent of create/2 if I were using a Phoenix.Controller. (All my webhooks come in as POST requests.)

defp verify_signature(conn, opts) do
  opts = prepare_options(opts) # Normally my init/1 function.
  with [request_signature|_] <- get_req_header(conn, opts[:header]),
       secret when not is_nil(secret) <- opts[:secret],
       {:ok, body, _} <- Plug.Conn.read_body(conn),
       signature = Plug.Crypto.MessageVerifier.sign(body, secret, :sha256),
       true <- Plug.Crypto.secure_compare(signature, request_signature) do
    handle_webhook(conn, Poison.decode!(body))
  else
    nil ->
      Logger.error(fn -> "Webhook secret is not set" end)
      halt send_resp(conn, 401, "")
    false ->
      Logger.error(fn -> "Received webhook with invalid signature" end)
      halt send_resp(conn, 401, "")
    _ ->
      halt send_resp(conn, 401, "")
  end
end

Important: Note how each response includes halt/1. This ensures the conn won’t proceed to the next plug and foul things up (since the request body has already been read). Whatever you implement for handle_webhook/2, it needs to also included halt/1 with its response.

defp handle_webhook(conn, webhook) do
  event_params = get_params(webhook)
  with {:error, changeset} <- Event.store(event_params),
       true <- is_nil(changeset.errors[:resource_topic]) do
    Logger.warn(fn -> "Failed to store event: #{inspect changeset}" end)
  end
  halt send_resp(conn, 200, "")
end

Finally, my Plug is placed in endpoint.ex at the location noted above.

MyApp.Plug.Webhook, mount: "/v1/webhooks", header: "X-Request-Signature-Sha-256", secret: "s3cret"

Where Next?

Popular in Guides/Tuts Top

sergio
https://sergiotapia.me/generate-images-with-name-initials-using-elixir-and-imagemagick-374eca4d14ff Hope this saves you guys some time!...
New
anuragg
Hi everyone! I’m the founder of Render, a new cloud provider with native support for Elixir. When we launched Elixir support the most po...
New
pel_daniel
Hi! I created some animations to explain what happens internally with map &amp; reduce functions. The code is open source. Any feedback...
New
sergio
Wrote this guide on how to integrate DropzoneJS with Phoenix Liveview. Hope it helps someone out! https://sergiotapia.com/dropzonejs-dir...
New
nelsonic
Complete beginners Todo List Tutorial in Phoenix 1.5.3 (latest and greatest): It’s a feature complete TodoMVC clone with 0% JavaScri...
New
WolfDan
So my main OS is Windows, I do must of my work with it, Elixir and vscode elixirls works just fine when you’re working only with elixir, ...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
GenericJam
Just leaving some breadcrumbs for future me and future others like me. Connect with TCP (not secured) - most servers will reject but use...
New
dkuhlman
For those of you who might be interested in using ZeroMQ in Elixir, I converted the Erlang examples in the ZeroMQ “zguide” to Elixir. I ...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

Other popular topics Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

We're in Beta

About us Mission Statement