Module: OmniSocials::Webhooks
- Defined in:
- lib/omnisocials/webhooks.rb
Overview
Webhook signature verification.
OmniSocials signs every webhook delivery with HMAC-SHA256 (Stripe-style):
- Header: X-OmniSocials-Signature: t=
,v1= - Signed value: "timestamp.raw_body" (UTF-8), keyed with the webhook's secret, hex digest.
Class Method Summary collapse
-
.verify(payload:, signature:, secret:, tolerance: 300) ⇒ Object
Verify an OmniSocials webhook delivery and return the parsed event.
Class Method Details
.verify(payload:, signature:, secret:, tolerance: 300) ⇒ Object
Verify an OmniSocials webhook delivery and return the parsed event.
payload - the raw request body String, exactly as received. Do not
parse and re-serialize it first; the signature is over the
raw bytes.
signature - the X-OmniSocials-Signature header value, of the form
t=
Returns the parsed event as a Hash (string keys).
Raises OmniSocials::WebhookVerificationError if the header is malformed, the timestamp is outside the tolerance window, or the signature does not match. Uses a constant-time comparison.
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/omnisocials/webhooks.rb', line 34 def verify(payload:, signature:, secret:, tolerance: 300) unless payload.is_a?(String) raise WebhookVerificationError, "payload must be a String (the raw request body), got #{payload.class}." end unless signature.is_a?(String) && !signature.empty? raise WebhookVerificationError, "Missing signature header." end = nil candidates = [] signature.split(",").each do |pair| key, _, value = pair.strip.partition("=") case key when "t" begin = Integer(value, 10) rescue ArgumentError, TypeError raise WebhookVerificationError, "Malformed signature header: non-integer timestamp." end when "v1" candidates << value end end if .nil? raise WebhookVerificationError, "Malformed signature header: missing 't=' timestamp." end if candidates.empty? raise WebhookVerificationError, "Malformed signature header: missing 'v1=' signature." end unless tolerance.nil? drift = (Time.now.to_i - ).abs if drift > tolerance raise WebhookVerificationError, "Timestamp outside the tolerance window (#{drift}s > #{tolerance}s). " \ "The delivery may be stale or replayed." end end signed_value = "#{}.".b + payload.b expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_value) unless candidates.any? { |candidate| secure_compare(expected, candidate) } raise WebhookVerificationError, "Signature mismatch: the payload was not signed with this secret, " \ "or the body was modified in transit." end begin event = JSON.parse(payload) rescue JSON::ParserError raise WebhookVerificationError, "Payload is not valid JSON." end unless event.is_a?(Hash) raise WebhookVerificationError, "Payload is not a JSON object." end event end |