Verifying signatures
Check that an event comes from Ovianta.
Every event carries an x-signature header: the HMAC-SHA256 of the raw request body, keyed with the endpoint's signing secret, encoded as lowercase hex. Use the whole secret as the key, whsec_ prefix included.
- Read the body exactly as received, before any JSON parsing.
- Compute the HMAC-SHA256 of it with the secret, as hex.
- Compare it with
x-signatureusing a constant-time comparison. - Answer
401if they differ. Ovianta does not retry a401.
import crypto from 'node:crypto';
import express from 'express';
const secret = process.env.OVIANTA_WEBHOOK_SECRET!; // whsec_...
const app = express();
// express.raw keeps the body as the exact bytes Ovianta signed.
app.post('/ovianta/events', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto.createHmac('sha256', secret).update(req.body).digest();
const received = Buffer.from(req.get('x-signature') ?? '', 'hex');
if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Store the event, answer, and process it afterwards.
res.sendStatus(200);
});Sign the raw bytes
Parsing the JSON and serializing it again changes spacing and key order, and the signature will not match.
Rotating the secret
Each endpoint has its own secret, and it cannot be changed. To replace it, add a new endpoint with the same URL and events, accept both secrets for a while, then delete the old endpoint. While both exist every event arrives twice, with the same eventId.