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.
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:
- Retrieve the
X-Bettermode-Request-Timestamp
header on the HTTP request and the raw body of the request. - 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. - With the help of HMAC SHA256 implemented in your favorite programmig language, hash the above basestring, using the Bettermode
Signing secret
as the key. - Compare this computed signature to the
X-Bettermode-Signature
header on the request.
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.'));
});
You can check our Bettermode Starter App on GitHub to see signature verification in action.