Fivetrex.WebhookSignature (Fivetrex v0.2.1)
View SourceHMAC-SHA256 signature verification for Fivetran webhook payloads.
When you create a webhook with a secret, Fivetran signs each request body
using HMAC-SHA256 and includes the signature in the X-Fivetran-Signature-256
header. This module provides functions to verify these signatures.
Security
Signature verification is crucial for ensuring webhook requests actually originate from Fivetran and haven't been tampered with. Always verify signatures before processing webhook payloads.
Usage
In your webhook handler (e.g., a Phoenix controller):
def webhook(conn, _params) do
signature = get_req_header(conn, "x-fivetran-signature-256") |> List.first()
body = conn.assigns[:raw_body] # Requires custom plug to capture raw body
secret = Application.get_env(:my_app, :fivetran_webhook_secret)
case Fivetrex.WebhookSignature.verify(body, signature, secret) do
:ok ->
# Process the webhook
json(conn, %{status: "ok"})
{:error, :invalid_signature} ->
conn |> put_status(401) |> json(%{error: "Invalid signature"})
{:error, :missing_signature} ->
conn |> put_status(400) |> json(%{error: "Missing signature"})
end
endCapturing Raw Body
To verify signatures, you need access to the raw request body. Phoenix typically parses JSON automatically, so you need to capture the raw body first. Add a custom plug:
# In your endpoint.ex, before Plug.Parsers:
plug :capture_raw_body
defp capture_raw_body(conn, _opts) do
{:ok, body, conn} = Plug.Conn.read_body(conn)
conn
|> assign(:raw_body, body)
|> Plug.Conn.put_req_header("x-raw-body", body)
endOr use Fivetrex.WebhookPlug which handles this automatically.
Security Notes
- This module uses constant-time comparison via
Plug.Crypto.secure_compare/2to prevent timing attacks - Store webhook secrets securely (environment variables, secrets manager)
- Never log secrets or raw signatures in production
- Rotate secrets periodically
See Also
Fivetrex.Webhooks- API functions for managing webhooksFivetrex.WebhookPlug- Plug that handles signature verificationFivetrex.Models.WebhookEvent- Struct for parsing webhook payloads
Summary
Functions
Computes the HMAC-SHA256 signature for a payload.
Returns the expected HTTP header name for Fivetran signatures.
Verifies that a webhook payload signature is valid.
Functions
Computes the HMAC-SHA256 signature for a payload.
Returns the hex-encoded signature in uppercase, matching Fivetran's format.
Parameters
payload- The raw request body as a stringsecret- Your webhook secret
Returns
The hex-encoded HMAC-SHA256 signature in uppercase.
Examples
signature = Fivetrex.WebhookSignature.compute_signature(
~s({"event":"sync_end"}),
"my_secret"
)
# Returns something like "A1B2C3D4..."
@spec signature_header() :: String.t()
Returns the expected HTTP header name for Fivetran signatures.
Fivetran sends the signature in the X-Fivetran-Signature-256 header.
Use this function to get the header name for extracting signatures from
incoming requests.
Returns
The string "x-fivetran-signature-256" (lowercase, as headers are
case-insensitive in HTTP).
Examples
header_name = Fivetrex.WebhookSignature.signature_header()
signature = get_req_header(conn, header_name) |> List.first()
@spec verify(String.t(), String.t() | nil, String.t()) :: :ok | {:error, :invalid_signature | :missing_signature}
Verifies that a webhook payload signature is valid.
Computes the expected HMAC-SHA256 signature for the payload using the provided secret and compares it to the signature from the request header.
Parameters
payload- The raw request body as a string (before JSON parsing)signature- The signature from theX-Fivetran-Signature-256headersecret- Your webhook secret configured in Fivetran
Returns
:ok- Signature is valid{:error, :invalid_signature}- Signature does not match{:error, :missing_signature}- No signature provided (nil or empty)
Examples
# Valid signature
payload = ~s({"event":"sync_end","connector_id":"abc123"})
secret = "my_webhook_secret"
signature = Fivetrex.WebhookSignature.compute_signature(payload, secret)
:ok = Fivetrex.WebhookSignature.verify(payload, signature, secret)
# Invalid signature
{:error, :invalid_signature} =
Fivetrex.WebhookSignature.verify(payload, "wrong_signature", secret)
# Missing signature
{:error, :missing_signature} =
Fivetrex.WebhookSignature.verify(payload, nil, secret)