Webhooks
Webhooks Authentication
Integration Steps
For integration you need:
- Create a public endpoint to receive HTTP POST requests using SSL.
- Add webhook HMAC signature verification logic. When comparing signatures, don’t use simple string comparison, as it is susceptible to timing attacks. Instead, use a safe equals version in your framework of choice. An example is provided below.
- Configure the webhook and obtain an HMAC key in the Zentact admin panel under Configure -> Webhooks.
Signature Validation
To protect your server from unauthorised webhook events, you must verify that the message is authentic by using Hash-based message authentication code (HMAC) signatures. Each webhook event will include a signature calculated using a secret HMAC key and a payload from the webhook. By verifying this signature, you confirm that the webhook was sent by Zentact, and was not modified during transmission.
To build HMAC validation function perform the following steps:
- Convert received HTTP body payload to a binary representation using UTF-8 charset.
- Convert the HMAC key that you received from Zentact to a binary representation, given the UTF-8 charset.
- Calculate HMAC using cryptography library in your language of choice using: The SHA256 function. The binary representation of the payload. The binary representation of the HMAC key.
- To get the final signature, Base64-encode the result. Compare the signatures generated in step 3 and from received
x-hmac-signature
HTTP header using Timing attack safe comparison method.
TypeScript example:
import * as crypto from "crypto";
const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET;
const verify_signature = (req: Request) => {
const trustedSignature = crypto
.createHmac("sha256", Buffer.from(WEBHOOK_SECRET, 'hex'))
.update(req.body, 'utf8')
.digest("base64");
const untrustedSignature = req.headers.get("x-hmac-signature");
return return stringsSafeEqual(trustedSignature, untrustedSignature);
};
const stringsSafeEqual = (a, b) => {
return a.length === b.length && crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
};
const handleWebhook = (req: Request, res: Response) => {
if (!verify_signature(req)) {
res.status(401).send("Unauthorized");
return;
}
// The rest of your logic here
};
Python Example
import hashlib
import hmac
def verify_signature(data, hmac_secret_hex, signature_header):
"""Verify that the payload was sent from Zentact by validating SHA256.
Raise and return 403 if not authorized.
Args:
payload_body: original request body to verify (request.body())
secret_token: Zentact app webhook token (WEBHOOK_SECRET)
signature_header: header received from Zentact (x-hmac-signature)
"""
if not signature_header:
raise HTTPException(status_code=403, detail="x-hmac-signature header is missing!")
# Decode the hex encoded secret to bytes
secret_key = bytes.fromhex(hmac_secret_hex)
# Decode the provided base64 signature to bytes
provided_signature_bytes = base64.b64decode(signature_header)
# Create a new HMAC object using the secret key and the SHA256 hash function
hmac_obj = hmac.new(secret_key, data.encode(), hashlib.sha256)
# Generate the HMAC signature and encode it in base64
generated_signature_bytes = hmac_obj.digest()
if not hmac.compare_digest(generated_signature_bytes, provided_signature_bytes):
raise HTTPException(status_code=403, detail="Request signatures didn't match!")