class GoCardlessPro::Webhook

Public Class Methods

parse(options = {}) click to toggle source

Validates that a webhook was genuinely sent by GoCardless using `.signature_valid?`, and then parses it into an array of `GoCardlessPro::Resources::Event` objects representing each event included in the webhook

@option options [String] :request_body the request body @option options [String] :signature_header the signature included in the request,

found in the `Webhook-Signature` header

@option options [String] :webhook_endpoint_secret the webhook endpoint secret for

your webhook endpoint, as configured in your GoCardless Dashboard

@return [Array<GoCardlessPro::Resources::Event>] the events included

in the webhook

@raise [InvalidSignatureError] if the signature header specified does not match

the signature computed using the request body and webhook endpoint secret

@raise [ArgumentError] if a required keyword argument is not provided or is not

of the required type
# File lib/gocardless_pro/webhook.rb, line 24
def parse(options = {})
  validate_options!(options)

  unless signature_valid?(request_body: options[:request_body],
                          signature_header: options[:signature_header],
                          webhook_endpoint_secret: options[:webhook_endpoint_secret])
    raise InvalidSignatureError, "This webhook doesn't appear to be a genuine " \
                                  'webhook from GoCardless, because the signature ' \
                                  "header doesn't match the signature computed" \
                                  ' with your webhook endpoint secret.'
  end

  events = JSON.parse(options[:request_body])['events']

  events.map { |event| Resources::Event.new(event) }
end
signature_valid?(options = {}) click to toggle source

Validates that a webhook was genuinely sent by GoCardless by computing its signature using the body and your webhook endpoint secret, and comparing that with the signature included in the `Webhook-Signature` header

@option options [String] :request_body the request body @option options [String] :signature_header the signature included in the request,

found in the `Webhook-Signature` header

@option options [String] :webhook_endpoint_secret the webhook endpoint secret for

your webhook endpoint, as configured in your GoCardless Dashboard

@return [Boolean] whether the webhook's signature is valid @raise [ArgumentError] if a required keyword argument is not provided or is not

of the required type
# File lib/gocardless_pro/webhook.rb, line 53
def signature_valid?(options = {})
  validate_options!(options)

  computed_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'),
                                               options[:webhook_endpoint_secret],
                                               options[:request_body])

  secure_compare(options[:signature_header], computed_signature)
end

Private Class Methods

secure_compare(a, b) click to toggle source

Performs a “constant time” comparison of two strings, safe against timing attacks

Vendored from Rack's `Rack::Utils.secure_compare` (github.com/rack/rack/blob/eb040cf1bbb1b2dacd496ab0aa549de8408d8a27/lib/rack/utils.rb#L368-L382). Licensed under The MIT License (MIT). Copyright (C) 2007-2018 Christian Neukirchen.

# File lib/gocardless_pro/webhook.rb, line 71
def secure_compare(a, b)
  return false unless a.bytesize == b.bytesize
  l = a.unpack('C*')

  r = 0
  i = -1
  b.each_byte { |v| r |= v ^ l[i += 1] }
  r == 0
end
validate_options!(options) click to toggle source
# File lib/gocardless_pro/webhook.rb, line 81
def validate_options!(options)
  unless options[:request_body].is_a?(String)
    raise ArgumentError, 'request_body must be provided and must be a string'
  end

  unless options[:signature_header].is_a?(String)
    raise ArgumentError, 'signature_header must be provided and must be a string'
  end

  unless options[:webhook_endpoint_secret].is_a?(String)
    raise ArgumentError, 'webhook_endpoint_secret must be provided and must be a ' \
                         'string'
  end
end