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

  1. Create a canonical string using the HTTP headers from the incoming webhook, utilizing the Content-Type, Content-MD5, Request URI and the DATE timestamp. If content-type or content-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.
  2. 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'


  1. Use the canonical_string to create the signature, which is a Base64-encoded SHA1 HMAC, using your private hmac_secret key.
  2. 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