Module: SeatLayer::Webhook
- Defined in:
- lib/seatlayer/webhook.rb
Overview
Webhook signature verification.
The most security-sensitive thing an integrator writes by hand, and the two classic mistakes are both easy to make and silent:
- verifying against a re-serialised body, which changes bytes and fails — or worse, gets "fixed" by skipping verification entirely;
- comparing signatures with ==, which leaks the expected value through timing.
So the SDK does it, takes the RAW body, and compares in constant time.
Class Method Summary collapse
-
.verify(payload, signature, secret) ⇒ Object
Verify a delivery and return its decoded payload.
Class Method Details
.verify(payload, signature, secret) ⇒ Object
Verify a delivery and return its decoded payload.
payload must be the raw request body. In Rails that is
request.raw_post; in Sinatra, request.body.read. Never a
parsed Hash re-encoded.
NOTE ON REPLAY: deliveries are signed over the body, which carries an "at" timestamp — but nothing enforces a freshness window, so a captured delivery stays valid indefinitely. Replay protection is yours: every event carries an occurrenceId, and the correct pattern is to record processed ids and ignore repeats. Do not skip this.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/seatlayer/webhook.rb', line 33 def verify(payload, signature, secret) raise WebhookVerificationError, "A webhook signing secret is required." if secret.nil? || secret.empty? if signature.nil? || signature.empty? raise WebhookVerificationError, "Missing X-SeatLayer-Signature header." end scheme, _, provided = signature.partition("=") if scheme != "sha256" || provided.empty? raise WebhookVerificationError, "Unsupported signature format #{signature.inspect}; expected \"sha256=<hex>\"." end expected = OpenSSL::HMAC.hexdigest("SHA256", secret, payload) # secure_compare is constant time and handles a length mismatch without # leaking which of the two failures occurred. unless OpenSSL.secure_compare(expected, provided) raise WebhookVerificationError, "Webhook signature did not match." end begin JSON.parse(payload) rescue JSON::ParserError => e raise WebhookVerificationError, "Signature verified but the body is not valid JSON: #{e.}" end end |