Webhooks expliqués : signatures et tentatives
Vous avez construit une API et devez maintenant notifier d'autres services lorsque des événements se produisent. Vous mettez en place un point de terminaison webhook, mais vous découvrez rapidement que n'importe qui peut envoyer de faux payloads, et que lorsque votre serveur est hors ligne, les événements sont perdus à jamais. C'est un piège courant : les webhooks sont simples en concept mais nécessitent une conception minutieuse pour la sécurité et la fiabilité.
Dans ce guide, nous allons parcourir l'essentiel des webhooks : comment signer les payloads pour empêcher toute altération, comment vérifier les signatures côté récepteur, et comment implémenter une logique de nouvelle tentative avec backoff exponentiel pour gérer les échecs. Nous aborderons également l'idempotence pour éviter le traitement en double. À la fin, vous disposerez d'un système de webhooks robuste et fiable.
Que sont les webhooks ?
Les webhooks sont des callbacks HTTP définis par l'utilisateur. Lorsqu'un événement se produit dans un système source (par exemple, un paiement est traité), il envoie une requête HTTP POST à une URL que vous spécifiez, avec un payload contenant les détails de l'événement. Cela permet une communication en temps réel entre les services sans interrogation répétée (polling).
Cependant, les webhooks introduisent deux défis :
- Sécurité : Comment le récepteur sait-il que la requête provient réellement de l'expéditeur ?
- Fiabilité : Que se passe-t-il si le récepteur est temporairement indisponible ? Comment garantir que l'événement est finalement livré ?
Sécuriser les webhooks avec des payloads signés
Pour empêcher les requêtes falsifiées, l'expéditeur doit signer le payload à l'aide d'un secret partagé. Le récepteur peut ensuite vérifier la signature pour authentifier l'expéditeur. La méthode la plus courante est HMAC (Hash-based Message Authentication Code) avec SHA-256.
Comment fonctionne la signature HMAC
- L'expéditeur et le récepteur partagent une clé secrète (idéalement une chaîne longue et aléatoire).
- Pour chaque webhook, l'expéditeur calcule un HMAC du corps de la requête à l'aide du secret et l'inclut dans un en-tête (par exemple,
X-Signature). - Le récepteur calcule le même HMAC sur le corps reçu et le compare à la valeur de l'en-tête. S'ils correspondent, la requête est authentique.
Il est crucial de signer le corps brut de la requête, et non une version analysée, pour éviter les divergences dues aux différences de sérialisation JSON.
Exemple : signer un payload en Node.js
const crypto = require('crypto');
function signPayload(payload, secret) {
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
// Côté expéditeur :
const payload = JSON.stringify({ event: 'payment.success', amount: 100 });
const signature = signPayload(payload, 'your-secret-key');
// Inclure la signature dans l'en-tête : X-Signature: signature
Vérifier la signature côté récepteur
Du côté récepteur, vous devez vérifier la signature avant de traiter le payload. Utilisez une comparaison à temps constant pour éviter les attaques temporelles.
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));
}
// Dans votre point de terminaison webhook :
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const payload = req.rawBody; // corps brut sous forme de chaîne
if (!verifySignature(payload, signature, 'your-secret-key')) {
return res.status(401).send('Invalid signature');
}
// Traiter l'événement
res.status(200).send('OK');
});
Remarque : Utilisez toujours HTTPS pour éviter les attaques de type man-in-the-middle. Envisagez également d'inclure un horodatage dans le payload signé pour prévenir les attaques par rejeu.
Implémenter des tentatives avec backoff exponentiel
Même avec un récepteur fiable, des problèmes réseau ou des pannes temporaires peuvent entraîner l'échec de la livraison des webhooks. Un expéditeur robuste doit réessayer les livraisons échouées. Cependant, des tentatives naïves peuvent submerger le récepteur ou provoquer un traitement en double. La solution : un backoff exponentiel avec jitter et une limite maximale de tentatives.
Stratégie de nouvelle tentative
- Réessayer en cas d'échec : Réessayez lorsque le récepteur renvoie un code de statut non-2xx ou expire.
- Backoff exponentiel : Augmentez le délai entre les tentatives de manière exponentielle (par exemple, 1s, 2s, 4s, 8s...).
- Jitter : Ajoutez une variation aléatoire au délai pour éviter les problèmes de type « thundering herd ».
- Nombre maximal de tentatives : Fixez une limite (par exemple, 5 tentatives), puis marquez le webhook comme échoué (en alertant éventuellement un administrateur).
Exemple : logique de nouvelle tentative en Node.js
async function deliverWebhook(url, payload, secret, maxRetries = 5) {
const signature = signPayload(payload, secret);
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Signature': signature },
body: payload,
});
if (response.ok) return;
throw new Error(`HTTP ${response.status}`);
} catch (error) {
attempt++;
if (attempt >= maxRetries) throw error;
const delay = Math.min(1000 * 2 ** attempt + Math.random() * 1000, 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Cette fonction réessaie jusqu'à 5 fois avec un backoff exponentiel et un jitter, en plafonnant le délai à 30 secondes. Ajustez les paramètres selon vos besoins.
Garantir l'idempotence
Les tentatives peuvent entraîner des livraisons en double, votre gestionnaire de webhook doit donc être idempotent. Incluez un ID d'événement unique dans le payload et demandez au récepteur de suivre les ID traités pour éviter un double traitement.
// Côté récepteur :
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const { id } = req.body;
if (processedEvents.has(id)) {
return res.status(200).send('Already processed');
}
// Traiter l'événement
processedEvents.add(id);
res.status(200).send('OK');
});
En production, utilisez un stockage persistant (par exemple, Redis ou une base de données) pour suivre les ID traités à travers les redémarrages.
Comparaison : webhooks signés vs non signés
| Aspect | Non signé | Signé (HMAC) |
|---|---|---|
| Sécurité | N'importe qui peut falsifier des requêtes | Seul le détenteur du secret peut créer une signature valide |
| Intégrité | Le payload peut être altéré | Toute altération invalide la signature |
| Complexité | Faible | Modérée (gestion des clés, vérification) |
| Cas d'utilisation | Réseaux internes de confiance | Tout webhook public |
FAQ
Qu'est-ce qu'une signature de webhook ?
Une signature de webhook est un hachage cryptographique (généralement HMAC) du payload, créé avec un secret partagé. Elle permet au récepteur de vérifier que la requête provient de l'expéditeur attendu et que le payload n'a pas été altéré.
Combien de fois dois-je réessayer un webhook échoué ?
Il n'y a pas de nombre fixe, mais la pratique courante est de 3 à 5 tentatives avec un backoff exponentiel. Ensuite, marquez la livraison comme échouée et alertez votre équipe. Tenez compte de la criticité de l'événement : les notifications de paiement peuvent justifier plus de tentatives que les mises à jour non critiques.
Puis-je utiliser des webhooks sans HTTPS ?
Vous ne devriez jamais utiliser de webhooks sur HTTP simple en production. HTTPS chiffre le payload en transit, empêchant l'écoute clandestine et les attaques de type man-in-the-middle. Même avec des signatures, HTTP expose des données sensibles.
Prêt à tester vos payloads de webhook ? Utilisez notre JSON Formatter pour embellir et valider les payloads JSON avant de les envoyer ou de les déboguer.