module Recurly::Webhooks

Constants

DEFAULT_TOLERANCE

Public Class Methods

verify_signature(header, secret, body, tolerance: DEFAULT_TOLERANCE) click to toggle source

Verify webhook signature

@param header [String] recurly-signature header from request @param secret [String] Shared secret for notification endpoint @param body [String] Request POST body @param tolerance [Integer] Allowed notification time drift in milliseconds @example

begin
  Recurly::Webhooks.verify_signature(header,
                                     ENV['WEBHOOKS_KEY'],
                                     request.body)
rescue Recurly::Errors::SignatureVerificationError => e
  puts e.message
end
# File lib/recurly/webhooks.rb, line 20
def self.verify_signature(header, secret, body, tolerance: DEFAULT_TOLERANCE)
  s_timestamp, *signatures = header.split(",")
  timestamp = Integer(s_timestamp)
  now = (Time.now.to_f * 1000).to_i

  if (now - timestamp).abs > tolerance
    raise Recurly::Errors::SignatureVerificationError.new(
      "Notification (#{Time.at(timestamp / 1000.0)}) is more than #{tolerance / 1000.0}s out of date"
    )
  end

  expected = OpenSSL::HMAC.hexdigest("sha256", secret, "#{timestamp}.#{body}")

  unless signatures.any? { |s| secure_compare(expected, s) }
    raise Recurly::Errors::SignatureVerificationError.new(
      "No matching signatures found for payload"
    )
  end
end

Private Class Methods

secure_compare(a, b) click to toggle source

github.com/rack/rack/blob/2-2-stable/lib/rack/utils.rb#L374 github.com/heartcombo/devise/blob/4-1-stable/lib/devise.rb#L477

# File lib/recurly/webhooks.rb, line 42
def self.secure_compare(a, b)
  return false if a.bytesize != b.bytesize
  l = a.unpack("C#{a.bytesize}")

  res = 0
  b.each_byte { |byte| res |= byte ^ l.shift }
  res == 0
end