Packages
stripity_stripe
2.3.0
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).
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) ::
{:ok, Stripe.Event.t()} | {:error, any}
def construct_event(payload, signature_header, secret, tolerance \\ @default_tolerance) do
case verify_header(payload, signature_header, secret, tolerance) do
:ok ->
{:ok, convert_to_event!(payload)}
error ->
error
end
end
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
:crypto.hmac(:sha256, secret, payload)
|> Base.encode16(case: :lower)
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(input_codepoint ^^^ expected_codepoint)
|> secure_compare(input, expected)
end
defp convert_to_event!(payload) do
payload
|> Stripe.API.json_library().decode!()
|> Stripe.Converter.convert_result()
end
end