Verify payment webhook signatures
Node · Node · advanced · greenfield
Adds signature verification for the payment webhook: computes the HMAC over the raw body and compares it to the header in constant time, then accepts or rejects. Small and direct.
Requirements
- Verify the `X-Signature` header on payment webhooks: HMAC-SHA256 of the raw request body, hex-encoded, keyed with our webhook secret.
- The comparison must be constant-time.
- Reject invalid signatures with 401; accept valid ones with 202.
- One payment provider, one algorithm — no others planned.
Files touched
- src/lib/verifySignature.js
- src/routes/webhooks.js
--- src/lib/verifySignature.js
+const crypto = require("crypto");
+
+const HEX_SHA256 = /^[0-9a-f]{64}$/i;
+
+function verifySignature(rawBody, signatureHex, secret) {
+ if (!Buffer.isBuffer(rawBody) || !HEX_SHA256.test(signatureHex)) {
+ return false;
+ }
+ const expected = crypto.createHmac("sha256", secret).update(rawBody).digest();
+ return crypto.timingSafeEqual(Buffer.from(signatureHex, "hex"), expected);
+}
+
+module.exports = { verifySignature };
--- src/routes/webhooks.js
+const express = require("express");
+const { verifySignature } = require("../lib/verifySignature");
+const { WEBHOOK_SECRET } = require("../config/secrets");
+
+const router = express.Router();
+
+router.post("/webhooks/payments", express.raw({ type: "application/json" }), (req, res) => {
+ const signature = req.get("X-Signature") || "";
+ if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
+ return res.status(401).json({ error: "invalid signature" });
+ }
+ return res.status(202).json({ received: true });
+});
+
+module.exports = router;