Current section
Files
Jump to
Current section
Files
lib/plugs/http_signature.ex
# Based on:
# https://github.com/dashbitco/bytepack_archive/blob/main/apps/bytepack_web/lib/bytepack_web/controllers/webhooks/http_signature.ex
defmodule CaptainHookClient.Plugs.HTTPSignature do
@moduledoc """
Verifies the request body in order to ensure that its signature is valid.
This verification can avoid someone to send a request on behalf of our client.
So the client must send a header with the following structure:
t=timestamp-in-seconds,
v1=signature
Where the `timestamp-in-seconds` is the system time in seconds, and `signature`
is the HMAC using the SHA256 algorithm of timestamp and the payload, signed by
a shared secret with us.
This is based on what Stripe is doing: https://stripe.com/docs/webhooks/signatures
"""
defmodule RawBodyNotPresentError do
defexception message: "raw body is not available"
end
import Plug.Conn
@behaviour Plug
@signature_header_name "Signature"
@impl true
@spec init(keyword) :: keyword
def init(opts) do
Keyword.fetch!(opts, :secret)
opts
end
@impl true
@spec call(Plug.Conn.t(), keyword) :: Plug.Conn.t()
def call(conn, opts) do
signature_header_name = Keyword.get(opts, :signature_header_name, @signature_header_name)
with {:ok, header} <- signature_header(conn, signature_header_name),
{:ok, body} <- raw_body(conn),
:ok <- CaptainHookClient.verify(header, body, fetch_secret!(conn, opts), opts) do
conn
else
{:error, error} ->
conn
|> put_status(:bad_request)
|> put_resp_content_type("application/json")
|> send_resp(
Plug.Conn.Status.code(:bad_request),
"{\"errors\": {\"detail\": \"HTTP Signature is invalid: #{error}\"}}"
)
|> halt()
end
end
defp signature_header(conn, header_name) do
case get_req_header(conn, header_name) do
[header] when is_binary(header) ->
{:ok, header}
_ ->
{:error, "signature is not present in header #{inspect(header_name)}"}
end
end
defp raw_body(conn) do
case conn do
%Plug.Conn{assigns: %{raw_body: raw_body}} ->
{:ok, IO.iodata_to_binary(raw_body)}
_ ->
raise RawBodyNotPresentError
end
end
defp fetch_secret!(conn, opts) do
opts
|> Keyword.fetch!(:secret)
|> case do
secret when is_binary(secret) -> secret
secret when is_function(secret, 1) -> secret.(conn)
end
end
end