Handtekeningen verifiëren
Hoe er wordt ondertekend
De ondertekende inhoud is de webhook-id-header, de webhook-timestamp-header en de ruwe verzoekbody, met punten aan elkaar gezet: <id>.<timestamp>.<body>. De sleutel is je geheim zonder het voorvoegsel whsec_, base64-gedecodeerd. webhook-signature bevat v1, gevolgd door de base64-HMAC-SHA256 van die inhoud.
Zolang een geroteerd geheim in zijn overgangsperiode zit, bevat webhook-signature twee handtekeningen gescheiden door een spatie, één per geheim; accepteer de levering als er één klopt.
- Verifieer de ruwe body precies zoals ontvangen, voordat je JSON parset: opnieuw serialiseren verandert de bytes.
- Weiger een webhook-timestamp die meer dan 5 minuten van je klok afligt, zodat een onderschept verzoek later niet kan worden afgespeeld.
- Vergelijk handtekeningen in constante tijd.
- Antwoord snel met 2xx en doe trage taken daarna: we wachten 10 seconden.
Met een Standard Webhooks-bibliotheek
De standardwebhooks-pakketten op npm en PyPI (en bibliotheken voor de meeste andere talen) controleren handtekening en tijdstempel in één aanroep en geven een fout als een van beide niet klopt.
// npm install standardwebhooksimport { Webhook } from "standardwebhooks";const wh = new Webhook(process.env.DOTWICK_WEBHOOK_SECRET); // whsec_…// Express: app.post("/dotwick", express.raw({ type: "application/json" }), handler)function handler(req, res) { let event; try { event = wh.verify(req.body.toString("utf8"), { "webhook-id": req.header("webhook-id"), "webhook-timestamp": req.header("webhook-timestamp"), "webhook-signature": req.header("webhook-signature"), }); } catch { return res.status(400).end(); } res.status(204).end(); // answer first, then work queue.add(event.type, event);}Met de hand
Geen afhankelijkheid: de hele controle is een paar regels van de standaardbibliotheek.
import { createHmac, timingSafeEqual } from "node:crypto";function verifyDotwick(secret, headers, rawBody) { const id = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; const signatures = headers["webhook-signature"] ?? ""; if (!id || !timestamp) return false; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest(); return signatures.split(" ").some((entry) => { const [version, signature] = entry.split(","); const received = Buffer.from(signature ?? "", "base64"); return ( version === "v1" && received.length === expected.length && timingSafeEqual(received, expected) ); });}