Was this article helpful?
Thanks for your feedback
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.
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:
Now, your URL is secure with the above basic auth username and password.
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:
Note: You can set multiple custom header key-value pairs.
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.
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.
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`
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.
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;
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:
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' }); }
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 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?
Thanks for your feedback