Module: GlobiGuard::TrustWebhook

Defined in:
lib/globiguard.rb

Class Method Summary collapse

Class Method Details

.secure_compare(left, right) ⇒ Object



319
320
321
322
# File 'lib/globiguard.rb', line 319

def secure_compare(left, right)
  return false unless left.bytesize == right.bytesize
  left.bytes.zip(right.bytes).reduce(0) { |memo, pair| memo | (pair[0] ^ pair[1]) }.zero?
end

.verify(headers, raw_body, signing_secret, tolerance_seconds: 300) ⇒ Object



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/globiguard.rb', line 303

def verify(headers, raw_body, signing_secret, tolerance_seconds: 300)
  normalized = headers.transform_keys { |key| key.to_s.downcase }
  delivery = normalized["x-globiguard-delivery-id"]
  timestamp = normalized["x-globiguard-timestamp"]
  event_type = normalized["x-globiguard-event-type"]
  signature = normalized["x-globiguard-signature"]
  return { ok: false, error: "Missing required webhook headers." } unless delivery && timestamp && event_type && signature
  return { ok: false, error: "Webhook timestamp is outside the replay window." } if (Time.now.to_i - timestamp.to_i).abs > tolerance_seconds

  signed = "globiguard-hmac-sha256-v1.#{delivery}.#{timestamp}.#{event_type}.#{raw_body}"
  expected = "v1=#{OpenSSL::HMAC.hexdigest("SHA256", signing_secret, signed)}"
  return { ok: false, error: "Invalid webhook signature." } unless secure_compare(expected, signature)

  { ok: true, envelope: JSON.parse(raw_body) }
end