Start learning free

LESSON 110 OF 173 · BACKEND: PAYMENTS AND OTHER SERVICES

Check the Webhook's Signature in JavaScript

Practice

Anyone on the internet can send a POST to /webhooks/payments, not only the provider. Right now a forged event saying payment.succeeded for order 9 marks it paid, and 1250 worth of goods would ship for free. The provider proves that an event comes from it by signing it with a secret that only it and your shop know.

How signing works: the provider computes an HMAC of the exact bytes of the body with the secret (here with SHA-256) and sends it in the X-Signature header, written as hexadecimal characters. Your shop computes the same HMAC and compares the two. Nobody can compute it without the secret, and changing one character of the body changes it completely. With another key and message:

const sig = crypto.createHmac("sha256", "key").update("hello").digest();
console.log(sig.length);                                     // 32 bytes
console.log(sig.toString("hex").length);                     // 64 hexadecimal characters
console.log(crypto.timingSafeEqual(sig, Buffer.alloc(32)));  // false

Compare with crypto.timingSafeEqual, not === on two texts: === stops at the first different character, so an attacker who measures the time can find the signature one character at a time. timingSafeEqual throws an error when the two don't have the same length, so check the lengths first. When the lengths differ or the signatures don't match, answer 400 before the event is read, so a forged event changes nothing:

POST /webhooks/payments  (signed with the secret)   →  200  {"received": true}
POST /webhooks/payments  (a made-up X-Signature)    →  400  {"error": "Bad signature"}
POST /webhooks/payments  (no X-Signature header)    →  400  {"error": "Bad signature"}
GET /orders/9   →  200  {…, "payment_id": null, "status": "pending", "total": 1250}

Your task: store the secret "whsec_test" in WEBHOOK_SECRET. At the top of the webhook route, compute the expected signature from the raw body, read the X-Signature header as bytes, and answer {"error": "Bad signature"} with 400 when the two don't match.


Assignment
Follow the Coding Tutorial to check the signature of every event that reaches POST /webhooks/payments.


Hint
req.body is already the raw bytes, thanks to express.raw. Buffer.from(text, "hex") turns the header's text into bytes. Give req.get("X-Signature") a default of "" with ||, so a missing header gives 0 bytes and simply doesn't match.


STEP 1 OF 6
    Ctrl + Enter to check

    Pay Webhook Signature in JavaScript

    Keep going with JavaScript1,600+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free