Verify Webhook Signature

Verify the events that Elements send to your webhook endpoints to avoid any unusual traffic to the endpoint.

Retrieve webhook signing secret

Before you can verify signature, go to Elements Merchant console to retrieve the webhook secrets, you will need to use the secret verify webhook events.

Verify the signature

Below is a webhook example

curl --location --request POST '<Your endpoint receive the webhook>' \
--header 'timestamp: 1650410593' \
--header 'signature: ngp+k/e0RuHY7fS9OeWFTfr1Wlh53rv87tCjqTwxWHg=' \
--header 'Content-Type: application/json' \
--data-raw '{
  "id": "CH-RSZ8dyzAmZHFGJ8m1XNDqRaX",
  "type": "charge",
  "status": "failed",
  "amount_subunit": 12345,
  "captured_amount_subunit": 12345,
  "currency": "USD",
  "metadata": null,
  "captured": true,
  "description": "test data",
  "created_at": 1621816399,
  "refunded": false,
  "disputed": true,
  "reference_id": null,
  "funding_source": null,
  "funding_source_details": {
    "expiration_month": null,
    "expiration_year": null
  },
  "psp_source_details": {
    "name": "StripeGateway"
  }
}'

Get the data used for verification - Get the timestamp value from webhook request header and raw request body json, concat them to a string.

timestamp = request.headers[:timestamp]
raw_request_body = JSON.parse(request.body.read)

data_for_hmac_verify = "#{timestamp}.#{raw_request_body.to_json}"

Calculate an HMAC using:

  1. The SHA256 function.
  2. Binary representation of the data_for_hmac_verify, given the UTF-8 charset.
  3. Binary representation of the HMAC key, given the UTF-8 charset.
  4. To get the final signature, Base64-encode the result.
raw_request_body = JSON.parse(request.body.read)
timestamp = request.headers[:timestamp]
signature = request.headers[:signature]

data_for_hmac_verify = "#{timestamp}.#{raw_request_body.to_json}"

calculated_hmac = Util::Hmac.calculate(
webhook_signing_secret,
data_for_hmac_verify,
)

## Util class to calculate HMAC value
module Util
  class Hmac
    DIGEST = OpenSSL::Digest.new('sha256')

    def self.calculate(key, data)
      hmac_key_hex = string_to_hex(key)
      hex = OpenSSL::HMAC.digest(DIGEST, hmac_key_hex, data)
      base64_encode(hex)
    end

    def self.string_to_hex(string)
      Array(string).pack('H*')
    end

    def self.base64_encode(string)
      Base64.strict_encode64 string
    end
  end
end

Compare with the signature

If the signature that you calculated above matches the signature from request header, you can confirm that the notification was sent by Elements and was not modified during transmission.