import * as crypto from "crypto";
const isValidSignature = (
body: string, // raw JSON string of the request body
timestamp: string, // timestamp string from X-Api-Timestamp header
signature: string, // hex string from X-Api-Signature header
secret: string // your webhook secret
): boolean => {
const expectedSignature = generateSignature(body, timestamp, secret);
// Use timing-safe comparison to avoid timing attack vulnerability
const sigBuffer = Buffer.from(signature, "hex");
const expectedSigBuffer = Buffer.from(expectedSignature, "hex");
return (
sigBuffer.length === expectedSigBuffer.length &&
crypto.timingSafeEqual(sigBuffer, expectedSigBuffer)
);
};
// Example usage in an Express.js route handler
app.post("/my-webhook-endpoint", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.header("x-api-signature");
const timestamp = req.header("x-api-timestamp");
const rawBody = req.body.toString(); // assuming body is kept as raw Buffer
if (!signature || !timestamp || !isValidSignature(rawBody, timestamp, signature, WEBHOOK_SECRET)) {
console.error("Invalid webhook signature – request possibly forged!");
return res.sendStatus(400); // reject if signature doesn't match
}
// Optionally, also check the timestamp here for staleness
const now = Date.now();
const reqTime = Number(timestamp);
if (isNaN(reqTime) || Math.abs(now - reqTime*1000) > 5 * 60 * 1000) { // example: 5 minute tolerance
console.error("Webhook timestamp outside of tolerance – possible replay attack!");
return res.sendStatus(400);
}
// Signature is valid and timestamp is recent – process the webhook
const event = JSON.parse(rawBody);
handleWebhookEvent(event);
res.sendStatus(200);
});