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

{: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.
2 Likes
ok thanks I got my problem solved. Really appreciate it man
1 Like