Webhooks expliqués : payloads signés et retries
Pourquoi les webhooks sont délicats
Les webhooks sont une idée simple : au lieu d'interroger une API pour détecter des changements, le fournisseur pousse les événements vers votre endpoint via HTTP POST. Mais en pratique, les webhooks introduisent un ensemble de défis qui piègent de nombreux développeurs : vérifier que la requête provient bien du fournisseur, gérer les livraisons en double et faire face aux pannes réseau. Si vous ignorez ces aspects, vous risquez des failles de sécurité, une corruption des données et des utilisateurs mécontents.
Cet article passe en revue les deux aspects les plus critiques des webhooks en production : les payloads signés et les retries. Nous verrons comment vérifier les signatures, comment implémenter des retries avec backoff exponentiel et comment rendre votre traitement idempotent.
Payloads signés : prouver l'authenticité
N'importe qui peut envoyer un HTTP POST vers votre endpoint. Pour s'assurer que la requête provient bien de votre fournisseur, celui-ci signe le payload avec une clé secrète et envoie la signature dans un en-tête. Votre travail consiste à recalculer la signature et à la comparer.
La plupart des fournisseurs utilisent HMAC-SHA256. La signature est généralement calculée sur le corps brut de la requête (parfois avec un timestamp) et encodée en hexadécimal. Par exemple, Stripe utilise un en-tête comme Stripe-Signature contenant un timestamp et une signature.
Comment fonctionne HMAC
HMAC (Hash-based Message Authentication Code) combine une clé secrète avec une fonction de hachage. Avec la même clé et le même message, le résultat est déterministe. Sans la clé, un attaquant ne peut pas forger une signature valide.
Voici un exemple minimal en Node.js :
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
En Python, c'est tout aussi simple :
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Bonnes pratiques pour la vérification de signature
- Utilisez une comparaison à temps constant pour éviter les attaques temporelles.
crypto.timingSafeEqualethmac.compare_digestsont tous deux sûrs. - Vérifiez le corps brut, pas un objet JSON parsé. Le parsing et la re-sérialisation peuvent modifier les espaces et casser la signature.
- Incluez un timestamp dans les données signées pour éviter les attaques par rejeu. Rejetez les requêtes plus anciennes que quelques minutes.
- Stockez les secrets de manière sécurisée — utilisez des variables d'environnement ou un gestionnaire de secrets, ne les codez jamais en dur.
Gérer les timestamps et les attaques par rejeu
De nombreux fournisseurs incluent un timestamp dans l'en-tête de signature. Par exemple, l'en-tête peut être t=1614556800,v1=abc123.... Vous devez :
- Extraire le timestamp et la signature.
- Vérifier que le timestamp est dans une fenêtre acceptable (par ex., 5 minutes).
- Recalculer la signature en utilisant le timestamp et le payload.
- Comparer les signatures de manière sécurisée.
Cela empêche un attaquant de rejouer une ancienne requête valide.
Retries : gérer les échecs avec élégance
Les webhooks sont livrés sur l'internet public, donc les échecs sont inévitables. Votre endpoint peut être indisponible, lent ou renvoyer une erreur 500. Un fournisseur robuste retentera les livraisons échouées, mais vous devez aussi gérer les retries de votre côté si vous êtes le fournisseur.
Backoff exponentiel avec jitter
Lors des retries, évitez de marteler l'endpoint avec des tentatives immédiates. Utilisez un backoff exponentiel : attendez plus longtemps après chaque échec. Ajoutez du jitter (aléatoire) pour éviter les problèmes de thundering herd lorsque de nombreux webhooks échouent simultanément.
Un calendrier de retry typique pourrait ressembler à :
| Tentative | Délai (approx) |
|---|---|
| 1 | Immédiat |
| 2 | 30 secondes |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 heures |
Après un nombre maximum de tentatives, marquez la livraison comme échouée et alertez.
Idempotence : la clé des retries sûrs
Comme les retries peuvent causer des livraisons en double, votre gestionnaire de webhook doit être idempotent. Cela signifie que traiter le même événement plusieurs fois doit avoir le même effet que le traiter une seule fois.
Comment atteindre l'idempotence :
- Utilisez un ID d'événement unique fourni dans le payload (par ex., champ
id). Stockez les IDs traités dans une base de données avec une contrainte d'unicité. - Concevez des opérations idempotentes — par exemple, utilisez des upserts au lieu d'inserts, ou vérifiez si une ressource existe déjà avant de la créer.
- Renvoyez rapidement un 2xx et traitez en asynchrone si le travail est lourd. Cela réduit le risque de timeouts qui déclenchent des retries.
Implémenter les retries côté fournisseur
Si vous construisez un système de webhooks, vous avez besoin d'un mécanisme de livraison fiable. Voici une approche simplifiée :
- Lorsqu'un événement se produit, mettez en file d'attente une tâche de livraison avec le payload et l'URL cible.
- Un worker récupère la tâche et envoie le HTTP POST avec un timeout (par ex., 10 secondes).
- Si le statut de la réponse n'est pas 2xx, planifiez un retry avec backoff exponentiel.
- Suivez les tentatives et abandonnez après un maximum (par ex., 5 tentatives).
- Journalisez les échecs et offrez un moyen de rejouer manuellement.
Utilisez une file persistante (par ex., Redis, RabbitMQ ou une table de base de données) pour que les retries survivent aux redémarrages.
Tout mettre ensemble : une checklist pour le récepteur de webhook
Lorsque vous construisez un récepteur de webhook, suivez cette checklist :
- Vérifiez la signature en utilisant le secret du fournisseur et une comparaison à temps constant.
- Vérifiez le timestamp pour prévenir les attaques par rejeu.
- Répondez rapidement avec un 2xx — accusez réception avant d'effectuer un traitement lourd.
- Traitez en asynchrone si nécessaire, en utilisant une file d'attente.
- Assurez l'idempotence en suivant les IDs d'événements.
- Journalisez tout pour le débogage et l'audit.
- Surveillez les échecs et configurez des alertes pour les retries répétés.
FAQ
Qu'est-ce qu'un payload de webhook signé ?
Un payload signé inclut une signature cryptographique (généralement HMAC-SHA256) générée avec un secret partagé. Le récepteur recalcule la signature pour vérifier que la requête provient de l'expéditeur attendu et n'a pas été altérée.
Pourquoi les webhooks ont-ils besoin de retries ?
Des problèmes réseau, une indisponibilité du serveur ou des erreurs temporaires peuvent faire échouer une livraison de webhook. Les retries avec backoff exponentiel garantissent que l'événement est finalement livré sans submerger le récepteur.
Comment éviter le traitement en double des webhooks ?
Utilisez l'idempotence : incluez un ID d'événement unique dans le payload, stockez les IDs traités et concevez vos opérations pour être idempotentes (par ex., des upserts). Ainsi, même si un webhook est livré plusieurs fois, l'effet est le même.
Si vous avez besoin d'inspecter ou de déboguer des payloads de webhook, le JSON Formatter peut vous aider à formater et valider rapidement des données JSON.