feat: add HMAC webhook signature verification utilities
This commit is contained in:
40
src/lib/signature.js
Normal file
40
src/lib/signature.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const crypto = require("node:crypto");
|
||||||
|
|
||||||
|
function hmacSHA256Hex(payload, secret) {
|
||||||
|
return crypto
|
||||||
|
.createHmac("sha256", secret)
|
||||||
|
.update(payload, "utf8")
|
||||||
|
.digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifySignature({ payload, secret, signature }) {
|
||||||
|
if (!secret) {
|
||||||
|
throw new Error("webhook_secret_required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!signature) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = signature.startsWith("sha256=")
|
||||||
|
? signature.slice("sha256=".length)
|
||||||
|
: signature;
|
||||||
|
|
||||||
|
const expected = hmacSHA256Hex(payload, secret);
|
||||||
|
|
||||||
|
const expectedBuf = Buffer.from(expected, "hex");
|
||||||
|
const givenBuf = Buffer.from(normalized, "hex");
|
||||||
|
|
||||||
|
if (expectedBuf.length !== givenBuf.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return crypto.timingSafeEqual(expectedBuf, givenBuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
hmacSHA256Hex,
|
||||||
|
verifySignature,
|
||||||
|
};
|
||||||
33
test/signature.test.js
Normal file
33
test/signature.test.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const { hmacSHA256Hex, verifySignature } = require("../src/lib/signature");
|
||||||
|
|
||||||
|
test("verifies valid signature", () => {
|
||||||
|
const payload = JSON.stringify({ hello: "world" });
|
||||||
|
const secret = "topsecret";
|
||||||
|
const sig = hmacSHA256Hex(payload, secret);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
verifySignature({ payload, secret, signature: `sha256=${sig}` }),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects invalid signature", () => {
|
||||||
|
const payload = JSON.stringify({ hello: "world" });
|
||||||
|
const secret = "topsecret";
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
verifySignature({ payload, secret, signature: "sha256=deadbeef" }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects missing signature", () => {
|
||||||
|
const payload = "abc";
|
||||||
|
const secret = "topsecret";
|
||||||
|
|
||||||
|
assert.equal(verifySignature({ payload, secret, signature: "" }), false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user