Verifying request signatures in Elixir/Phoenix

If you’re working with public APIs a common security task is to verify request signatures to prove the request is coming from who you believe you are really talking to.

I’m working on a Slack bot that requires verifying requests using a custom header and a request signature. The documentation is actually pretty good, even providing a sample walkthrough in pseudocode (that leans heavily on Python).

The problem? This works in a much different way with Elixir/Phoenix.

A step-by-step walkthrough of verifying secrets in Slack

1. Grab your signing secret and request body

The signing secret is a secret token that you’d best store in your environment variables (e.g. System.get_env("SLACK_SIGNING_SECRET")).

The request body is the first wrench in your plan. The raw request body is not accessible from the Phoenix conn at all. Even though params (which includes the body_params from the request body) has the request body, Phoenix, in a defensible move, opted to transform raw request data into an Elixir map.

So how do you grab the raw body? You have to intercept your endpoint before the standard Plug.Parser gets to it with your own custom parser.

The answer is succinctly defined within the official Plug docs. For Phoenix, you’ll need to add your request body module to your lib/ folder and call it within your endpoint.ex file above your other Plug.Parsers to properly intercept the request body:

# lib/cache_body_reader.ex
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

# lib/APPNAME_web/endpoint.ex
defmodule APPNAMEWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :APPNAME

  # ...

  # Your new Plug.Parser
  plug(
    Plug.Parsers,
    parsers: [:urlencoded, :json],
    pass: ["text/*"],
    body_reader: {APPNAME.CacheBodyReader, :read_body, []},
    json_decoder: Poison
  )

  # All other existing Plug.Parsers
  plug(
    Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Poison
  )

  # ...
end

Now your conn plugs will contain a newly assigned property, assigns[:raw_body] which provides the stringified, raw request body.

2. Extract the timestamp header from the request

Getting the request timestamp is significantly easier than the raw request body, but there are still a few things that don’t exactly line up with the Slack tutorial:

  1. The request headers all come into Phoenix in lowercase
  2. Elixir timestamps are not the standard UNIX timestamp

The request timestamp is easy to convert: grab the header from the conn object (returned as an Enum) in all lowercase and convert it to a number (headers arrive over as a string):

timestamp =
  conn
  |> get_req_header("x-slack-request-timestamp")
  |> Enum.at(0)
  |> String.to_integer()

The local timestamp is kind of weird. The easiest way is to leverage the Erlang API but the numbers start with the start of the Gregorian calendar (so roughly 2018 years ago) rather than the standard UNIX timestamp (Jan 1, 1970). So you’ll just have to subtract that difference, which is an oddly specific magic number:

@unix_gregorian_offset 62_167_219_200

gregorian_timestamp =
  :calendar.local_time()
  |> :calendar.datetime_to_gregorian_seconds()

local_timestamp = gregorian_timestamp - @unix_gregorian_offset

Now you just need to take the absolute value of the difference and make sure it’s within some reasonable delta (in the case of the tutorial, it’s 5 minutes or 300 seconds):

if abs(local_timestamp - timestamp) > 300 do
  # nothing / return false
else
  # process request / return true
end

3. Concatenate the signature string

This is the easiest step because interpolated strings are just as easy as you think they are, and now that you have the raw request body, grabbing this will be trivial:

sig_basestring = "v0:#{timestamp}:#{conn.assigns[:raw_body]}"

4. Hash the string with the signing secret into a hex signature

Now you need a signature to compare against the one Slack sends you along with the timestamp. Erlang provides a nice crypto library for computing HMAC-SHA256 keyed hashes, you’ll just need to turn that into a hex digest (using Base.encode16()):

my_signature =
  "v0=#{
    :crypto.hmac(
      :sha256,
      System.get_env("SLACK_SIGNING_SECRET"),
      sig_basestring
    )
    |> Base.encode16()
  }"

5. Compare the resulting signature to the header on the request

The light is at the end of the tunnel! There are a few more gotchas that aren’t exactly intuitive:

  1. get_req_header returns an array, even though the same sounds like it returns the singular value
  2. Leverage the Plug.Crypto library to do secure signature comparisons

As you may have seen from earlier code, get_req_header takes in your conn and the string of the request header key to return the value…as an array. Not sure why but it’s easy to remedy with pattern matching.

Finally, to achieve the hmac.compare pseudocode from the Slack tutorial, Elixir has an equal Plug.Crypto.secure_compare:

[slack_signature] = conn |> get_req_header("x-slack-signature")

Plug.Crypto.secure_compare(my_signature, slack_signature)

Putting it all together

Now that we’ve solved all the tutorial discrepancies, it’s time to put it all together in the context of a Phoenix application.

We’ve already added the custom raw request body header library and modified our endpoint to accept this Plug.Parser. Now we just need to bring the verification into our API controller endpoint you specify within your Slack app dashboard:

defmodule MYAPPWeb.SlackController do
  use MYAPPWeb, :controller

  @doc """
  Handle when a user clicks an interactive button in the Slack app
  """
  def actions(conn, params) do
    %{
      "actions" => actions,
      "team" => %{"id" => team_id},
      "type" => "interactive_message",
      "user" => %{"id" => user_id}
    } = Poison.decode!(params["payload"])

    if verified(conn) do
      # do your thing!
      conn |> send_resp(:ok, "")
    else
      conn |> send_resp(:unauthorized, "")
    end
  end

  defp verified(conn) do
    timestamp =
      conn
      |> get_req_header("x-slack-request-timestamp")
      |> Enum.at(0)
      |> String.to_integer()

    local_timestamp =
      :calendar.local_time()
      |> :calendar.datetime_to_gregorian_seconds()

    if abs(local_timestamp - 62_167_219_200 - timestamp) > 60 * 5 do
      false
    else
      my_signature =
        "v0=#{
          :crypto.hmac(
            :sha256,
            System.get_env("SLACK_SIGNING_SECRET"),
            "v0:#{timestamp}:#{conn.assigns[:raw_body]}"
          )
          |> Base.encode16()
        }"
        |> String.downcase()

      [slack_signature] = conn |> get_req_header("x-slack-signature")

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

Congratulations! You can now securely talk with Slack by verifying it’s header signature against the one you’ve generated by hashing your signing secret with the current timestamp. The Slack API documentation is thorough and helpful, but translating it to work in Elixir/Phoenix is not as intuitive as you might imagine.


Get the FREE UI crash course

Sign up for our newsletter and receive a free UI crash course to help you build beautiful applications without needing a design background. Just enter your email below and you'll get a download link instantly.

A new version of this app is available. Click here to update.