Current section

Files

Jump to
slack_request lib slack_request.ex
Raw

lib/slack_request.ex

defmodule SlackRequest do
@moduledoc """
Utilities for verifing an HTTP request can be authenticated as a request
coming from Slack HQ.
https://api.slack.com/authentication/verifying-requests-from-slack
"""
@allowed_leeway 5 * 60
@signature_header_key "x-slack-signature"
@timestamp_header_key "x-slack-request-timestamp"
@version "v0"
@spec valid_request?(Plug.Conn.t(), Keyword.t()) :: boolean()
def valid_request?(conn, options \\ []) do
valid_timestamp?(conn, options) && valid_signature?(conn, options)
end
@spec valid_timestamp?(Plug.Conn.t(), Keyword.t()) :: boolean()
@spec valid_timestamp?(binary(), Keyword.t()) :: boolean()
def valid_timestamp?(timestamp, options \\ [])
def valid_timestamp?(%Plug.Conn{} = conn, options) do
conn
|> timestamp()
|> valid_timestamp?(options)
end
def valid_timestamp?(timestamp, options) when is_binary(timestamp) do
abs(
String.to_integer(timestamp) -
Keyword.get(options, :current_timestamp, System.system_time(:second))
) <= @allowed_leeway
end
def valid_timestamp?(_timestamp, _options), do: false
@spec valid_signature?(Plug.Conn.t(), Keyword.t()) :: boolean()
def valid_signature?(conn, opts \\ []) do
body = Keyword.get_lazy(opts, :body, fn -> SlackRequest.BodyReader.cached_body(conn) end)
secret = Keyword.get_lazy(opts, :secret, &signing_secret/0)
hmac_hex =
:crypto.mac(
:hmac,
:sha256,
secret,
"#{@version}:#{timestamp(conn)}:#{body}"
)
|> Base.encode16(case: :lower)
signature(conn) == "#{@version}=#{hmac_hex}"
end
@spec timestamp(Plug.Conn.t()) :: binary() | nil
def timestamp(conn) do
header_value(conn, @timestamp_header_key)
end
@spec signature(Plug.Conn.t()) :: binary() | nil
def signature(conn) do
header_value(conn, @signature_header_key)
end
defp header_value(conn, key) do
conn
|> Plug.Conn.get_req_header(key)
|> case do
[value] ->
value
_ ->
nil
end
end
defp signing_secret do
Application.fetch_env!(:slack_request, :signing_secret)
end
end