Aller au contenu

Vérifier les signatures

Comment c’est signé

Le contenu signé est l’en-tête webhook-id, l’en-tête webhook-timestamp et le corps brut de la requête, joints par des points : <id>.<timestamp>.<body>. La clé est votre secret sans son préfixe whsec_, décodé en base64. webhook-signature contient v1, suivi du HMAC-SHA256 en base64 de ce contenu.

Tant qu’un secret renouvelé est dans sa période de grâce, webhook-signature contient deux signatures séparées par une espace, une par secret ; acceptez l’envoi dès que l’une correspond.

  • Vérifiez le corps brut exactement tel que reçu, avant d’analyser le JSON : le sérialiser à nouveau modifie les octets.
  • Rejetez un webhook-timestamp à plus de 5 minutes de votre horloge, pour qu’une requête capturée ne puisse pas être rejouée plus tard.
  • Comparez les signatures en temps constant.
  • Répondez 2xx rapidement et faites le travail lent ensuite : nous attendons 10 secondes.

Avec une bibliothèque Standard Webhooks

Les paquets standardwebhooks sur npm et PyPI (et des bibliothèques pour la plupart des autres langages) vérifient la signature et l’horodatage en un seul appel et lèvent une erreur quand l’un des deux est incorrect.

exemplevérifier
// 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);}

À la main

Aucune dépendance : toute la vérification tient en quelques lignes de la bibliothèque standard.

exemplevérifier
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)    );  });}