I am implementing a secure webhook consumer for Genesys Cloud routing events using Node.js. The goal is to validate the X-Genesys-Signature header to prevent replay attacks. The documentation suggests comparing the calculated HMAC-SHA256 against the provided signature. My code looks like this:
const signature = crypto.createHmac('sha256', secret).update(payload).digest('hex');
if (signature !== req.headers['x-genesys-signature']) { return res.status(401); }
It’s failing every time. Am I missing a timestamp check in the payload?
You’re likely hitting the timestamp validation window too hard. Genesys Cloud includes a timestamp in the payload to prevent replay attacks, but if your server clock is even slightly out of sync or the network latency is high, the signature check fails because the calculated HMAC doesn’t match the one generated at the source. The X-Genesys-Signature header isn’t just a static hash; it’s tied to the exact moment the event was created. You need to ensure you’re parsing the raw body correctly before hashing it. If you’re using express.json(), the body is already parsed into an object. You can’t just hash the object reference. You have to stringify it back to the exact JSON format Genesys sent, including any whitespace or key ordering. That’s where most people trip up. Also, make sure you’re using the correct secret key from the Integration settings, not the client secret. Here’s a safer way to handle it in Node.js, assuming you have access to the raw request body:
const crypto = require('crypto');
// Ensure you capture the raw body before parsing it as JSON
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const secret = process.env.GENESYS_WEBHOOK_SECRET;
const payload = req.body; // This is a Buffer or String, not an Object
const signature = req.headers['x-genesys-signature'];
if (!signature || !secret) {
return res.status(400).send('Missing signature or secret');
}
// Calculate HMAC-SHA256
const hmac = crypto.createHmac('sha256', secret);
const calculatedSignature = hmac.update(payload).digest('hex');
// Constant time comparison to prevent timing attacks
if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(calculatedSignature))) {
res.status(200).send('OK');
} else {
res.status(401).send('Invalid signature');
}
});
Just double-check that your server’s system time is synced with NTP. If it’s off by more than a few minutes, the webhook will reject the event before you even get to the signature check in some configurations.