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
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.
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.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 })
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
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.Last modified 2yr ago