-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
144 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# This is an example handler for Customer.io reporting webhooks. You | ||
# can find more documentation here https://customer.io/docs/api/webhooks/#operation/reportingWebhook | ||
class Webhooks::CustomerIoHandler < Munster::BaseHandler | ||
def process(webhook) | ||
json = JSON.parse(webhook.body, symbolize_names: true) | ||
case json[:metric] | ||
when "subscribed" | ||
# ... | ||
when "unsubscribed" | ||
# ... | ||
when "cio_subscription_preferences_changed" | ||
# ... | ||
end | ||
end | ||
|
||
def extract_event_id_from_request(action_dispatch_request) | ||
action_dispatch_request.params.fetch(:event_id) | ||
end | ||
|
||
# Verify that request is actually comming from customer.io here | ||
# @see https://customer.io/docs/api/webhooks/#section/Securely-Verifying-Requests | ||
# | ||
# - Should have "X-CIO-Signature", "X-CIO-Timestamp" headers. | ||
# - Combine the version number, timestamp and body delimited by colons to form a string in the form v0:<timestamp>:<body> | ||
# - Using HMAC-SHA256, hash the string using your webhook signing secret as the hash key. | ||
# - Compare this value to the value of the X-CIO-Signature header sent with the request to confirm | ||
def valid?(action_dispatch_request) | ||
signing_key = Rails.application.secrets.customer_io_webhook_signing_key | ||
xcio_signature = action_dispatch_request.headers["HTTP_X_CIO_SIGNATURE"] | ||
xcio_timestamp = action_dispatch_request.headers["HTTP_X_CIO_TIMESTAMP"] | ||
request_body = action_dispatch_request.body.read | ||
string_to_sign = "v0:#{xcio_timestamp}:#{request_body}" | ||
hmac = OpenSSL::HMAC.hexdigest("SHA256", signing_key, string_to_sign) | ||
Rack::Utils.secure_compare(hmac, xcio_signature) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# This is for Revolut V1 API for webhooks - https://developer.revolut.com/docs/business/webhooks-v-1-deprecated | ||
class RevolutBusinessV1Handler < Munster::BaseHandler | ||
def valid?(_) | ||
# V1 of Revolut webhooks does not support signatures | ||
true | ||
end | ||
|
||
def self.process(webhook) | ||
parsed_payload = JSON.parse(webhook.body) | ||
topic = parsed_payload.fetch("Topic") | ||
case topic | ||
when "tokens" # Account access revocation payload | ||
# ... | ||
when "draftpayments/transfers" # Draft payment transfer notification payload | ||
# ... | ||
else | ||
# ... | ||
end | ||
end | ||
|
||
def self.extract_event_id_from_request(action_dispatch_request) | ||
# Since b-tree indices generally divide from the start of the string, place the highest | ||
# entropy component at the start (the EventId) | ||
key_components = %w[EventId Topic Version] | ||
key_components.map do |key| | ||
action_dispatch_request.params.fetch(key) | ||
end.join("-") | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# This is for Revolut V2 API for webhooks - https://developer.revolut.com/docs/business/webhooks-v-2 | ||
class RevolutBusinessV2Handler < Munster::BaseHandler | ||
def valid?(request) | ||
# 1 - Validate the timestamp of the request. Prevent replay attacks. | ||
# "To validate the event, make sure that the Revolut-Request-Timestamp date-time is within a 5-minute time tolerance of the current universal time (UTC)". | ||
# Their examples list `timestamp = '1683650202360'` as a sample value, so their timestamp is in millis - not in seconds | ||
timestamp_str_from_headers = request.headers["HTTP_REVOLUT_REQUEST_TIMESTAMP"] | ||
delta_t_seconds = (timestamp_str_from_headers / 1000) - Time.now.to_i | ||
return false unless delta_t_seconds.abs < (5 * 60) | ||
|
||
# 2 - Validate the signature | ||
# https://developer.revolut.com/docs/guides/manage-accounts/tutorials/work-with-webhooks/verify-the-payload-signature | ||
string_to_sign = [ | ||
"v1", | ||
timestamp_str_from_headers, | ||
request.body.read | ||
].join(".") | ||
computed_signature = "v1=" + OpenSSL::HMAC.hexdigest("SHA256", Rails.application.secrets.revolut_business_webhook_signing_key, string_to_sign) | ||
# Note: "This means that in the period when multiple signing secrets remain valid, multiple signatures are sent." | ||
# https://developer.revolut.com/docs/guides/manage-accounts/tutorials/work-with-webhooks/manage-webhooks#rotate-a-webhook-signing-secret | ||
# https://developer.revolut.com/docs/guides/manage-accounts/tutorials/work-with-webhooks/about-webhooks#security | ||
# An HTTP header may contain multiple values if it gets sent multiple times. But it does mean we need to test for multiple provided | ||
# signatures in case of rotation. | ||
provided_signatures = request.headers["HTTP_REVOLUT_SIGNATURE"].split(",") | ||
# Use #select instead of `find` to compare all signatures even if only one matches - this to avoid timing leaks. | ||
# Small effort but might be useful. | ||
matches = provided_signatures.select do |provided_signature| | ||
ActiveSupport::SecurityUtils.secure_compare(provided_signature, computed_signature) | ||
end | ||
matches.any? | ||
end | ||
|
||
def self.process(webhook) | ||
Rails.logger.info { "Processing Revolut webhook #{webhook.body.inspect}" } | ||
end | ||
|
||
def self.extract_event_id_from_request(action_dispatch_request) | ||
# The event ID is only available when you retrieve the failed webhooks, which is sad. | ||
# We can divinate a synthetic ID though by taking a hash of the entire payload though. | ||
Digest::SHA256.hexdigest(action_dispatch_request.body.read) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# frozen_string_literal: true | ||
|
||
# This handler is an example for Starling Payments API, | ||
# you can find the documentation here https://developer.starlingbank.com/payments/docs#account-and-address-structure-1 | ||
class StarlingPaymentsHandler < Munster::BaseHandler | ||
# This method will be used to process webhook by async worker. | ||
def process(received_webhook) | ||
Rails.logger.info { received_webhook.body } | ||
end | ||
|
||
# Starling supplies signatures in the form SHA512(secret + request_body) | ||
def valid?(action_dispatch_request) | ||
supplied_signature = action_dispatch_request.headers.fetch("X-Hook-Signature") | ||
supplied_digest_bytes = Base64.strict_decode64(supplied_signature) | ||
sha512 = Digest::SHA2.new(512) | ||
signing_secret = Rails.credentials.starling_payments_webhook_signing_secret | ||
computed_digest_bytes = sha512.digest(signing_secret.b + action_dispatch_request.body.b) | ||
ActiveSupport::SecurityUtils.secure_compare(computed_digest_bytes, supplied_digest_bytes) | ||
end | ||
|
||
# Some Starling webhooks do not provide a notification UID, but for those which do we can deduplicate | ||
def extract_event_id_from_request(action_dispatch_request) | ||
action_dispatch_request.params.fetch("notificationUid", SecureRandom.uuid) | ||
end | ||
end |