ruby 41 lines · 7 steps

Signing and verifying webhooks in Ruby

An HMAC-based scheme that binds a timestamp to each payload and rejects stale or tampered requests.

Explained by highlit
1require "openssl"
2require "base64"
3 
4module WebhookSignature
5 DEFAULT_TOLERANCE = 300
6 
7 module_function
8 
9 def sign(payload, secret, timestamp: Time.now.to_i)
10 digest = compute(payload, secret, timestamp)
11 "t=#{timestamp},v1=#{digest}"
12 end
13 
14 def verify(payload, header, secret, tolerance: DEFAULT_TOLERANCE)
15 parts = parse(header)
16 timestamp = Integer(parts.fetch("t"))
17 provided = parts.fetch("v1")
18 
19 raise "timestamp outside tolerance" if (Time.now.to_i - timestamp).abs > tolerance
20 
21 expected = compute(payload, secret, timestamp)
22 unless OpenSSL::Utils.secure_compare(expected, provided)
23 raise "signature mismatch"
24 end
25 
26 true
27 end
28 
29 def compute(payload, secret, timestamp)
30 signed = "#{timestamp}.#{payload}"
31 raw = OpenSSL::HMAC.digest("SHA256", secret, signed)
32 Base64.urlsafe_encode64(raw, padding: false)
33 end
34 
35 def parse(header)
36 header.split(",").each_with_object({}) do |pair, acc|
37 key, value = pair.strip.split("=", 2)
38 acc[key] = value
39 end
40 end
41end
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Including a timestamp in the signed material lets you reject replayed requests with a tolerance window.
  2. 2Signatures must be checked with a constant-time comparison to avoid leaking information through timing.
  3. 3Centralizing the signed-string format in one helper keeps signing and verifying provably in sync.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Signing and verifying webhooks in Ruby — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code