You are reading the docs for rws 18.6 The latest release is 18.8. Read this page for the latest release.

Webhook Signature Verification

Verify inbound webhook signatures from GitHub, Shopify, and Stripe with built-in HMAC helpers.

Requires the webhook feature:

[dependencies]
rust-web-server = { version = "18", features = ["webhook"] }

Any handler that accepts webhooks from a third party needs to confirm the request actually came from that provider — otherwise anyone who finds (or guesses) the endpoint URL can send a forged payload. hmac and sha2 are already dependencies elsewhere in rws (auth, crypto, storage-s3); this feature just adds the provider-specific conventions on top so you don't re-derive them from each provider's docs.

Quick start

use rust_web_server::webhook::{verify_webhook_signature, WebhookProvider};

fn handle_github_webhook(body: &[u8], signature_header: &str, secret: &[u8]) -> bool {
    verify_webhook_signature(WebhookProvider::GitHub, body, secret, signature_header)
}
Use the raw request body

body must be the exact bytes read off the wire. Re-serializing a parsed Json<T> value before verifying will not reproduce the byte sequence the provider actually signed, and verification will always fail. Read it via the Body extractor or request.body directly.

Supported providers

Provider Header Format
GitHub X-Hub-Signature-256 sha256=<hex-hmac-sha256-of-body>
Shopify X-Shopify-Hmac-Sha256 <base64-hmac-sha256-of-body>
Stripe Stripe-Signature t=<unix_ts>,v1=<hex>[,v1=<hex>...]

Call the per-provider function directly when the provider is known at the call site — this is equivalent to verify_webhook_signature with the matching WebhookProvider variant:

use rust_web_server::webhook::{verify_github_signature, verify_shopify_signature, verify_stripe_signature};

verify_github_signature(body, secret, header_value);
verify_shopify_signature(body, secret, header_value);
verify_stripe_signature(body, secret, header_value);
GitHub's legacy X-Hub-Signature header

GitHub also sends an older SHA-1-based X-Hub-Signature for backward compatibility. verify_github_signature only supports X-Hub-Signature-256 — SHA-1 is cryptographically broken and this crate has no SHA-1 dependency. Always prefer the SHA-256 header; GitHub recommends it for all new integrations.

Stripe's timestamp tolerance

Stripe signs "{timestamp}.{body}", not the body alone, and expects you to reject requests whose timestamp is too far from the current time — this stops a captured request from being replayed later. verify_stripe_signature checks against a default 300-second (5-minute) window, matching Stripe's own recommendation:

use rust_web_server::webhook::{verify_stripe_signature_with_tolerance, STRIPE_DEFAULT_TOLERANCE_SECS};

// Widen the window to 10 minutes:
verify_stripe_signature_with_tolerance(body, secret, header_value, 600);

// The default constant, if you want to reference it:
assert_eq!(300, STRIPE_DEFAULT_TOLERANCE_SECS);

Stripe may list multiple v1= entries in the header while a signing-secret rotation is in progress — verification succeeds if any entry matches, so you don't need to special-case rotation.

In a handler

use rust_web_server::app::App;
use rust_web_server::core::New;
use rust_web_server::request::Request;
use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
use rust_web_server::webhook::{verify_webhook_signature, WebhookProvider};

fn stripe_webhook(request: &Request) -> Response {
    let signature = request
        .get_header("Stripe-Signature".to_string())
        .map(|h| h.value.to_string())
        .unwrap_or_default();

    if !verify_webhook_signature(WebhookProvider::Stripe, &request.body, b"whsec_...", &signature) {
        let mut res = Response::new();
        res.status_code = *STATUS_CODE_REASON_PHRASE.n401_unauthorized.status_code;
        return res;
    }

    // Signature verified — safe to process request.body as a trusted Stripe event.
    Response::new()
}

Edit this page on GitHub