Event Signature Validation
How to validate the `X-Honeybee-Signature` header with common language examples.
All the examples here can be found on here on Github.
Avoiding "gotchas" for validating the webhook signature:
Our current implementation for generating a signature will encode a payload following the Common Gateway Interface (CGI) specification as defined in IETF RFC 3875. The encodeURIComponent function in javascript will encode a payload following the Uniform Resource Identifier (URI) syntax specification as defined in IETF RFC 3986.
The main difference in the encoding formats for our payloads is how spaces are encoded. The encodeURIComponent function will encode a space as %20
, whereas our CGI implementation will encode a space as +
.
Ruby
require 'cgi'
require 'base64'
require 'openssl'
require 'json'
request = {
url: "https://webhook.site/bafb9675-e60a-4db3-ab3c-377061a12ed5",
method: "POST",
body: {
"event_id": "5d62dca6-1ce3-4807-afc6-8cfaa47577c5",
"event_type": "RX_RECEIVED",
"patient_id": "v3y4od",
"medication_requests": [
{
"id": 82,
"prescription_id": 82,
"active": true,
"patient_id": "v3y4od",
"prescriber_name": "Dr. Jane Foster",
"receive_date": "2023-05-10T16:16:58.469+00:00",
"drug_name": "ATORVASTATIN 10MG TABLET",
"ndc": "5976201551",
"sig_text": "Take 1 tablet daily",
"written_qty": 90,
"days_supply": nil,
"refills_left": 2,
"expire_date": "2023-06-12T14:15:22Z",
"drug_schedule": 4
}
]
}
}
CLIENT_SECRET = "[CLIENT_SECRET_HERE]"
HASHED_CLIENT_SECRET = Digest::SHA256.hexdigest(CLIENT_SECRET)
base = CGI.escape(request[:method] + request[:url] + (request[:body].to_json))
digest = Base64.encode64("#{OpenSSL::HMAC.digest('sha1', HASHED_CLIENT_SECRET, base)}\n")
puts digest
Python
"""Example illustrating how to validate webhook event signature in Python"""
import hashlib
import hmac
import json
from base64 import b64encode
import urllib.parse
request = {
"url": "https://webhook.site/bafb9675-e60a-4db3-ab3c-377061a12ed5",
"method": "POST",
"body": {
"event_id": "5d62dca6-1ce3-4807-afc6-8cfaa47577c5",
"event_type": "RX_RECEIVED",
"patient_id": "v3y4od",
"medication_requests": [
{
"id": 82,
"prescription_id": 82,
"active": True,
"patient_id": "v3y4od",
"prescriber_name": "Dr. Jane Foster",
"receive_date": "2023-05-10T16:16:58.469+00:00",
"drug_name": "ATORVASTATIN 10MG TABLET",
"ndc": "5976201551",
"sig_text": "Take 1 tablet daily",
"written_qty": 90,
"days_supply": None,
"refills_left": 2,
"expire_date": "2023-06-12T14:15:22Z",
"drug_schedule": 4,
}
],
},
}
CLIENT_SECRET = "[CLIENT_SECRET_HERE]"
HASHED_CLIENT_SECRET = hashlib.sha256(CLIENT_SECRET.encode("utf-8")).hexdigest()
request_str = json.dumps(request["body"], separators=(",", ":"))
base = urllib.parse.quote_plus(
request["method"] + request["url"] + request_str, safe=""
)
digest = hmac.digest(
HASHED_CLIENT_SECRET.encode("utf-8"), base.encode("utf-8"), hashlib.sha1
)
print(b64encode(digest + b"\n").decode())
Node.js
const crypto = require('crypto');
const querystring = require('querystring');
const request = {
url: "https://webhook.site/bafb9675-e60a-4db3-ab3c-377061a12ed5",
method: "POST",
body: {
"event_id": "5d62dca6-1ce3-4807-afc6-8cfaa47577c5",
"event_type": "RX_RECEIVED",
"patient_id": "v3y4od",
"medication_requests": [
{
"id": 82,
"prescription_id": 82,
"active": true,
"patient_id": "v3y4od",
"prescriber_name": "Dr. Jane Foster",
"receive_date": "2023-05-10T16:16:58.469+00:00",
"drug_name": "ATORVASTATIN 10MG TABLET",
"ndc": "5976201551",
"sig_text": "Take 1 tablet daily",
"written_qty": 90,
"days_supply": null,
"refills_left": 2,
"expire_date": "2023-06-12T14:15:22Z",
"drug_schedule": 4,
}
],
},
};
const CLIENT_SECRET = "[CLIENT_SECRET_HERE]";
const HASHED_CLIENT_SECRET = crypto.createHash('sha256').update(CLIENT_SECRET).digest('hex');
const requestStr = JSON.stringify(request.body);
const base = querystring.escape(request.method + request.url + requestStr).replace(/%20/g, '+');
const hmac = crypto.createHmac('sha1', HASHED_CLIENT_SECRET);
hmac.update(base);
const digest = Buffer.from(hmac.digest('binary') + '\n', 'binary').toString('base64')
console.log(digest);
Go
package main
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
)
type RawRequest struct {
Url string
Method string
Body string
}
type RequestBody struct {
EventID string `json:"event_id"`
EventType string `json:"event_type"`
PatientID string `json:"patient_id"`
MedicationRequests []MedicationRequest `json:"medication_requests"`
}
type MedicationRequest struct {
ID int `json:"id"`
PrescriptionID int `json:"prescription_id"`
Active bool `json:"active"`
PatientID string `json:"patient_id"`
PrescriberName string `json:"prescriber_name"`
ReceiveDate string `json:"receive_date"`
DrugName string `json:"drug_name"`
NDC string `json:"ndc"`
SigText string `json:"sig_text"`
WrittenQty int `json:"written_qty"`
DaysSupply interface{} `json:"days_supply"`
RefillsLeft int `json:"refills_left"`
ExpireDate string `json:"expire_date"`
DrugSchedule int `json:"drug_schedule"`
}
func main() {
request := RawRequest{
Url: "https://webhook.site/bafb9675-e60a-4db3-ab3c-377061a12ed5",
Method: "POST",
Body: `{
"event_id": "5d62dca6-1ce3-4807-afc6-8cfaa47577c5",
"event_type": "RX_RECEIVED",
"patient_id": "v3y4od",
"medication_requests": [
{
"id": 82,
"prescription_id": 82,
"active": true,
"patient_id": "v3y4od",
"prescriber_name": "Dr. Jane Foster",
"receive_date": "2023-05-10T16:16:58.469+00:00",
"drug_name": "ATORVASTATIN 10MG TABLET",
"ndc": "5976201551",
"sig_text": "Take 1 tablet daily",
"written_qty": 90,
"days_supply": null,
"refills_left": 2,
"expire_date": "2023-06-12T14:15:22Z",
"drug_schedule": 4
}
]
}`,
}
clientSecret := "[CLIENT_SECRET_HERE]"
hashedClientSecret := sha256.Sum256([]byte(clientSecret))
hashedClientSecretStr := hex.EncodeToString(hashedClientSecret[:])
flattenedBody, err := flattenJson(request.Body)
if err != nil {
panic(err)
}
base := url.QueryEscape(request.Method + request.Url + flattenedBody)
hash := hmac.New(sha1.New, []byte(hashedClientSecretStr))
hash.Write([]byte(base))
digest := hash.Sum(nil)
fmt.Println(base64.StdEncoding.EncodeToString(append(digest, '\n')))
}
func flattenJson(jsonStr string) (string, error) {
var data RequestBody
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return "", err
}
body, err := json.Marshal(data)
if err != nil {
return "", err
}
return string(body), nil
}
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import java.util.Base64;
import java.util.Map;
public class Main {
public static void main(String[] args)
throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException,
ArrayStoreException, NullPointerException {
Map<String, String> request = Map.of(
"url", "https://webhook.site/bafb9675-e60a-4db3-ab3c-377061a12ed5",
"method", "POST",
"body",
"{\"event_id\":\"5d62dca6-1ce3-4807-afc6-8cfaa47577c5\",\"event_type\":\"RX_RECEIVED\",\"patient_id\":\"v3y4od\",\"medication_requests\":[{\"id\":82,\"prescription_id\":82,\"active\":true,\"patient_id\":\"v3y4od\",\"prescriber_name\":\"Dr. Jane Foster\",\"receive_date\":\"2023-05-10T16:16:58.469+00:00\",\"drug_name\":\"ATORVASTATIN 10MG TABLET\",\"ndc\":\"5976201551\",\"sig_text\":\"Take 1 tablet daily\",\"written_qty\":90,\"days_supply\":null,\"refills_left\":2,\"expire_date\":\"2023-06-12T14:15:22Z\",\"drug_schedule\":4}]}");
String CLIENT_SECRET = "[CLIENT_SECRET_HERE]";
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(CLIENT_SECRET.getBytes(StandardCharsets.UTF_8));
String HASHED_CLIENT_SECRET = bytesToHex(hash);
String encodedUrl = URLEncoder.encode(request.get("url"), StandardCharsets.UTF_8.toString());
String encodedMethod = URLEncoder.encode(request.get("method"), StandardCharsets.UTF_8.toString());
String encodedBody = URLEncoder.encode(request.get("body"), StandardCharsets.UTF_8.toString());
String base = encodedMethod + encodedUrl + encodedBody;
Mac sha1_HMAC = Mac.getInstance("HmacSHA1");
SecretKeySpec secret_key = new SecretKeySpec(HASHED_CLIENT_SECRET.getBytes(), "HmacSHA1");
sha1_HMAC.init(secret_key);
byte[] hashBase = sha1_HMAC.doFinal(base.getBytes());
byte[] hashBaseWithNewline = new byte[hashBase.length + 1];
System.arraycopy(hashBase, 0, hashBaseWithNewline, 0, hashBase.length);
hashBaseWithNewline[hashBase.length] = '\n';
String base64HashBase = Base64.getEncoder().encodeToString(hashBaseWithNewline);
System.out.println(base64HashBase);
}
private static String bytesToHex(byte[] hash) {
StringBuilder hexString = new StringBuilder(2 * hash.length);
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}