7
votes

I have a requirement to check the digest of JSON content sent to a phoenix server. To check the digest the raw body is needed. Is there any way to access the raw content in a plug later in the pipeline than the parsers. I want to add the following Digest verification plug to the end of the pipeline but cannot work out how it accesses the raw content that was sent.

  plug Plug.Parsers,
    parsers: [:urlencoded, :json],
    pass: ["*/*"],
    json_decoder: Poison

  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.VerifyDigest
2
The body seems to be discarded after Plug.Parsers.JSON is done parsing it. I think you'll have to copy that module and modify it to store the raw body and then use that instead of default :json parser.Dogbert
If thats really the case it seams like plug is missing some helpful abstractions. Thinking some more about this problem it seams like it would make sense to put the verify digest plug first and then have some way for it to leave the body in the plug so the parsers could work as normalPeter Saxton
Plug.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 in Plug.Conn.read_body/2.Dogbert
Perhaps another approach is the right way then, hopefully without degrading the simplicity of the plug pipeline.Peter Saxton

2 Answers

5
votes

Copied from my answer here.

You can pass a custom :body_reader option to Plug.Parsers in order to cache the body for later use.

You'll want to not read the body before the Parser and instead cache the body to read later from your plug that wants to hash it.

Option:

:body_reader - an optional replacement (or wrapper) for Plug.Conn.read_body/2 to provide a function that gives access to the raw body before it is parsed and discarded. It is in the standard format of {Module, :function, [args]} (MFA) and defaults to {Plug.Conn, :read_body, []}.

Example:

Sometimes you may want to customize how a parser reads the body from the connection. For example, you may want to cache the body to perform verification later, such as HTTP Signature Verification. This can be achieved with a custom body reader that would read the body and store it in the connection, such as:

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

which could then be set as:

plug Plug.Parsers,
  parsers: [:urlencoded, :json],
  pass: ["text/*"],
  body_reader: {CacheBodyReader, :read_body, []},
  json_decoder: Jason

It was added in Plug v1.5.1.

3
votes

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 connection 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