Skip to content
Tous les articles

Vérification correcte des signatures des webhooks et gestion des nouvelles tentatives

Charles Archibong

, Cofondateur

Vérifiez la signature V2 sur les octets de la requête brute, rejetez les horodatages en dehors de la fenêtre de relecture, enregistrez l'ID de l'événement sous une contrainte unique avant d'effectuer tout travail et ne renvoyez 2xx qu'une fois que l'événement est stocké de manière durable.

Titre « Signatures et nouvelles tentatives de webhook » à côté d'une illustration d'une fenêtre de code avec des chevrons, sur un dégradé indigo foncé.

Points clés

  • Calculez le HMAC sur les octets bruts de la requête, jamais sur le JSON resérialisé.
  • L'horodatage de la signature, combiné à la fenêtre de relecture, empêche la relecture d'un ancien envoi valablement signé.
  • Dédupliquer sur l'ID de l'événement, et non sur l'ID de livraison : un renvoi manuel constitue une nouvelle livraison du même événement.
  • Ne renvoyez 2xx qu'une fois l'événement enregistré de manière durable, et effectuez le travail lent ensuite.

Un récepteur de webhook a trois tâches qui s'exécutent dans un ordre fixe. Prouvez que la requête provient de Toden Vérifiez la signature sur les octets reçus. Assurez-vous de sa fraîcheur en comparant l'horodatage signé à la fenêtre de relecture. Prouvez ensuite que vous n'avez pas déjà agi en conséquence en enregistrant l'identifiant de l'événement sous une contrainte d'unicité. Ce n'est qu'après ces trois étapes que vous renvoyez les données.2xxEt ce n'est qu'après cela que vous faites le vrai travail.

Une erreur dans l'ordre des requêtes entraîne l'absence de notification. Une analyse effectuée avant la vérification peut conduire à l'envoi d'un corps de requête falsifié à votre système métier. L'omission de l'horodatage permet la réutilisation indéfinie d'une requête capturée. Une déduplication basée sur une clé incorrecte peut entraîner l'inscription d'un client à deux reprises lors d'un renvoi. Cet article détaille chaque étape. Toden webhooks (s’ouvre dans un nouvel onglet) , avec un récepteur que vous pouvez adapter.

Ce qui arrive avec chaque livraison

Chaque livraison est un HTTP POSTavec un JSON Un corps et un ensemble d'en-têtes. Ceux qui sont importants pour la vérification, comme indiqué dans la documentation sur l'authentification par webhook (s’ouvre dans un nouvel onglet) :

En-tête

Exemple

Qu'en faire ?

X-Myaza-Signature-V2

t=1786441200,v1=8df1…

Analysez l'horodatage et chaquev1valeur. Vérifiez par rapport à ces valeurs.

X-Myaza-Replay-Window

300

Durée de relecture du point de terminaison en secondes. Non signé, ne l'utilisez jamais pour élargir votre vérification.

X-Myaza-Event-Id

evt_…

Clé stable pour la déduplication.

X-Myaza-Delivery

del_…

Le bon de livraison. Utile pour le traçage, mais pas pour la déduplication.

X-Myaza-Event

verification.completed

Pour le routage. L'organe signataire fait foi.

X-Myaza-Signature

sha256=…

Signature de compatibilité héritée sur le corps seul.

Si vous avez construit contre l'ancienX-Myaza-SignatureL'en-tête fonctionne encore. Il signe le corps sans horodatage, ce qui ne permet pas de savoir si une requête est récente. Passez à l'en-tête V2.

Étape 1 : vérifier la signature sur les octets bruts

La signature est en hexadécimal minuscule HMAC - SHA256 , chiffré avec votre secret de point de terminaison, sur la chaîne<t>.<rawBody>: l'horodatage de l'en-tête, un point, puis le corps de la requête tel qu'il est arrivé.

« Exactement comme à sa réception » est ce qui pose problème pour les intégrations. La plupart des frameworks web analysent JSON avant l'exécution de votre gestionnaire. Si vous effectuez une vérification par rapport àJSON.stringify(req.body)Vous effectuez une resérialisation : les espaces, l’ordre des clés et le formatage des nombres peuvent différer de la signature originale, et la vérification échoue de manière intermittente, ce qui peut indiquer un secret corrompu. Configurez la route pour qu’elle vous fournisse le tampon brut.

