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
require 'openssl'
require 'rack/utils'
class SignatureVerifier
class SignatureError < StandardError; end
class MissingHeaderError < SignatureError; end
DEFAULT_TOLERANCE = 300 # seconds
def initialize(secret, tolerance: DEFAULT_TOLERANCE)
@secret = secret
@tolerance = tolerance
end
def verify!(payload:, header:)
raise MissingHeaderError, 'missing signature header' if header.nil? || header.empty?
parts = parse(header)
timestamp = parts.fetch('t', nil)
signature = parts.fetch('v1', nil)
raise SignatureError, 'malformed signature header' unless timestamp && signature
if (Time.now.to_i - timestamp.to_i).abs > @tolerance
raise SignatureError, 'timestamp outside tolerance window'
end
expected = OpenSSL::HMAC.hexdigest('SHA256', @secret, "#{timestamp}.#{payload}")
unless Rack::Utils.secure_compare(expected, signature)
raise SignatureError, 'signature mismatch'
end
true
end
private
def parse(header)
header.split(',').each_with_object({}) do |pair, acc|
key, value = pair.split('=', 2)
acc[key.strip] = value.to_s.strip if key && value
end
end
end
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post
data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)
HMAC signed API requests for webhook and partner integrity
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
Share this code
Here's the card — post it anywhere.