module DuffelAPI::WebhookEvent

Constants

SHA_256
SIGNATURE_REGEXP

Public Class Methods

genuine?(request_body:, request_signature:, webhook_secret:) click to toggle source

Checks if a webhook event you received was a genuine webhook event from Duffel by checking that it was signed with your shared secret.

Assuming that you’ve kept that secret secure and only shared it with Duffel, this can give you confidence that a webhook event was genuinely sent by Duffel.

@param request_body [String] The raw body of the received request @param request_signature [String] The signature provided with the received

request, found in the `X-Duffel-Signature` request header

@param webhook_secret [String] The secret of the webhook, registered with Duffel @return [Boolean] whether the webhook signature matches

# File lib/duffel_api/webhook_event.rb, line 28
def genuine?(request_body:, request_signature:, webhook_secret:)
  parsed_signature = parse_signature!(request_signature)

  calculated_hmac = calculate_hmac(
    payload: request_body,
    secret: webhook_secret,
    timestamp: parsed_signature[:timestamp],
  )

  secure_compare(calculated_hmac, parsed_signature[:v1])
rescue InvalidRequestSignatureError
  # If the signature doesn't even look like a valid one, then the webhook
  # event can't be genuine
  false
end

Private Class Methods

calculate_hmac(secret:, payload:, timestamp:) click to toggle source

Calculates the signature for a request body in the same way that the Duffel API does it

@param secret [String] @param payload [String] @param timestamp [String] @return [String]

# File lib/duffel_api/webhook_event.rb, line 53
def calculate_hmac(secret:, payload:, timestamp:)
  signed_payload = %(#{timestamp}.#{payload})
  Base16.encode16(OpenSSL::HMAC.digest(SHA_256, secret,
                                       signed_payload)).strip.downcase
end
parse_signature!(signature) click to toggle source

Parses a webhook signature and extracts the ‘v1` and `timestamp` values, if available.

@param signature [String] A webhook event signature received in a request @return [Hash] @raise InvalidRequestSignatureError when the signature isn’t valid

# File lib/duffel_api/webhook_event.rb, line 65
def parse_signature!(signature)
  matches = signature.match(SIGNATURE_REGEXP)

  if matches
    {
      v1: matches[2],
      timestamp: matches[1],
    }
  else
    raise InvalidRequestSignatureError
  end
end
secure_compare(a, b) click to toggle source

Checks if two strings are equal, performing a constant time string comparison resistant to timing attacks.

@param a [String] @param b [String] @return [Boolean] whether the two strings are equal rubocop:disable Naming/MethodParameterName

# File lib/duffel_api/webhook_event.rb, line 91
def secure_compare(a, b)
  # rubocop:enable Naming/MethodParameterName
  return false unless a.bytesize == b.bytesize

  OpenSSL.fixed_length_secure_compare(a, b)
end