Comparez en utilisant une fonction à temps constant, et comparez avec chaquev1valeur, pas seulement la première. Pendant la rotation secrète Toden L'en-tête contient à la fois le secret actif et celui qui est retiré, de sorte que deux candidats y figurent. Acceptez la requête si l'un des deux correspond, et supprimez l'ancien secret uniquement après la fin de sa période de grâce dans le tableau de bord.

Étape 2 : rejeter les horodatages obsolètes

Une signature valide prouve qui a envoyé une requête, et non quand. Quiconque a intercepté une livraison (à partir d'un journal de proxy, par exemple) pourrait la renvoyer, et elle serait toujours validée. L'horodatage signé comble cette lacune : cartsi cette valeur se trouve à l'intérieur de la chaîne signée, un attaquant ne peut pas la modifier sans enfreindre la signature.

Rejetez toute livraison dont l'horodatage est manquant, illisible, antérieur à la fenêtre de relecture ou anormalement lointain.X-Myaza-Replay-WindowL'en-tête indique la fenêtre de configuration du point de terminaison, mais il se trouve en dehors de la chaîne signée ; par conséquent, toute personne rejouant une requête pourrait le modifier. Utilisez-le pour renforcer votre vérification, mais jamais pour l'élargir : limitez-le à la fenêtre configurée pour le point de terminaison. Assurez-vous que l'horloge de votre serveur est synchronisée ; un décalage peut entraîner des rejets aléatoires.

Retour401pour une signature manquante, périmée ou invalide. Ne pas retourner200Pour que les nouvelles tentatives cessent : cela indique à l’expéditeur que l’événement a été accepté.

Étape 3 : dédupliquer au niveau de l’événement, et non de la livraison

Les webhooks sont transmis au moins une fois. Un délai d'attente dépassé pour le récepteur, une interruption de réseau après votre réponse ou le fait qu'un collègue clique sur « Renvoyer » dans le tableau de bord vous renverront la même notification.

La question est de savoir quel identifiant signifie « la même chose ». Il y a deux candidats, et ils se comportent différemment :

  • L' identifiant de l'événement (X-Myaza-Event-Id, etidL'identifiant figurant dans l'enveloppe correspond à l'événement commercial. Il reste inchangé lors des nouvelles tentatives automatiques et lors d'un nouvel envoi manuel.

  • L' identifiant de livraison (X-Myaza-Delivery) identifie un enregistrement de livraison. Un renvoi manuel crée un autre enregistrement de livraison, qui reçoit donc un nouvel identifiant de livraison.

Utilisez l'ID d'événement pour la déduplication. Le guide de test des webhooks (s’ouvre dans un nouvel onglet) l'indique clairement et suggère une insertion qui ne résout pas les conflits :

insert into received_myaza_events (event_id, event_type, payload)
values (:id, :type, :payload)
on conflict (event_id) do nothing;

Un événement nécessite une attention particulière.verification.status_updatedest envoyé à plat, non emballé dans le{ id, event, createdAt, data }enveloppe que les autres événements utilisent, donc le corps n'en a pasidLe champ. Récupérer l'ID de l'événement à partir de l'en-tête fonctionne pour tous les types d'événements, ce qui est la règle la plus simple.

Étape 4 : accusez réception rapidement, puis effectuez le travail.

Retour2xxUne fois l'événement enregistré de manière durable, et pas avant, traitez-le en dehors de la requête : mettez à jour la fiche client, notifiez le demandeur et ouvrez un ticket interne. Un traitement trop lent au sein de la requête peut entraîner des délais d'attente, considérés comme des échecs et nécessitant une nouvelle tentative.

Les types d'événements inconnus doivent être stockés ou ignorés tout en étant pris en compte. De nouveaux événements sont ajoutés au catalogue au fil du temps ; le rejet d'un événement non reconnu entraîne une boucle de nouvelles tentatives pour un événement que vous n'aviez pas l'intention de traiter.

Un récepteur qui fait les quatre

Adapté de l'exemple Node.js de la documentation, avec la fenêtre de relecture limitée et l'insertion de déduplication ajoutée.saveEventOnceetqueueVous pouvez vous substituer à votre propre entrepôt et à votre responsable de chantier.

import crypto from "node:crypto";
import express from "express";

const app = express();

app.post("/webhooks/myaza", express.raw({ type: "application/json" }), async (req, res) => {
  const parts = String(req.header("X-Myaza-Signature-V2") || "").split(",");
  const timestamp = parts.find((p) => p.startsWith("t="))?.slice(2);
  const candidates = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
  const MAX_WINDOW = 300; // the replay window configured for this endpoint
  const windowSeconds = Math.min(Number(req.header("X-Myaza-Replay-Window")) || MAX_WINDOW, MAX_WINDOW);

  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > windowSeconds) {
    return res.status(401).send("Expired signature");
  }

  const expected = crypto
    .createHmac("sha256", process.env.MYAZA_WEBHOOK_SECRET)
    .update(`${timestamp}.`)
    .update(req.body)
    .digest("hex");

  const valid = candidates.some((c) =>
    c.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(c), Buffer.from(expected))
  );
  if (!valid) return res.status(401).send("Invalid signature");

  const eventId = req.header("X-Myaza-Event-Id");
  const payload = JSON.parse(req.body.toString("utf8"));
  const isNew = await saveEventOnce(eventId, req.header("X-Myaza-Event"), payload);

  res.status(200).send("OK");
  if (isNew) queue.add("myaza-event", { eventId });
});

