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