Webhook Signature Verification
Overview
Webhook signature verification is a security mechanism used to ensure that data received by a server from a webhook is legitimate and has not been tampered with during transmission. This process involves the webhook provider (Hover) adding a signature to the webhook payload. The receiving server (the service consuming the webhook) then verifies this signature using a shared secret or public key to confirm the authenticity and integrity of the payload.
How To Generate a Signature
Saving the HMAC Secret
When you successfully register a webhook with Hover, the response body will includehmac_secret
value. This value will should be stored as it will be needed to generate the HMAC signature.
This HMAC Secret should be kept private.
//Response Body for a successful POST request to https://hover.to/api/v2/webhooks
{
"id": 12343,
"owner_type": "Org",
"owner_id": 55555,
"url": "https://yourwebhook.com/hoverwebhook",
"created_at": "2023-09-11T16:39:01.906Z",
"updated_at": "2023-09-11T16:39:01.906Z",
"verified_at": null,
//store the hmac_secret
"hmac_secret": "NDYBduht2zrCg6fMXFWadfsT8fT3gQskoFPmRen7KcksDQBqmrpXyx12340ObRqLLjQxBxrTol/lia3WRHtQA==",
"last_error": null,
"content_type": "json"
}
Using this hmac_secret
value, your integration can recursively generate a signature for all webhook payloads coming from Hover. If the generated signature matches the HMAC signature included in the header of each webhook event, you can verify that the webhook is coming from Hover.
Generating the Signature
- Create a canonical string using the HTTP headers from the incoming webhook, utilizing the
Content-Type
,Content-MD5
,Request URI
and theDATE
timestamp. Ifcontent-type
orcontent-MD5
are not present, then a blank string can be used in their place. If the timestamp isn't present, a valid HTTP date is automatically added to the request. - The canonical string string is computed as
canonical_string = 'content-type, content-MD5, ,timestamp'
. Here is an example of what this computation could look like:
// these values are for demonstration purposes only and may not reflect actual values
webhook_url = url = "https://c452-2600.ngrok-free.app/webhooks/hover"
incoming_request.content_type = 'application/json'
incoming_request['DATE'] = 'Tue, 06 Aug 2024 23:15:50 GMT'
incoming_request['Content-MD5'] = "QpvEO+siRXYz123n2w3nF6A=="
canonical_string = 'content-type, content-MD5, ,timestamp'
// sample canonical_string output:
//'application/json,B0WWTAASYKYPURFJP8hS3w==, https://c452-2600.ngrok-free.app/webhooks/hover , Tue, 06 Aug 2024 23:15:50 GMT'
- Use the
canonical_string
to create the signature, which is a Base64-encoded SHA1 HMAC, using your privatehmac_secret
key. - Confirm that the signature included in the incoming webhook header matches the one you generated. Hover uses the same process to generate the signature on our outbound webhooks. If they match, you can verify the webhook came from Hover.
Here is a sample webhook signature generated from Hover will include the webhook ID value, followed by the HMAC signature:
APIAuth 14845:JHYDXZt4St4D4wSz2tbiNSjG5UY=
Webhook Signature Code Example
This code can be used as a template to construct the logic to generate a webhook signature and compare the value with the signature generated by Hover. This is for demonstration purposes only.
require 'net/http'
require 'digest/md5'
require 'base64'
require 'json'
require 'openssl'
# The incoming request. This is based off of a sample request payload that is sent from Hover to your webhook listener.
#your webhook URL
url = "https://c452-2600.ngrok-free.app/webhooks/hover"
uri = URI.parse(url)
puts uri
null = nil
incoming_request = Net::HTTP::Post.new(uri)
incoming_request.content_type = 'application/json'
#sample request body sent by HOVER that you want to verify
incoming_request.body = '{
"event": "webhook-verification-code",
"code": "829924ae-36ba-4190-a013-1f66a276395e",
"webhook_id":55555
}'
#date from the webhook event headers
incoming_request['DATE'] = 'Tue, 06 Aug 2024 23:15:50 GMT'
#Content-MD5 from webhook event headers
incoming_request['Content-MD5'] = "QpvEO+abc123XYz123n2w3nF6A=="
#Authorization header from webhook event headers
incoming_request['Authorization'] = "APIAuth 14845:Gabc123JYvsqxVR5AW82Cj+fU="
# A function to authenticate incoming request---
def authenticate_webhook(request, hmac_id, hmac_secret)
canonical_string = [
request.content_type,
Digest::MD5.base64digest(request.body),
request.path,
request['DATE']
].join(",")
puts canonical_string
digest = OpenSSL::Digest.new('sha1')
signature = Base64.strict_encode64(OpenSSL::HMAC.digest(digest, hmac_secret, canonical_string))
header = "APIAuth #{hmac_id}:#{signature}"
return request['Authorization'] == header
end
# Use the authentication function
#HMAC secret from the response body from when you initially create the webhook with HOVER
hmac_secret = "1eQAEqq3knIllqU+/38PBhD62HdAJMZ36+kSabc123456xcyqzMgSrow4mTYhhQ6tQxVJVTgF0NBlWe108ryw=="
#hmac_id == webhook ID
hmac_id = 55555
result = authenticate_webhook(incoming_request, hmac_id, hmac_secret)
puts result
Updated about 2 months ago