Packages
stripity_stripe
3.3.1
3.3.2
3.3.1
3.2.0
3.1.1
3.1.0
3.0.0
2.17.3
2.17.2
2.17.1
2.17.0
2.16.0
2.15.1
2.15.0
2.14.1
2.14.0
2.13.0
2.12.1
2.12.0
2.11.0
2.10.0
2.9.0
2.8.0
2.7.2
2.7.1
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.3
2.2.2
2.2.1
2.2.0
2.1.0
2.0.1
2.0.0
2.0.0-alpha.11
2.0.0-alpha.10
2.0.0-alpha.9
2.0.0-alpha.8
2.0.0-alpha.7
2.0.0-alpha.6
2.0.0-alpha.5
2.0.0-alpha.4
2.0.0-alpha.3
2.0.0-alpha.2
2.0.0-alpha.1
1.6.2
1.6.1
1.6.0
1.4.0
1.3.0
1.2.0
1.1.0
0.5.0
0.4.0
0.3.0
0.2.0
A Stripe client for Elixir.
Current section
Files
Jump to
Current section
Files
lib/stripe/webhook.ex
defmodule Stripe.Webhook do
@moduledoc """
Creates a Stripe Event from webhook's payload if signature is valid.
"""
@default_tolerance 300
@expected_scheme "v1"
@doc """
Verify webhook payload and return a Stripe event.
`payload` is the raw, unparsed content body sent by Stripe, which can be
retrieved with `Plug.Conn.read_body/2`. Note that `Plug.Parsers` will read
and discard the body, so you must implement a [custom body reader][1] if the
plug is located earlier in the pipeline.
`signature` is the value of `Stripe-Signature` header, which can be fetched
with `Plug.Conn.get_req_header/2`.
`secret` is your webhook endpoint's secret from the Stripe Dashboard.
`tolerance` is the allowed deviation in seconds from the current system time
to the timestamp found in `signature`. Defaults to 300 seconds (5 minutes).
`opts` is a keyword list of options. Supported options:
* `:response_as` - controls the shape of the value returned in the `:ok`
tuple. One of:
* `:struct` (default) - returns a `Stripe.Event.t()`.
* `:map` - returns the decoded payload as a map with string keys (useful
when persisting webhooks for later replay or struct conversion via
`Stripe.Converter.convert_result/1`).
* `:raw` - returns the original `payload` string verbatim.
When `tolerance` is omitted, `opts` may be passed directly as the 4th
argument:
Stripe.Webhook.construct_event(payload, signature, secret, response_as: :map)
Stripe API reference:
https://stripe.com/docs/webhooks/signatures#verify-manually
[1]: https://hexdocs.pm/plug/Plug.Parsers.html#module-custom-body-reader
## Example
case Stripe.Webhook.construct_event(payload, signature, secret) do
{:ok, %Stripe.Event{} = event} ->
# Return 200 to Stripe and handle event
{:error, reason} ->
# Reject webhook by responding with non-2XX
end
"""
@spec construct_event(
String.t(),
String.t(),
String.t(),
integer | Keyword.t(),
Keyword.t()
) :: {:ok, Stripe.Event.t() | map() | String.t()} | {:error, any}
def construct_event(payload, signature_header, secret, tolerance_or_opts \\ @default_tolerance, opts \\ [])
def construct_event(payload, signature_header, secret, opts, []) when is_list(opts) do
construct_event(payload, signature_header, secret, @default_tolerance, opts)
end
def construct_event(payload, signature_header, secret, tolerance, opts) when is_integer(tolerance) do
case verify_header(payload, signature_header, secret, tolerance) do
:ok -> {:ok, format_response(payload, Keyword.get(opts, :response_as, :struct))}
error -> error
end
end
defp format_response(payload, :struct), do: convert_to_event!(payload)
defp format_response(payload, :map), do: convert_to_map!(payload)
defp format_response(payload, :raw), do: payload
defp verify_header(payload, signature_header, secret, tolerance) do
case get_timestamp_and_signatures(signature_header, @expected_scheme) do
{nil, _} ->
{:error, "Unable to extract timestamp and signatures from header"}
{_, []} ->
{:error, "No signatures found with expected scheme #{@expected_scheme}"}
{timestamp, signatures} ->
with {:ok, timestamp} <- check_timestamp(timestamp, tolerance),
{:ok, _signatures} <- check_signatures(signatures, timestamp, payload, secret) do
:ok
else
{:error, error} -> {:error, error}
end
end
end
defp get_timestamp_and_signatures(signature_header, scheme) do
signature_header
|> String.split(",")
|> Enum.map(&String.split(&1, "="))
|> Enum.reduce({nil, []}, fn
["t", timestamp], {nil, signatures} ->
{to_integer(timestamp), signatures}
[^scheme, signature], {timestamp, signatures} ->
{timestamp, [signature | signatures]}
_, acc ->
acc
end)
end
defp to_integer(timestamp) do
case Integer.parse(timestamp) do
{timestamp, _} ->
timestamp
:error ->
nil
end
end
defp check_timestamp(timestamp, tolerance) do
now = System.system_time(:second)
tolerance_zone = now - tolerance
if timestamp < tolerance_zone do
{:error, "Timestamp outside the tolerance zone (#{now})"}
else
{:ok, timestamp}
end
end
defp check_signatures(signatures, timestamp, payload, secret) do
signed_payload = "#{timestamp}.#{payload}"
expected_signature = compute_signature(signed_payload, secret)
if Enum.any?(signatures, &secure_equals?(&1, expected_signature)) do
{:ok, signatures}
else
{:error, "No signatures found matching the expected signature for payload"}
end
end
defp compute_signature(payload, secret) do
digest = hmac(:sha256, secret, payload)
Base.encode16(digest, case: :lower)
end
# Remove when OTP 22 compatibility is no longer needed
if System.otp_release() >= "22" do
defp hmac(digest, key, data), do: :crypto.mac(:hmac, digest, key, data)
else
defp hmac(digest, key, data), do: :crypto.hmac(digest, key, data)
end
defp secure_equals?(input, expected) when byte_size(input) == byte_size(expected) do
input = String.to_charlist(input)
expected = String.to_charlist(expected)
secure_compare(input, expected)
end
defp secure_equals?(_, _), do: false
defp secure_compare(acc \\ 0, input, expected)
defp secure_compare(acc, [], []), do: acc == 0
defp secure_compare(acc, [input_codepoint | input], [expected_codepoint | expected]) do
import Bitwise
acc
|> bor(bxor(input_codepoint, expected_codepoint))
|> secure_compare(input, expected)
end
defp convert_to_map!(payload) do
Stripe.API.json_library().decode!(payload)
end
defp convert_to_event!(payload) do
payload |> convert_to_map!() |> Stripe.Converter.convert_result()
end
end