Verifying Signature from request header in elixir

I have a typeform request where I need to verify the signature manually. :crypto.hmac is always returning an error.

No sure what I’m doing wrong here:

defmodule ApiWeb.PageController do
  use ApiWeb, :controller

  def index(conn, _params) do
    conn
    |> verify_signature
    
    send_resp(conn, 201, "")
  end
  
  defp verify_signature(conn) do
    {:ok, body, conn} = Plug.Conn.read_body(conn)

    my_signature = :crypto.hmac(:sha256, System.get_env("TYPEFORM_SECRET"), body) |> Base.encode16 |> String.downcase

    [typeform_signature] = conn |> get_req_header("Typeform-Signature")

    Plug.Crypto.secure_compare(my_signature, typeform_signature)
  end
end

Here’s the error returned

** (exit) an exception was raised:
** (ArgumentError) argument error
(crypto 4.7) crypto.erl:978: :crypto.hmac/3
(api 0.1.0) lib/api_web/controllers/page_controller.ex:14: ApiWeb.PageController.verify_signature/1
(api 0.1.0) lib/api_web/controllers/page_controller.ex:6: ApiWeb.PageController.index/2
(api 0.1.0) lib/api_web/controllers/page_controller.ex:1: ApiWeb.PageController.action/2
(api 0.1.0) lib/api_web/controllers/page_controller.ex:1: ApiWeb.PageController.phoenix_controller_pipeline/2
(phoenix 1.5.3) lib/phoenix/router.ex:352: Phoenix.Router.call/2
(api 0.1.0) lib/api_web/endpoint.ex:1: ApiWeb.Endpoint.plug_builder_call/2
(api 0.1.0) lib/plug/debugger.ex:132: ApiWeb.Endpoint.“call (overridable 3)”/2
(api 0.1.0) lib/api_web/endpoint.ex:1: ApiWeb.Endpoint.call/2
(phoenix 1.5.3) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4
(cowboy 2.7.0) api/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
(cowboy 2.7.0) api/deps/cowboy/src/cowboy_stream_h.erl:320: :cowboy_stream_h.execute/3
(cowboy 2.7.0) api/deps/cowboy/src/cowboy_stream_h.erl:302: :cowboy_stream_h.request_process/3
(stdlib 3.13) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

:wave:

{:ok, body, conn} = Plug.Conn.read_body(conn) probably returns {:ok, nil, conn} and passing nil to :crypto.hmac/3 causes the error.

This is because it’s only possible to read the body once, and plug Plug.Parsers already does it (it’s in your endpoint.ex). The workaround is a custom body reader which caches the raw body contents.

1 Like

That’s confusing, how can body be nil if there’s a payload?

IIRC reading the body is a stateful process. Cowboy reads the body from the socket until it’s over or some size limit is reached and trying to read the body again doesn’t find anything on the socket and returns nothing. And Plug.Parsers doesn’t store the body since it’s usually not needed after it’s been processed and might hurt performance.

so is making a custom body reader the “best practice” of reading the body? or is there a more proper way? I feel like i’m hacking my way through this typeform signature comparison

It’s in the docs, so yes.

Check out https://elixirforum.com/search?q=read%20req%20body for more (possibly outdated) discussion.

So I followed the instructions, Created a module called CacheBodyReader saved as file cache_body_reader.ex, and updated Plug.Parsers in endpoint.ex to look like this:
plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], body_reader: {ApiWeb.CacheBodyReader, :read_body, []}, json_decoder: Phoenix.json_library()

Now I’m calling:
{:ok, body, conn} = ApiWeb.CacheBodyReader.read_body(conn, [])

instead of {:ok, body, conn} = Plug.Conn.read_body(conn)

I’m still getting the same error in the controller. What am I doing wrong?

You don’t need to call your module. You can access the raw body contents in the conn.

If your cache body reader module is like this:

defmodule CacheBodyReader do
  def read_body(conn, opts) do
    {:ok, body, conn} = Plug.Conn.read_body(conn, opts)
    conn = update_in(conn.assigns[:raw_body], &[body | (&1 || [])])
    {:ok, body, conn}
  end
end

then you can access your raw body in conn.assigns.raw_body. Note that you probably don’t need to save the body for every request, but only for those that need it. I usually put some conditions on conn.path_name to decide if there is a need to save the raw body.

1 Like

ok thanks I got my problem solved. Really appreciate it man

1 Like