We’ve set up an endpoint to consume real-time conversation events from CXone. The documentation mentions a signature header for verification, but the logic isn’t sticking. I’m trying to prevent replay attacks by validating the X-Nice-Webhook-Signature against the raw request body.
Here’s the Node.js handling the verification:
const crypto = require('crypto');
app.post('/webhook', (req, res) => {
const signature = req.headers['x-nice-webhook-signature'];
const timestamp = req.headers['x-nice-webhook-timestamp'];
const body = JSON.stringify(req.body); // Raw body as string
// Constructing the payload string as per docs: timestamp + body
const payload = `${timestamp}.${body}`;
const hmac = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET);
hmac.update(payload);
const calculatedSignature = hmac.digest('hex');
console.log('Expected:', signature);
console.log('Calculated:', calculatedSignature);
if (signature !== calculatedSignature) {
console.log('Signature mismatch');
return res.status(401).send('Invalid signature');
}
res.status(200).send('OK');
});
The signatures never match. Even when I log the raw body string, it looks identical to what I’m passing into the HMAC function. I’ve confirmed the secret key is correct by testing it in a local Python script where it works fine. The issue seems to be in how the payload string is constructed or encoded in the Node environment.
I’ve tried using req.body directly, JSON.stringify(req.body), and even reading req.rawBody if available. Nothing aligns with the header value. The timestamp header is present and looks valid (Unix epoch). Is there a specific encoding required for the body before hashing? Or maybe the order of concatenation is different than timestamp.body?
I’m stuck on this. The webhook returns 200 if I skip the check, but that’s not secure. Any ideas on what’s causing the hash mismatch?