Webhooks expliqués : signatures et tentatives
Vous avez créé un point de terminaison webhook pour recevoir des événements d'un service tiers. Tout fonctionne en test, mais en production, vous remarquez des événements en double, des notifications manquées et parfois des problèmes de sécurité liés à des requêtes falsifiées. Ce sont des pièges courants lorsqu'on implémente des webhooks sans les protections adéquates.
Les webhooks sont des callbacks HTTP qui permettent aux services de notifier votre application lorsqu'un événement se produit. Contrairement au polling, ils fournissent des mises à jour en temps réel. Mais pour les utiliser de manière fiable, vous devez aborder deux aspects critiques : l'authenticité (s'assurer que la requête provient de l'expéditeur attendu) et la livraison (gérer les échecs et les tentatives). Cet article explique les deux avec des exemples de code pratiques.
Comment fonctionnent les webhooks
Un webhook est un point de terminaison HTTP POST défini par l'utilisateur. Lorsqu'un événement se produit, le service source envoie une requête à cette URL avec un payload décrivant l'événement. Par exemple, un processeur de paiement peut envoyer un webhook lorsqu'un paiement réussit.
Le flux est simple :
- Vous enregistrez une URL de webhook auprès du service.
- Le service envoie un HTTP POST à cette URL lorsqu'un événement se déclenche.
- Votre point de terminaison traite le payload et renvoie un statut 2xx pour accuser réception.
Sans vérification appropriée, n'importe qui peut envoyer des requêtes à votre point de terminaison. Sans gestion des tentatives, des échecs transitoires peuvent entraîner des événements perdus. Abordons chacun de ces points.
Sécuriser les webhooks avec des payloads signés
Pour garantir qu'un webhook provient de l'expéditeur attendu, les services signent le payload à l'aide d'un secret partagé. La signature est généralement un HMAC (Hash-based Message Authentication Code) du corps de la requête, envoyé dans un en-tête comme X-Signature.
Comment fonctionne la signature HMAC
L'expéditeur calcule un HMAC à l'aide d'une clé secrète et du corps brut de la requête, puis inclut la signature encodée en hexadécimal dans un en-tête. Votre point de terminaison recalcule le HMAC avec le même secret et le compare à la signature reçue. S'ils correspondent, la requête est authentique.
Voici un exemple minimal en Node.js avec Express :
const crypto = require('crypto');
const express = require('express');
const app = express();
// Utiliser le corps brut pour la vérification de la signature
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Traiter l'événement
const event = JSON.parse(payload);
// ... gérer l'événement
res.status(200).send('OK');
});
Points clés :
- Utilisez toujours le corps brut de la requête pour la vérification, et non un objet analysé. L'analyse peut altérer le payload (par exemple, les espaces) et casser la signature.
- Utilisez une comparaison à temps constant pour éviter les attaques temporelles.
- Stockez les secrets de manière sécurisée (variables d'environnement, gestionnaires de secrets) et faites-les tourner périodiquement.
Mesures de sécurité supplémentaires
- Validation de l'horodatage : incluez un horodatage dans la signature (par exemple,
t=timestamp,v1=signature) et rejetez les requêtes plus anciennes que quelques minutes pour éviter les attaques par rejeu. - Liste blanche d'IP : si le fournisseur publie des plages d'IP, limitez votre point de terminaison à ces IP.
- HTTPS : utilisez toujours HTTPS pour protéger le payload en transit.
Implémenter des tentatives fiables
La livraison des webhooks n'est pas garantie. Les réseaux échouent, votre serveur peut être indisponible ou vous pouvez renvoyer un statut non-2xx. Pour gérer cela, les expéditeurs réessaient généralement avec un backoff exponentiel. En tant que récepteur, vous devez concevoir votre point de terminaison pour être idempotent et répondre rapidement.
Côté expéditeur (si vous envoyez des webhooks)
Si vous créez un service qui envoie des webhooks, implémentez des tentatives avec backoff. Une stratégie courante :
- Réessayez en cas d'erreurs réseau ou de réponses 5xx.
- Ne réessayez pas en cas de réponses 4xx (sauf 429 Too Many Requests).
- Utilisez un backoff exponentiel avec jitter : attendez 1s, 2s, 4s, 8s, etc., jusqu'à un maximum.
- Définissez un nombre maximal de tentatives (par exemple, 5) et envisagez une file d'attente de messages morts pour les échecs.
Exemple de logique de tentative en Python :
import time
import requests
from requests.exceptions import RequestException
def send_webhook(url, payload, max_retries=5):
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, timeout=5)
if 200 <= response.status_code < 300:
return True
elif 400 <= response.status_code < 500 and response.status_code != 429:
# Erreur client, ne pas réessayer
return False
except RequestException:
pass
# Backoff exponentiel avec jitter
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
return False
Côté récepteur
Votre point de terminaison doit :
- Accuser réception rapidement : renvoyez un 2xx en quelques secondes. Si le traitement prend plus de temps, mettez l'événement en file d'attente et traitez-le de manière asynchrone.
- Être idempotent : utilisez un ID d'événement du payload pour dédupliquer. Stockez les ID traités et ignorez les répétitions.
- Gérer la concurrence : utilisez des transactions ou des verrous de base de données pour éviter les conditions de course lors du traitement simultané du même événement.
Exemple d'idempotence utilisant une contrainte d'unicité de base de données :
CREATE TABLE processed_events (
event_id VARCHAR(255) PRIMARY KEY,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Avant de traiter, essayez d'insérer l'ID de l'événement. S'il viole la clé primaire, ignorez le traitement.
Tester les webhooks localement
Pendant le développement, vous avez besoin d'une URL publique pour que le service puisse atteindre votre serveur local. Des outils comme ngrok ou localtunnel exposent votre localhost. Utilisez toujours des secrets de test et vérifiez les signatures en développement pour détecter les problèmes tôt.
Comparaison : polling vs webhooks
| Aspect | Polling | Webhooks |
|---|---|---|
| Latence | Dépend de l'intervalle | Quasi temps réel |
| Charge serveur | Élevée (requêtes fréquentes) | Faible (uniquement sur événements) |
| Complexité | Simple à implémenter | Nécessite la vérification de signature et la gestion des tentatives |
| Fiabilité | Vous contrôlez les tentatives | Dépend de la politique de tentative de l'expéditeur |
FAQ
Qu'est-ce qu'un payload webhook signé ?
Un payload signé inclut une signature cryptographique (généralement HMAC) générée avec un secret partagé. Elle permet au récepteur de vérifier que la requête provient de l'expéditeur attendu et n'a pas été altérée.
Comment gérer les livraisons de webhooks en double ?
Rendez votre point de terminaison idempotent en utilisant un ID d'événement unique provenant du payload. Stockez les ID traités et ignorez les doublons. Cela garantit que les tentatives ou les problèmes réseau ne provoquent pas de double traitement.
Dois-je réessayer les livraisons de webhooks échouées ?
Oui, mais uniquement pour les erreurs transitoires (problèmes réseau, 5xx). Utilisez un backoff exponentiel avec jitter et fixez une limite maximale de tentatives. Évitez de réessayer en cas d'erreurs 4xx (sauf 429), car elles indiquent un problème avec la requête elle-même.
Lors du débogage de payloads webhook, vous avez souvent besoin d'inspecter des données JSON. Le JSON Formatter peut vous aider à formater et valider rapidement les payloads JSON.