Webhooks expliqués : signatures et tentatives
Vous venez d'intégrer un fournisseur de paiement tiers. La documentation indique : « Nous enverrons un webhook lorsqu'un paiement réussit. » Simple, non ? Mais vous réalisez ensuite : comment savoir que la requête provient réellement de chez eux ? Et si votre serveur est hors ligne au moment de l'envoi ? Et s'ils envoient le même événement deux fois ?
Les webhooks sont puissants mais nécessitent une gestion minutieuse. Ce guide couvre l'essentiel : signer les payloads pour vérifier l'authenticité, implémenter des tentatives pour gérer les échecs et assurer l'idempotence pour éviter les traitements en double.
Que sont les webhooks ?
Un webhook est un callback HTTP : au lieu que votre application interroge une API pour détecter des changements, le fournisseur envoie un POST HTTP à une URL que vous spécifiez lorsqu'un événement se produit. Cela rend les intégrations efficaces et quasi temps réel.
Les exemples courants incluent les notifications de paiement (Stripe, PayPal), les déclencheurs CI/CD (GitHub, GitLab) et les plateformes de messagerie (Slack, Discord).
Pourquoi sécuriser les webhooks avec des signatures ?
Votre endpoint de webhook est accessible publiquement. N'importe qui peut lui envoyer une requête POST. Sans vérification, un attaquant pourrait falsifier des événements—par exemple, marquer une commande impayée comme payée.
Signer les payloads avec un secret partagé vous permet de vérifier que la requête provient bien de l'expéditeur attendu et que le payload n'a pas été altéré.
Comment fonctionnent les signatures HMAC
Le fournisseur et vous partagez une clé secrète. Pour chaque webhook, le fournisseur calcule un HMAC (Hash-based Message Authentication Code) du payload à l'aide de ce secret et inclut la signature dans un en-tête (par exemple, X-Signature). Vous recalculez le HMAC de votre côté et comparez.
Le HMAC garantit l'intégrité et l'authenticité car seul quelqu'un possédant le secret peut produire une signature valide.
Implémentation des payloads signés
Parcourons une implémentation typique en Node.js (Express) et Python (Flask). Les principes s'appliquent à tout langage.
Étape 1 : Obtenir et stocker le secret
Lorsque vous enregistrez le webhook, le fournisseur vous donne un secret. Stockez-le de manière sécurisée—variables d'environnement ou gestionnaire de secrets—jamais dans le code.
Étape 2 : Capturer le corps brut
La vérification de la signature nécessite les octets exacts du corps brut. Si votre framework analyse le JSON avant que vous ne vérifiiez, la re-sérialisation peut modifier les octets et casser la vérification.
Dans Express, utilisez express.raw({ type: 'application/json' }) pour la route du webhook. Dans Flask, accédez à request.get_data() avant l'analyse.
Étape 3 : Calculer et comparer les signatures
Utilisez une fonction de comparaison à temps constant pour éviter les attaques temporelles.
// Node.js (Express)
const crypto = require('crypto');
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process event
const event = JSON.parse(req.body);
// ... handle event
res.status(200).send('OK');
});
En Python, utilisez hmac.compare_digest :
# Python (Flask)
import hmac, hashlib, os
from flask import request, abort
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
secret = os.environ['WEBHOOK_SECRET'].encode()
expected = hmac.new(secret, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.get_json()
# ... handle event
return 'OK', 200
Étape 4 : Gérer les horodatages (optionnel mais recommandé)
Certains fournisseurs incluent un horodatage dans la signature pour prévenir les attaques par rejeu. Par exemple, le schéma de Stripe signe timestamp.payload. Vous devez rejeter les requêtes avec des horodatages trop anciens (par exemple, plus de 5 minutes).
Implémentation des tentatives
Les réseaux échouent. Votre serveur peut être hors ligne ou la requête peut expirer. Les systèmes de webhooks fiables réessaient les livraisons échouées.
Du côté du fournisseur
Si vous construisez l'expéditeur de webhooks, implémentez des tentatives avec un backoff exponentiel. Un modèle courant : réessayer après 1 minute, puis 5 minutes, puis 30 minutes, etc., jusqu'à un nombre maximal de tentatives (par exemple, 5). Incluez un ID d'événement unique dans le payload pour que le récepteur puisse dédupliquer.
Du côté du récepteur
En tant que récepteur, vous devez :
- Répondre rapidement (en quelques secondes) avec un statut 2xx pour accuser réception. Effectuez les traitements lourds de manière asynchrone.
- Retourner un statut non-2xx pour les erreurs qui justifient une nouvelle tentative (par exemple, 500). Mais attention : si vous retournez un 4xx, le fournisseur peut ne pas réessayer.
- Rendre votre endpoint idempotent pour que les livraisons en double ne causent pas de problèmes.
Idempotence : gérer les événements en double
Les tentatives peuvent entraîner des événements en double. Utilisez l'ID d'événement (inclus par la plupart des fournisseurs) pour suivre les événements traités. Avant de traiter, vérifiez si l'ID a déjà été vu ; si oui, ignorez.
Stockez les IDs traités dans une base de données avec une TTL, ou utilisez une clé d'idempotence dans votre logique métier.
Tester les webhooks localement
Pendant le développement, votre serveur local n'est pas accessible publiquement. Des outils comme ngrok ou localtunnel créent une URL publique qui tunnelise vers votre machine. Alternativement, utilisez un service de test de webhooks qui capture les requêtes et vous permet de les rejouer.
Pièges courants et bonnes pratiques
- Ne faites pas confiance au payload avant vérification. Vérifiez toujours la signature en premier.
- Utilisez HTTPS. Les webhooks transportent des données sensibles ; chiffrez en transit.
- Journalisez tout. Journalisez la requête brute (en-têtes et corps) pour le débogage, mais masquez les informations sensibles.
- Surveillez les échecs. Configurez des alertes pour les échecs répétés de webhooks.
- Versionnez vos événements. Incluez une version dans le payload pour pouvoir évoluer sans casser les récepteurs.
Comparaison : webhooks signés vs non signés
| Aspect | Signé | Non signé |
|---|---|---|
| Sécurité | Élevée : vérifie l'origine et l'intégrité | Faible : n'importe qui peut falsifier |
| Effort d'implémentation | Modéré : calculer et comparer le HMAC | Minimal |
| Recommandé pour | Tous les systèmes en production | Réseaux internes de confiance uniquement |
FAQ
Comment vérifier une signature de webhook ?
Calculez un HMAC du corps brut de la requête en utilisant le secret partagé, puis comparez-le à l'en-tête de signature avec une fonction à temps constant. Utilisez toujours le corps brut, pas le JSON analysé.
Que doit retourner mon endpoint de webhook ?
Retournez rapidement un code de statut 2xx pour accuser réception. Si le traitement échoue, retournez un 5xx pour déclencher une nouvelle tentative, mais assurez-vous que votre endpoint est idempotent pour gérer les doublons.
Combien de fois un webhook sera-t-il réessayé ?
Cela dépend du fournisseur. La plupart réessaient plusieurs fois avec un backoff exponentiel sur une période de quelques heures ou jours. Consultez la documentation du fournisseur pour plus de détails.
Lors du débogage des livraisons de webhooks, l'inspection des journaux du serveur est souvent nécessaire. Si vous utilisez Nginx, l'Analyseur de journaux Nginx peut vous aider à analyser les journaux d'accès pour repérer les requêtes échouées ou les schémas inhabituels.