Links

Verifying Webhook Signatures

Verify authenticity, data integrity, and prevent replay attacks.
A Next_Tech_Signature header is sent in each webhook request. The value of the header has a format of t=<timestamp>,v1=<HMAC>, e.g.:
t=1612334274,v1=2b1d3da7000d832c32b1d4e7f04523f7e6d735f22cf2aa55f9e091ed2d8b207e

Timestamp

The timestamp is the time the webhook was sent. It is a good idea to check that this time is less than 60 seconds before the current time. A timestamp that is any older may indicate the message is being replayed. To verify the timestamp has not been modified, compute and compare the HMAC.

HMAC

An HMAC is a keyed-hash message authentication code. It allows verification of the authenticity of the webhook as well as the integrity of the webhook body (and timestamp). This is because both the message body and the timestamp are included in the calculation of the HMAC. The HMAC uses the SHA256 hash function and is keyed with your account secret key. The message passed to the hash function is the concatenation of the timestamp (from the header), a ., and the webhook POST body converted to a JSON string.

Verifying the Webhook Signature in Python

The following snippet shows how to verify the signature in Python using the Flask framework:
import time
import hashlib
import hmac
import json
from flask import jsonify
def req_handler(request):
received_at = time.time()
request_json = json.dumps(request.get_json(), separators=(',', ':'))
sig_header = list(map(lambda p: p.split('='), request.headers.get('Next-Tech-Signature').split(',')))
timestamp = sig_header[0][1]
received_hash = sig_header[1][1]
computed_hash = hmac.new(b'<ACCOUNT_SECRET_KEY>', f'{timestamp}.{request_json}').encode('utf-8'), hashlib.sha256).hexdigest()
hash_matches = received_hash == computed_hash
timestamp_valid = received_at - int(timestamp) < 60
return jsonify({ "hash_matches": hash_matches, "timestamp_valid": timestamp_valid })

Verifying the Webhook Signature in Ruby

The following snippet shows how to verify the signature in Ruby with a Rack::Request. The snippet was created and tested in the Google Cloud Functions Ruby environment:
require "functions_framework"
require "json"
# This function receives an HTTP request of type Rack::Request
# and interprets the body as JSON.
FunctionsFramework.http "req_handler" do |request|
received_at = Time.now.getutc.to_i
input = JSON.parse request.body.read rescue {}
sig_header = request.get_header('HTTP_NEXT_TECH_SIGNATURE').split(',').map { |p| p.split('=') }
timestamp = sig_header[0][1]
received_hash = sig_header[1][1]
computed_hash = OpenSSL::HMAC.hexdigest(
'SHA256',
"<ACCOUNT_SECRET_KEY>",
"#{timestamp}.#{input.to_json.gsub(/\\u0026/, '&').gsub(/\\u003e/, '>').gsub(/\\u003c/, '<')}"
)
hash_matches = received_hash == computed_hash
timestamp_valid = received_at - timestamp.to_i < 60
{ hash_matches: hash_matches, timestamp_valid: timestamp_valid }
end

Verifying the Webhook Signature in Node.js/JavaScript

Unfortunately, verifying the signature in Node.js/JavaScript is currently not supported. This is because when JSON.stringify() is used to convert the webhook POST body to a JSON string, floating point values ending in trailing zeros, e.g. (1.0) have the trailing zeros removed, e.g.(1). This changes the webhook body and results in a different HMAC code being calculated.