Packages
Complete Stripe SDK for Elixir with parity to the official Ruby SDK. V1+V2 coverage (193 services, 320 resource structs, 525 documented params). Per-event modules, Finch HTTP/2, RustyJSON, automatic retries, OAuth, webhooks, telemetry, per-client config, streaming pagination.
Current section
Files
Jump to
Current section
Files
lib/stripe/webhook_plug.ex
if Code.ensure_loaded?(Plug.Conn) do
defmodule Stripe.WebhookPlug do
@moduledoc """
Plug for verifying Stripe webhook signatures.
Reads the raw request body, verifies the `Stripe-Signature` header,
deserializes the event, and assigns it to `conn.assigns.stripe_event`.
Returns 400 on verification failure.
## Setup
Add to your endpoint *before* `Plug.Parsers` (which consumes the body):
plug Stripe.WebhookPlug,
path: "/webhook/stripe"
The signing secret is read from `config :tiger_stripe, :webhook_secret`.
To override the secret per-plug (e.g. multiple webhook endpoints):
plug Stripe.WebhookPlug,
secret: "whsec_other",
path: "/webhook/stripe"
Or use a runtime MFA tuple:
plug Stripe.WebhookPlug,
secret: {MyApp.Config, :stripe_webhook_secret, []},
path: "/webhook/stripe"
## Options
* `:secret` - Signing secret (string or `{mod, fun, args}` MFA tuple).
Defaults to `Application.get_env(:tiger_stripe, :webhook_secret)`.
* `:path` - Request path to match (string). Required.
* `:tolerance` - Maximum event age in seconds (default: 300).
"""
@behaviour Plug
import Plug.Conn
@impl true
def init(opts) do
unless Keyword.has_key?(opts, :path),
do: raise(ArgumentError, "Stripe.WebhookPlug requires :path option")
opts
end
@impl true
def call(%Plug.Conn{request_path: path} = conn, opts) do
if path == Keyword.fetch!(opts, :path) do
verify_and_assign(conn, opts)
else
conn
end
end
defp verify_and_assign(conn, opts) do
secret = resolve_secret(opts)
tolerance = Keyword.get(opts, :tolerance, 300)
with {:ok, body, conn} <- read_body(conn),
{:ok, sig_header} <- get_sig_header(conn),
{:ok, event} <-
Stripe.Webhook.construct_event(body, sig_header, secret, tolerance: tolerance) do
assign(conn, :stripe_event, event)
else
{:error, %Stripe.Error{message: message}} ->
conn
|> put_resp_content_type("text/plain")
|> send_resp(400, message || "Webhook verification failed")
|> halt()
end
end
defp get_sig_header(conn) do
case get_req_header(conn, "stripe-signature") do
[header | _] ->
{:ok, header}
[] ->
{:error,
%Stripe.Error{
type: :signature_verification_error,
message: "Missing Stripe-Signature header"
}}
end
end
defp resolve_secret(opts) do
case Keyword.get(opts, :secret) do
nil ->
case Application.get_env(:tiger_stripe, :webhook_secret) do
nil ->
raise ArgumentError, """
Stripe webhook secret not configured. Either:
config :tiger_stripe, webhook_secret: "whsec_..."
Or pass it explicitly:
plug Stripe.WebhookPlug, secret: "whsec_...", path: "/webhook/stripe"
"""
secret ->
resolve_secret_value(secret)
end
secret ->
resolve_secret_value(secret)
end
end
defp resolve_secret_value({mod, fun, args}), do: apply(mod, fun, args)
defp resolve_secret_value(secret) when is_binary(secret), do: secret
end
end