Webhooks expliqués : charges utiles signées et stratégies de nouvelle tentative
Vous avez construit une API et devez maintenant notifier d'autres services lorsque des événements se produisent. L'interrogation périodique (polling) est inefficace, vous implémentez donc des webhooks. Mais vous êtes rapidement confronté à deux problèmes courants : comment s'assurer que la charge utile n'a pas été altérée, et que faire lorsque le point de terminaison de réception est indisponible. Cet article explique comment utiliser des charges utiles signées et des stratégies de nouvelle tentative pour rendre vos webhooks sécurisés et fiables.
Pourquoi les webhooks ont besoin de signatures et de nouvelles tentatives
Les webhooks sont des callbacks HTTP déclenchés par des événements. Ils sont simples à implémenter mais introduisent des défis de sécurité et de fiabilité. Sans signatures, un attaquant pourrait falsifier des requêtes vers votre point de terminaison, entraînant une corruption des données ou des actions non autorisées. Sans nouvelles tentatives, les défaillances transitoires (comme les coupures réseau ou les erreurs temporaires du serveur) provoquent des événements manqués.
Les charges utiles signées résolvent l'authenticité : l'expéditeur signe la charge utile avec un secret, et le récepteur la vérifie. Les nouvelles tentatives résolvent la fiabilité : l'expéditeur réessaie les livraisons échouées avec un backoff.
Comment fonctionnent les charges utiles signées
Une charge utile signée inclut une signature cryptographique générée à partir du corps de la requête et d'un secret partagé. La méthode la plus courante est HMAC (Hash-based Message Authentication Code) avec SHA-256.
Génération de la signature
L'expéditeur calcule le HMAC du corps brut de la requête en utilisant la clé secrète. La signature est envoyée dans un en-tête, souvent accompagnée d'un horodatage pour prévenir les attaques par rejeu.
// Exemple Node.js : génération de la signature HMAC
const crypto = require('crypto');
function signPayload(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return { timestamp, signature };
}
Incluez l'horodatage et la signature dans les en-têtes, par exemple X-Webhook-Timestamp et X-Webhook-Signature.
Vérification de la signature
Le récepteur doit reconstruire la charge utile signée en utilisant l'horodatage reçu et le corps brut, puis calculer le HMAC et le comparer de manière sécurisée.
// Exemple Node.js : vérification de la signature
function verifySignature(rawBody, timestamp, receivedSignature, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Utiliser une comparaison à temps constant
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
Utilisez toujours une comparaison à temps constant pour éviter les attaques temporelles. Vérifiez également que l'horodatage est récent (par exemple, dans les 5 minutes) pour atténuer les attaques par rejeu.
Conception d'une stratégie de nouvelle tentative
Les nouvelles tentatives sont essentielles pour une livraison fiable des webhooks. Mais des tentatives naïves peuvent submerger le récepteur ou provoquer un traitement en double. Une bonne stratégie de nouvelle tentative comprend :
- Backoff exponentiel : Attendre plus longtemps entre chaque tentative (par exemple, 1s, 2s, 4s, 8s...).
- Nombre maximal de tentatives : Limiter le nombre de tentatives (par exemple, 5) pour éviter les boucles infinies.
- File d'attente de messages morts : Après le nombre maximal de tentatives, stocker l'événement pour inspection manuelle.
- Idempotence : Inclure un ID d'événement unique pour que le récepteur puisse dédupliquer.
Voici une boucle de nouvelle tentative simple en pseudo-code :
function deliverWithRetry(url, payload, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, { method: 'POST', body: payload });
if (response.ok) return;
} catch (error) {
// Enregistrer l'erreur
}
attempt++;
const delay = Math.pow(2, attempt) * 1000; // backoff exponentiel
await new Promise(resolve => setTimeout(resolve, delay));
}
// Passer à la file d'attente de messages morts
}
Envisagez d'ajouter une gigue (jitter) au backoff pour éviter les problèmes de type « troupeau tonnant ».
Comparaison : interrogation périodique vs webhooks avec nouvelles tentatives
| Aspect | Interrogation périodique | Webhooks avec nouvelles tentatives |
|---|---|---|
| Latence | Élevée (dépend de l'intervalle) | Faible (quasi temps réel) |
| Charge serveur | Constante (même sans événement) | Pilotée par les événements |
| Fiabilité | Élevée (contrôlée par le récepteur) | Dépend de la stratégie de nouvelle tentative |
| Sécurité | Authentification API standard | Nécessite des charges utiles signées |
Étape par étape : implémentation de webhooks avec signatures et nouvelles tentatives
- Générez une clé secrète pour chaque point de terminaison de webhook et partagez-la de manière sécurisée.
- Lorsqu'un événement se produit, construisez la charge utile (JSON) et calculez la signature en utilisant HMAC-SHA256 avec le secret et un horodatage.
- Envoyez la requête POST au point de terminaison avec les en-têtes :
Content-Type: application/json,X-Webhook-TimestampetX-Webhook-Signature. - Côté récepteur, vérifiez la signature en utilisant le corps brut et l'horodatage. Rejetez si invalide ou si l'horodatage est trop ancien.
- Si la livraison échoue (réponse non-2xx ou erreur réseau), réessayez avec un backoff exponentiel jusqu'à un nombre maximal de tentatives.
- Après le nombre maximal de tentatives, enregistrez l'échec et alertez éventuellement ; stockez l'événement dans une file d'attente de messages morts pour un rejeu manuel.
- Assurez l'idempotence côté récepteur en utilisant un ID d'événement pour ignorer les doublons.
Pièges courants et bonnes pratiques
- Ne pas utiliser le corps brut : De nombreux frameworks analysent le JSON et modifient le corps. Utilisez le corps brut pour la vérification de la signature.
- Ignorer les horodatages : Sans validation de l'horodatage, les attaques par rejeu sont possibles.
- Pas d'idempotence : Les nouvelles tentatives peuvent causer un traitement en double. Utilisez des clés d'idempotence.
- Nouvelles tentatives infinies : Fixez toujours une limite maximale de tentatives et une file d'attente de messages morts.
- Ne pas journaliser : Journalisez les tentatives de livraison et les échecs pour le débogage.
FAQ
Qu'est-ce qu'une charge utile de webhook signée ?
Une charge utile de webhook signée inclut une signature cryptographique (par exemple, HMAC) qui 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.
Combien de fois dois-je réessayer un webhook échoué ?
Une pratique courante est de 3 à 5 tentatives avec un backoff exponentiel. Le nombre exact dépend de votre SLA et de la criticité de l'événement. Utilisez toujours une file d'attente de messages morts pour les événements qui dépassent la limite de tentatives.
Puis-je utiliser des webhooks sans signatures ?
Ce n'est pas recommandé. Sans signatures, votre point de terminaison est vulnérable aux requêtes falsifiées. Utilisez toujours des signatures (comme HMAC) et validez les horodatages pour prévenir les attaques par rejeu.
Lors du débogage des livraisons de webhooks, l'inspection des journaux du serveur est cruciale. Si vous utilisez Nginx, l'Nginx Log Analyzer peut vous aider à analyser les journaux d'accès pour tracer les requêtes de webhook et identifier les échecs.