I was facing a similar problem, and I wrote a Plug along the lines of this (note I'm still learning so this might be done better):
defmodule Esch.Plugs.HMACValidator do
import Plug.Conn
def init(default), do: default
def call(%Plug.Conn{req_headers: req_headers} = conn, _default) do
hmac_code_tuple = List.keyfind(req_headers, "hmac_token", 0)
if hmac_code_tuple do
hmac_code = elem(hmac_code_tuple,1) |> String.downcase
{:ok, body, conn} = read_body(conn)
hmac_test_code = :crypto.hmac(:sha512, "secret", body) |> Base.encode16 |> String.downcase
if hmac_test_code == hmac_code do
params = Poison.decode!(body)
conn
|> assign(:authorized_api_call, true)
|> struct(%{:body_params => params})
else
conn |> put_resp_content_type("text/plain") |> send_resp(401, "Not Authorized") |> halt
end
else
conn
end
end
def call(conn, _default) do
conn
end
end
The above request compares a HMAC signed body with the HMAC signature in a request header.
I circumvented the read_body
-problem by parsing the JSON within the same code when the signature matched the expected signature. The conn
ection is passed through if the request doesn't fit a typical API call (in my case doesn't have a HMAC-header-token), hence leaving the body_params unread.
I then plugged the above Plug in endpoint.ex
just before the Plug.Parsers is plugged in.
...
plug MyApp.Plugs.HMACValidator
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Poison
...
I got some inspiration from the discussion in this Phoenix issue: Way to Read Request Body As String
:json
parser. – DogbertPlug.Conn.read_body/2
currently asks the adapter to read the body. The default adapter, Cowboy, reads the body directly from the underlying TCP socket and does not allow any caching. I think your approach would require changes inPlug.Conn.read_body/2
. – Dogbert