Signaturen prüfen
So wird signiert
Signiert wird der Header webhook-id, der Header webhook-timestamp und der rohe Request-Body, mit Punkten verbunden: <id>.<timestamp>.<body>. Der Schlüssel ist Ihr Secret ohne das Präfix whsec_, Base64-dekodiert. webhook-signature enthält v1, gefolgt vom Base64-HMAC-SHA256 dieses Inhalts.
Solange ein rotiertes Secret in seiner Übergangsfrist ist, enthält webhook-signature zwei durch Leerzeichen getrennte Signaturen, eine pro Secret; akzeptieren Sie die Zustellung, wenn eine davon passt.
- Prüfen Sie den rohen Body genau so, wie er empfangen wurde, bevor Sie JSON parsen: Eine erneute Serialisierung verändert die Bytes.
- Lehnen Sie ein webhook-timestamp ab, das mehr als 5 Minuten von Ihrer Uhr abweicht, damit eine abgefangene Anfrage nicht später wiederholt werden kann.
- Vergleichen Sie Signaturen in konstanter Zeit.
- Antworten Sie schnell mit 2xx und erledigen Sie langsame Arbeit danach: Wir warten 10 Sekunden.
Mit einer Standard-Webhooks-Bibliothek
Die Pakete standardwebhooks auf npm und PyPI (und Bibliotheken für die meisten anderen Sprachen) prüfen Signatur und Zeitstempel in einem Aufruf und lösen einen Fehler aus, wenn eines davon nicht stimmt.
// 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);}Von Hand
Keine Abhängigkeit: Die ganze Prüfung sind ein paar Zeilen der Standardbibliothek.
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) ); });}