ruby 94 lines · 2 tabs

Verifying Stripe Webhook Signatures in Sinatra with a before Filter

Shared by codesnips Jul 2026
2 tabs
require 'sinatra/base'
require 'json'
require_relative 'signature_verifier'

class WebhookApp < Sinatra::Base
  configure do
    set :verifier, SignatureVerifier.new(ENV.fetch('STRIPE_WEBHOOK_SECRET'))
  end

  helpers do
    def raw_body
      @raw_body ||= request.body.read
    end

    def verified_event
      @verified_event ||= JSON.parse(raw_body)
    end

    def already_processed?(dedupe_key)
      ProcessedEvent.exists?(dedupe_key)
    end
  end

  before '/webhooks/*' do
    content_type :json
    begin
      settings.verifier.verify!(
        payload: raw_body,
        header: request.env['HTTP_STRIPE_SIGNATURE']
      )
    rescue SignatureVerifier::MissingHeaderError
      halt 401, { error: 'missing signature' }.to_json
    rescue SignatureVerifier::SignatureError => e
      halt 400, { error: e.message }.to_json
    end
  end

  post '/webhooks/stripe' do
    event = verified_event
    dedupe_key = event.fetch('id')

    if already_processed?(dedupe_key)
      status 200
      return { status: 'duplicate' }.to_json
    end

    ProcessWebhookJob.perform_async(dedupe_key, event['type'], event['data'])
    status 200
    { status: 'accepted' }.to_json
  end
end
2 files · ruby Explain with highlit

This snippet shows how a Sinatra service verifies inbound webhook signatures before any handler runs, which is the standard way to prove a request genuinely came from a provider like Stripe rather than a forged POST. The core idea is HMAC over the exact raw request body plus a timestamp, compared in constant time against the signature the provider sent in a header.

In SignatureVerifier, the class parses the Stripe-Signature header into its t= timestamp and v1= signature parts, then rebuilds the signed payload as timestamp.body and computes OpenSSL::HMAC.hexdigest with the shared secret. The comparison uses Rack::Utils.secure_compare (a constant-time equality check) so an attacker cannot use response timing to guess the signature byte by byte. It also rejects payloads whose timestamp is outside a tolerance window, which defends against replay attacks where a valid old request is captured and resent. The class raises typed errors (SignatureError) rather than returning booleans so callers can map each failure to a distinct outcome.

The webhook app wires this into the request lifecycle. A crucial detail is reading request.body.read and caching it in @raw_body; the signature must be computed over the untouched bytes, so parsing JSON first would corrupt the comparison. The before '/webhooks/*' filter runs the verifier and halts with 400 on a bad signature or 401 when the header is missing, meaning no route body ever executes for an unverified request. Verified events are handed to a dedupe_key-based check so redelivered events are acknowledged without double-processing.

The trade-offs are worth noting: the raw body must be captured before any middleware consumes it, the clock tolerance balances replay safety against legitimate delivery delay, and the secret belongs in the environment, never in code. Returning 2xx quickly and offloading real work to a background job keeps the provider from retrying due to timeouts. The helpers block exposes verified_event so individual routes stay thin and focused on business logic rather than security plumbing.


Related snips

Share this code

Here's the card — post it anywhere.

Verifying Stripe Webhook Signatures in Sinatra with a before Filter — share card
Link copied