CookbookVerify in Ruby

Verify webhooks in Ruby

⚠️
Preview — this feature is being built; the design below is the spec.

Check the syntic-signature header before trusting a webhook delivery. The header carries a timestamp t and one or more v1 signatures (several during secret rotation). The signature is the hex HMAC-SHA256 of t + "." + raw body, keyed with your endpoint’s signing secret. Reject the delivery if no v1 matches in constant time, or if t is more than 300 seconds away from your clock. Always hash the raw body bytes, before any JSON parsing.

require "openssl"
 
module SynticWebhooks
  def self.verify(header, raw_body, secret, now: Time.now.to_i)
    pairs = header.to_s.split(",").map { |p| p.strip.split("=", 2) }.select { |kv| kv.size == 2 }
    t = pairs.find { |k, _| k == "t" }&.last
    sigs = pairs.select { |k, _| k == "v1" }.map(&:last)
    return false if t.nil? || (now - t.to_i).abs > 300
 
    expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{t}.#{raw_body}")
    sigs.any? { |s| s.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(s, expected) }
  end
end
 
# Sinatra
post "/syntic" do
  raw = request.body.read
  halt 400 unless SynticWebhooks.verify(request.env["HTTP_SYNTIC_SIGNATURE"], raw, ENV.fetch("SYNTIC_WEBHOOK_SECRET"))
  WebhookJob.perform_later(raw) # process later
  200
end

In Rails, read request.raw_post rather than params, which has already been parsed and re-ordered.

Test it

Send a test event to your endpoint, or recompute a signature by hand as shown on Webhooks.

Was this page helpful?