Skip to main content

Verifying Webhook Requests

Every single Bettermode webhook request includes X-Bettermode-Signature and X-Bettermode-Request-Timestamp headers. We highly recommend verifying all webhook requests using the X-Bettermode-Signature and the Signing secret provided under "Credentials" section of your app in the Bettermode developers portal.

note

Not verifying X-Bettermode-Signature will let third parties misuse your webhook endpoint by faking POST requests and can be dangerous.

Webhook Verification Steps

Here's an overview of the process to validate a signed request from Bettermode:

  1. Retrieve the X-Bettermode-Request-Timestamp header on the HTTP request and the raw body of the request.
  2. Concatenate the extracted timestamp, and the raw body of the request to form a basestring. Use a colon (:) as the delimiter between the two elements.
  3. With the help of HMAC SHA256 implemented in your favorite programmig language, hash the above basestring, using the Bettermode Signing secret as the key.
  4. Compare this computed signature to the X-Bettermode-Signature header on the request.
note

Make sure that the request body contains no headers and is not deserialized in any way. Use only the raw request payload.

Preventing replays

Bettermode does not ensure that an event will be triggered just once. As an example, Bettermode will retry an event if it times out.

Every single Bettermode webhook request includes X-Bettermode-Request-Timestamp and a unique data.id.

To ensure that an event is handled only once on your side, we recommend checking data.id and making sure you haven't processed an event with that unique ID before. We also recommend checking X-Bettermode-Request-Timestamp and ignore any events older than 15 minutes before the request timestamp.

Doing so will also prevent replay attacks on your endpoints in case it happens.

Webhook Verification Example

Here is how you can verify Bettermode webhook requests using Node.js.

First we need to have a function that generates the signature based on the timestamp, raw body, and the signing secret. We can easily do so using the following function:

import * as crypto from 'crypto';

export const getSignature = ({ secret, body, timestamp }) => {
return crypto.createHmac('sha256', secret).update(`${timestamp}:${body}`).digest('hex');
};

Next, we need to create a function that compares the generated signature with X-Bettermode-Signature and also makes sure that the request is not older than 5 minutes based on the X-Bettermode-Request-Timestamp to prevent replay attacks.

const MILLISECONDS_IN_MINUTE = 1000 * 60;

export const verifySignature = ({ signature, secret, body, timestamp }) => {
const timeDifference = (timestamp - new Date().getTime()) / MILLISECONDS_IN_MINUTE;
if (timeDifference > 5) return false;
const hash = getSignature({ secret, body, timestamp });
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(hash));
};

This function will return true if all is good.

If we're using Express, we can create a middleware that throws an error if the signature is not verified. To do so, we need to use body-parser to pass the raw body to the request:

import bodyParser from 'body-parser';

app.use(
bodyParser.json({
verify: (req, res, buf) => {
req['rawBody'] = buf;
},
}),
);

Last but not least, we can introduce a new middleware to our app to verify the signature:

app.use((req, res, next) => {
// Filled by body-parser
const rawBody = req['rawBody'];

const timestamp = parseInt(req.header('X-Bettermode-Request-Timestamp'), 10);
const signature = req.header('X-Bettermode-Signature');

try {
if (rawBody && verifySignature({
body: rawBody,
timestamp,
signature,
secret: SIGNING_SECRET // Your signing secret
})) {
return next();
}
} catch (err) {
console.error(err);
}
return next(new Error(403, 'The X-Bettermode-Signature is not valid.'));
});
note

You can check our Bettermode Starter App on GitHub to see signature verification in action.