cs-icon.svg

Secure Your Webhooks

Webhooks are an ideal way to send information automatically to an external application. However, it is critical to ensure that the receiving app or server validates the source before accepting requests. To avoid potential security threats, users can secure your webhooks.

Contentstack offers some highly recommended security measures that you can implement when setting up a webhook. These are “Basic Authentication,” “Custom Headers,” “Webhook Signature,” and “IP Whitelisting”.

Let’s look at the ways you can secure your webhook event data in detail.

Basic Authentication

When setting up a webhook, Basic Authentication, i.e., Basic Auth, allows users to set a username and password associated with your HTTP endpoint. With this method, your basic auth field values are included in the header of the HTTP request.

To set this method, go to Settings > Webhooks. Here, you can add the basic auth details by providing the values for the following fields:

  • HTTP basic auth username
  • HTTP basic auth password

Now, your URL is secure with the above basic auth username and password.

Custom Headers

As an additional method of security, you can specify custom headers that Contentstack will use while sending the payload to the specified endpoint. Custom Headers give the destination application an option to authenticate your webhook requests, and reject any that do not contain these custom headers.

Custom headers are key-value parameters that you send/receive in the header of each call of your notifying URL.

To set this method, log in to your Contentstack account, go to your stack, and perform the following steps:

  1. Click the “Settings” icon (press “S”) on the left navigation panel.
  2. Click Webhooks (press “Alt + W” for Windows OS, and “Option + W” for Mac OS). Here, you can add custom headers by providing the values for the following fields under ‘Custom headers’:
    • Key
    • Value

Note: You can set multiple custom header key-value pairs.

Webhook Signature

Contentstack signs all webhook events sent to your endpoints with a signature. This signature appears in each event's X-Contentstack-Request-Signature header. It allows you to verify the integrity of the data and the provider's authenticity (Contentstack) from which data is coming.

Verify webhook data that Contentstack sends to your webhook endpoints

Whenever a webhook is triggered for a specific event, Contentstack generates a signature based on the payload and appends it to the X-Contentstack-Request-Signature header of the HTTP request. This header is used while sending the payload to the specified webhook endpoint.

Note: Contentstack uses the SHA-256 algorithm and RSA algorithm based private key to generate webhook signatures.

Each signature is denoted by a unique identifier and prefixed with "v1=". Let us look at an example to understand the possible values for this response header.

X-Contentstack-Request-Signature :
v1=gk2f/Hzbm7TcNPs8g/AoKaGsK1yXaa5/EnEpNEzyQ67RElj09S

Note: Each webhook signature contains 256 characters in length.

Perform the following steps to check whether the webhook data comes from an authenticated source.

Step 1 - Retrieve the Public Key

To verify a webhook signature, you need to use the Contentstack Signing Public Key shared in the response. To obtain the public key, hit the below API endpoint:

`https://[DOMAIN]/.well-known/public-keys.json`

Note: Here, Domain refers to the host in the region-specific login endpoint that you are currently using to access the Contentstack app.

The above API endpoint returns the signing public key in the response body:

/// RESPONSE
const response = {
    "signing-key": "-----BEGIN RSA PUBLIC KEY-----\212313131\n-----END RSA PUBLIC KEY-----"
;
const publicKey = response["signing-key"];

Note: You can also store the content of the public key in a file for access whenever needed.

Step 2 - Extract the Webhook Signature from the Header

To extract the webhook signature from the response header, use the "," character as a separator and split the header. This will fetch a list of elements. Now, use the "=" character as a separator to split each element in the list and retrieve a prefix and integer value pair.

const signatureString = req.get('X-Contentstack-Request-Signature');
const signature = signatureString.split(",")[0].split("=")[1];
const body = req.body;

Step 3 - Verify the Webhook Signature

You can use the crypto.verify() method to verify the webhook signature attached to a specific webhook event. You need to pass the request body, signature, and public key within this method. Check the below example.

const hashAlgo = 'sha256';
const isVerified = crypto.verify(
  hashAlgo,
   Buffer.from(JSON.stringify(body)),
   {
     key: publicKey,
     padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
   },
   Buffer.from(signature, 'base64')
 );

The crypto.verify() method returns a boolean value, true, if verification is successful. If verification fails, it returns false. In case it’s false, reject the request.

Note: In case you fail to verify the webhook signature, use these parameters with their respective values:

  • Hash Algorithm: Name of the message digest (RSA-PSS).
    Value: sha256
  • Salt Length: RSA-PSS defines a default salt length that corresponds to the output length of the digest.
    Value: 222

Here is a sample codebase of what your verification script (NodeJS) should look like:

const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const HASH_ALGO = 'sha256';
const PORT = 3000;
const PUBLIC_KEY = importPublicKey();
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {  
  const signature = req.get('X-Contentstack-Request-Signature');
  const body = req.body;
  const isVerified = crypto.verify(
    HASH_ALGO,
    Buffer.from(JSON.stringify(body)),
    {
      key: PUBLIC_KEY,
      padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
    },
    Buffer.from(signature.split(',')[0].split('=')[1], 'base64')
  );
  if (isVerified) {
    console.log('verified!', body)
    res.json();
  } else {
    console.log('failed!')
    res.send('Unable to verify signature');
  }
});
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
function importPublicKey() {
  const publicKeyFile = fs.readFileSync('public.key', 'utf8');
  return crypto.createPublicKey({
    key: publicKeyFile,
    format: 'pem',
    type: 'pkcs1'
  });
}

Help prevent replay attacks

A replay attack occurs when an attacker repeatedly sends data to a specific webhook endpoint and overwhelms the third-party application. To help prevent such attacks, Contentstack attaches a timestamp in the request body. The timestamp is passed against the triggered_at key.

The triggered_at key generates new signatures every time Contentstack generates a new payload. This makes it difficult for attackers to decipher signatures and helps avoid replay attacks.

The triggered_at key denotes the timestamp at which a specific webhook event was triggered. You can compare the received timestamp to the current local timestamp to determine whether it is outside your defined tolerance. If the received timestamp exceeds the tolerance limits, your application can reject the request.

Here is a sample code that defines the signature and timestamp:

let receivedTimestamp = req.body['triggered_at'];
let localTimestamp = Date.now();
// in case the defined tolerance is 1 minute, 60*1000  milliseconds
if (localTimestamp - receivedTimestamp > 60000) {
// reject request
}

IP Whitelisting with Contentstack

IP whitelisting is another security feature that gives only an approved list of IP addresses the permission to access your domain(s).

To protect your domain from potential attacks, Contentstack will provide you with a specific set of IP addresses that you can whitelist. This will allow you to limit and control access only to trusted IPs and lets you verify whether the data is sent from Contentstack.

To receive the Contentstack IPs, contact our Support team today.

Additional Resource: You can also read on how to Pass Contentstack Webhooks through Firewall, in our detailed documentation. 

Was this article helpful?
^