Skip to main content

Callback Signing

Callback signing is a method to secure your webhook endpoints from foreign requests by verifying that the incoming requests are genuine and originate from a trusted source.

Overview

When you receive a webhook notification, it is crucial to validate that the request has not been tampered with. This is achieved by computing a SHA-256 HMAC of the entire HTTP body content using a shared callback signing key. This computed HMAC should match the value sent in the Idenfy-Signature header.

The signing key is set for each webhook notification individually by navigating to Settings -> Notifications. You'll find the Signing key input field under the Headers section when opening/editing the notification itself.

Important Considerations for Middleware Setup

When setting up your webhook listener, it is essential to use the correct middleware to ensure that the raw body content of the request is preserved. This is necessary for accurate HMAC computation.

Use express.raw() for Raw Body Handling in Node.js

In Node.js applications, using the wrong middleware can cause validation to fail due to modified request bodies. Ensure you use express.raw() instead of express.json() or express.urlencoded().

Common Middleware Issue
  • Incorrect Middleware:

    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));

    Why it's Incorrect:
    The express.json() and express.urlencoded() middlewares parse the incoming request body and convert it into a JSON object or URL-encoded string. This transformation changes the original body content, causing the HMAC digest computed from it to differ from the value sent in the Idenfy-Signature header.

  • Correct Middleware:

    app.use(express.raw({ type: '*/*' }));

    Why it's Correct:
    The express.raw() middleware ensures that the request body is passed as a raw buffer without any transformations, preserving its integrity for accurate HMAC computation.

Security Notice

You should not use the standard == operator to compare HMAC digests, as it is susceptible to timing attacks. Instead, you should use hmac.compare_digest, crypto.timingSafeEqual, or an equivalent constant-time string comparison method to avoid such vulnerabilities.

Additionally, make sure that webhook notifications are not passing through a proxy and are not being modified by a load balancer or firewall in the destination API. Any proxy or intermediary that alters the payload (even slightly) will cause checksum differences, resulting in failed validations.

Common Errors and How to Troubleshoot

If you encounter errors when validating webhook signatures, review the following troubleshooting steps:

  1. Signature Mismatch:
    If you see an error message similar to Request body digest did not match Idenfy-Signature header, it is likely that the request body was altered by middleware or a proxy server. Use express.raw() to prevent this issue.

  2. Incorrect Signature Length:
    If you receive an error like ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH, ensure that the Idenfy-Signature header and computed HMAC digest have the same length before comparing them. This error often occurs when the signature or digest is formatted incorrectly (e.g., hex vs. base64 encoding).

  3. Missing Headers or Body:
    Verify that the request includes the Idenfy-Signature header and that the body content is not empty. If either of these is missing, return a 400 Bad Request response with an appropriate error message.

Proxy and Middleware Interference

Additionally, make sure that webhook notifications are not passing through a proxy and are not being validated by the destination API. A proxy might modify the payload, which will result in checksum differences and cause the validation to fail.

Example Implementations

Below are example implementations in Python and JavaScript for validating the webhook signature. Ensure that you adapt these examples to use the correct middleware setup as described above.

import hmac

CALLBACK_SIGNING_KEY = "Agegb..."

@app.route('/idenfy-callback')
def callback():
signature = hmac.new(
CALLBACK_SIGNING_KEY.encode(),
request.get_data(),
"sha256"
).hexdigest()

request_signature = request.headers.get("Idenfy-Signature")

if request_signature:
if hmac.compare_digest(signature, request_signature):
handle_request(request)