Webhooks Verification

Overview

This tutorial is designed to walk you through the process of subscribing to webhook with webhook verification turned on.

👍

This is an optional setup, but we would recommend enabling it for security purposes.

Note: By default, while subscribing to webhook this feature is disabled. It's recommended to enable it in order to make sure the webhook comes from Cleeng and its payload wasn't modified by any malicious 3rd party during transmission.

Verification Mechanism

Cleeng provides a way to verify webhook based on Hash-based message authentication code(HMAC) signatures. During subscribing endpoint to webhook you can choose to enable it by providing configuration of this mechanism via verification field in the request body.

Verification configuration is a json object and looks following:

{
    "name": "HMAC",
    "options": {
        "hashAlg": "SHA256",
        "sharedSecret": "b/ds[]7+=43cnd54-12-95[sd^faas$e"
    }
}
ParameterDescription
nameName of webhook verification method. Currently only supported one is HMAC.
hashAlgHashing function that will be used to calculate HMAC signature. Currently only supported one is SHA256.
sharedSecretA secret key shared between Cleeng and publisher used for generating HMAC signature. Allowed length is between 16 and 64 bytes inclusive. For security, we recommend the length to be at least 32 bytes.

🚧

Value of sharedSecret is a secret between Cleeng and publisher. It should not be divulged to any 3rd party. It's publisher responsibility to store/remember its value. For security purposes we don't have any mechanism to display its value to you in case it was forgotten.

3 Steps to secure Webhooks

Step 1: Generate a random sequence of characters that will be used as sharedSecret and store it somewhere safe. The recommended length is at least 32 bytes.

Step 2: Subscribe an endpoint to webhook topic with verification field filled in. Documentation of the relevant endpoint can be found here.
Example request body:

[
    {
        "url": "https://example-endpoint-with-verification.com",
        "verification": {
            "name": "HMAC",
            "options": {
                "hashAlg": "SHA256",
                "sharedSecret": "b/ds[]7+=43cnd54-12-95[sd^faas$e"
            }
        }
    }
]

Step 3: Implement HMAC verification of received signature that comes with webhook to an endpoint. More about that topic can be found further in this tutorial.

Signature Verification

Once your endpoint is subscribed with verification turned on with each webhook special header X-Webhook-Signature will be sent that contains the webhook signature.

In order to verify received a signature on endpoint following steps must be implemented:

Calculate HMAC using your sharedSecret, SHA256 hashing function, and raw body of the received request.
Encode obtained HMAC to base64 format - that's computed signature
Compare signature from X-Webhook-Signature header with signature computed in the previous step. If they are the same it means that the webhook comes from Cleeng and its payload wasn't modified by any 3rd party.

Code Sample - Signature Verification

const crypto = require('crypto');

// Here goes value of sharedSecret you provided during webhook subscription step
const SHARED_SECRET = '...';

module.exports.handler = async event => {
    // Header containing webhook signature
    const sigHeader = event.headers['X-Webhook-Signature'];
    
    //Raw body of received request
    const rawBody = event.body;
    // Computed base64 encoded signature
    const sig = crypto.createHmac('sha256', SHARED_SECRET)
        .update(rawBody)
        .digest('base64');
    
    if (sig !== sigHeader) {
        // Webhook signature does not match.
        // It means that this webhook cannot be trusted and should be discarded
        return {
          statusCode: 200,
          body: JSON.stringify({ message: 'Webhook rejected!' }),
        };
    }
    
    const webhookPayload = JSON.parse(rawBody).data;
    // Webhook can be trusted, process webhook...
};
// Here goes value of sharedSecret you provided during webhook subscription step
define('SHARED_SECRET', '...');
 
//Raw body of received request
$rawBody = file_get_contents('php://input');
 
// Header containing webhook signature
$sigHeader = getallheaders()['X-Webhook-Signature'];
 
// Computed base64 encoded signature
$sig = base64_encode(hash_hmac('sha256', $rawBody, SHARED_SECRET, true));
 
// Webhook signature does not match.
// It means that this webhook cannot be trusted and should be discarded        
if ($sig !== $sigHeader) {
    return;
}
 
$webhookPayload = json_decode($rawBody, true)['data'];
// Webhook can be trusted, process webhook...