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.Parser
s 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:
- The request headers all come into Phoenix in lowercase
- 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:
get_req_header
returns an array, even though the same sounds like it returns the singular value- 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.