Comme l'événement est stocké avant la réponse, un incident peut survenir entre la réponse et la réponse.queue.addRien n'est perdu : un petit processus de nettoyage récupère les événements stockés qui n'ont jamais été traités. Lors d'une rotation, conservez les deux secrets et calculez une valeur attendue pour chacun ; la vérification est alors réussie si un candidat correspond à au moins un secret.

Comment fonctionnent les nouvelles tentatives et ce qu'elles signifient pour vous

Une livraison qui ne reçoit pas de2xx, ou en cas de dépassement de délai, une nouvelle tentative est effectuée avec un délai d'attente :

Tentative

Délai après la tentative précédente

1

30 secondes

2

5 minutes

3

30 minutes

4

2 heures

5

24 heures

Après cinq tentatives infructueuses, la livraison est marquéeFAILEDet peut être réessayée manuellement depuis le tableau de bord. Deux conséquences en découlent.

Tout d'abord, une panne de votre côté d'un ou deux jours est récupérable sans perte de données, à condition de la résoudre avant la dernière tentative et de gérer de manière idempotente le pic de nouvelles tentatives lors du rétablissement du service. Configurez des alertes en cas d'échecs de livraison répétés afin d'être informé de la panne par votre système de surveillance plutôt que par un client.

Deuxièmement, les livraisons sont réessayées indépendamment, elles peuvent donc arriver dans le désordre. Supposons que votre flux de travail achemine une vérification vers la phase de révision et qu'un responsable de la conformité l'approuve une heure plus tard : deuxverification.status_updateddes événements, chacun avec son propre calendrier de nouvelles tentatives. Si la première livraison a échoué et a réussi lors de la nouvelle tentative, vous pouvez recevoirapprovedavantin_reviewLa règle établie consiste à traiter le plus récentchangedAtConsidérez-la comme la plus récente ; stockez-la et ignorez toute mise à jour antérieure. Lorsque les enjeux sont importants, lisez l’état actuel à partir de… API avant d'agir.

Tester avant la production

Créez un point de terminaison de test et utilisez le simulateur disponible sur sa page pour envoyer des événements d'exemple correctement signés via le chemin de livraison réel. Ensuite, testez volontairement les cas d'échec :

  1. Envoyez un événement simulé et vérifiez que le destinataire contrôle la signature V2 et renvoie un résultat.2xxrapidement.

  2. Modifiez un octet d'un corps capturé et confirmez.401.

  3. Rejouez une requête capturée après l'expiration du délai et confirmez-la.401.

  4. Renvoyez une livraison depuis le tableau de bord et confirmez que l'effet secondaire se produit une seule fois.

  5. Retournez un non-2xxtemporairement et confirmer qu'une nouvelle tentative est prévue.

  6. Faites tourner le secret et vérifiez que les deux signatures sont acceptées pendant la période de grâce.

Un récepteur qui réussit les six tests se comportera correctement le jour où quelque chose tourne mal, et c'est le seul jour où cela compte.

Sources

Charles Archibong

À propos de l’auteur

Charles Archibong

Cofondateur

Charles Archibong est le cofondateur de Toden. Il écrit sur la vérification d'identité, les technologies financières et les aspects pratiques de la création de services numériques de confiance.

Créez votre produit.Nous nous occupons du reste.

Identité et conformité de bout en bout, selon les normes internationales, à un prix adapté aux fondateurs.