Webhooks

Webhook Integration

Integration Steps

For integration you need:

  1. Create a public endpoint to receive HTTP POST requests using SSL.
  2. Configure the webhook and obtain an HMAC key in the Zentact admin panel under Configure, then Webhooks.
  3. 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.

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:

  1. Convert received HTTP body payload to a binary representation using UTF-8 charset.
  2. The HMAC key you received from Zentact is a hex string. Decode it from hex into bytes. Do not use the UTF-8 bytes of the text itself, or the signature will not match.
  3. 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.
  4. 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.

Compute the HMAC over the raw request body

Compute the signature over the exact bytes Zentact sent, which is the raw request body, before any JSON parsing or re-serialization. Parsing the body into an object and stringifying it again can reorder keys or change whitespace, which produces a different signature and makes verification fail. In Express, for example, capture the raw body (express.raw() or the verify callback) for the webhook route.

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) // the raw request body, for example from express.raw({ type: "application/json" })
    .digest("base64");

  const untrustedSignature = req.header("x-hmac-signature");
  if (!untrustedSignature) return false;
  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 base64
import hashlib
import hmac

from fastapi import HTTPException


def verify_signature(raw_body: bytes, hmac_secret_hex: str, signature_header: str):
    """Verify that the payload was sent from Zentact by validating SHA256.

    Raise 401 if not authorized.

    Args:
        raw_body: the raw request body bytes, before any JSON parsing (await request.body())
        hmac_secret_hex: the HMAC key from the Zentact admin portal (a hex string)
        signature_header: the x-hmac-signature header received from Zentact
    """
    if not signature_header:
        raise HTTPException(status_code=401, 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, raw_body, hashlib.sha256)

    # Generate the HMAC signature bytes
    generated_signature_bytes = hmac_obj.digest()

    if not hmac.compare_digest(generated_signature_bytes, provided_signature_bytes):
        raise HTTPException(status_code=401, detail="Request signatures didn't match!")
Previous
Introduction