Webhooks expliqués : signatures et logique de retry
Vous avez construit une API et devez maintenant notifier d'autres services lorsque des événements se produisent. Vous décidez d'implémenter des webhooks : des callbacks HTTP qui poussent des données vers des URLs d'abonnés. Mais vous êtes rapidement confronté à deux questions critiques : comment garantir que le payload n'a pas été altéré ? Et que se passe-t-il lorsque le serveur destinataire est indisponible ou renvoie une erreur ? Sans protections adéquates, les webhooks peuvent devenir une faille de sécurité et un cauchemar de fiabilité.
Dans ce guide, nous allons parcourir l'essentiel de la sécurisation des webhooks avec des payloads signés et l'implémentation d'une logique de retry robuste. Vous apprendrez à générer et vérifier des signatures HMAC, à concevoir des handlers idempotents et à construire un mécanisme de retry qui gère les échecs avec élégance. À la fin, vous aurez une approche prête pour la production.
Pourquoi les webhooks ont besoin de sécurité et de fiabilité
Les webhooks sont essentiellement des requêtes HTTP de serveur à serveur déclenchées par des événements. Contrairement aux appels API initiés par un client, les webhooks sont poussés depuis votre serveur vers l'endpoint d'un abonné. Ce modèle push introduit deux défis principaux :
- Sécurité : Le payload transite sur internet et pourrait être intercepté ou usurpé. Les abonnés doivent vérifier que la requête provient réellement de votre service.
- Fiabilité : Le serveur de l'abonné peut être temporairement indisponible, lent ou renvoyer une erreur. Vous avez besoin d'une stratégie pour réessayer les livraisons échouées sans provoquer de traitement en double.
Ignorer ces défis peut entraîner des fuites de données, des événements perdus ou des états incohérents entre systèmes. Traitons-les un par un.
Sécuriser les webhooks avec des payloads signés
Un payload signé utilise un hash cryptographique pour prouver l'intégrité et l'authenticité de la requête. L'approche la plus courante est HMAC (Hash-based Message Authentication Code) avec un secret partagé. Voici comment cela fonctionne :
- Secret partagé : L'expéditeur (votre service) et le destinataire (l'abonné) partagent une clé secrète. Cette clé est générée lorsque l'abonné enregistre son endpoint webhook.
- Génération de la signature : Pour chaque requête webhook, l'expéditeur calcule un HMAC du corps de la requête (et parfois des en-têtes comme le timestamp) à l'aide du secret. La signature est envoyée dans un en-tête, par exemple
X-Signature. - Vérification de la signature : Le destinataire recalcule le HMAC avec le même secret et le compare à la signature reçue. S'ils correspondent, le payload est authentique et non modifié.
Voyons un exemple de code en Node.js pour générer et vérifier des signatures.
const crypto = require('crypto');
// Sender: generate signature
function generateSignature(payload, secret) {
return crypto.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
}
// Receiver: verify signature
function verifySignature(payload, secret, receivedSignature) {
const expectedSignature = generateSignature(payload, secret);
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
Important : Utilisez toujours une fonction de comparaison à temps constant comme timingSafeEqual pour éviter les attaques temporelles. Incluez également un timestamp dans les données signées pour prévenir les attaques par rejeu. Par exemple, signez timestamp + '.' + payload et rejetez les requêtes plus anciennes que quelques minutes.
Bonnes pratiques pour les payloads signés
- Utilisez HMAC-SHA256 ou plus fort.
- Incluez un timestamp dans la signature et imposez une ancienneté maximale (par exemple, 5 minutes).
- Ne transmettez jamais le secret dans la requête ; il sert uniquement à signer.
- Faites tourner les secrets périodiquement et prenez en charge plusieurs secrets actifs pendant la rotation.
Implémenter une logique de retry pour une livraison fiable
Même avec la sécurité en place, les webhooks peuvent échouer en raison de problèmes réseau, d'erreurs serveur ou de timeouts. Un mécanisme de retry robuste garantit que les événements finissent par être livrés. Voici une approche étape par étape :
- Définissez un timeout : Définissez un timeout raisonnable pour les requêtes webhook (par exemple, 10 secondes). Si l'abonné ne répond pas à temps, considérez cela comme un échec.
- Classifiez les réponses : Traitez les codes de statut 2xx comme un succès. Pour les erreurs 4xx (sauf 429), réessayer est généralement inutile car la requête est invalide. Pour les erreurs 5xx et les timeouts, réessayez avec un backoff exponentiel.
- Backoff exponentiel avec jitter : Planifiez les retries à des intervalles croissants (par exemple, 1s, 5s, 30s, 5m, 30m) et ajoutez un jitter aléatoire pour éviter les problèmes de thundering herd.
- Limitez les retries : Définissez un nombre maximal de tentatives (par exemple, 5), puis marquez la livraison comme échouée. En option, notifiez l'abonné par email ou via un dashboard.
- Idempotence : Comme les retries peuvent provoquer des livraisons en double, incluez un ID d'événement unique dans le payload et l'en-tête. Les abonnés doivent utiliser cet ID pour dédupliquer le traitement.
Voici une implémentation simplifiée d'une file de retry en Python avec requests et time :
import requests
import time
import random
def deliver_webhook(url, payload, secret, max_retries=5):
signature = generate_signature(payload, secret)
headers = {'X-Signature': signature, 'Content-Type': 'application/json'}
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
if 200 <= response.status_code < 300:
return True
elif response.status_code == 429 or 500 <= response.status_code < 600:
# Retryable error
pass
else:
# Non-retryable error
return False
except requests.exceptions.RequestException:
# Network error, retry
pass
# Exponential backoff with jitter
delay = min(30, 2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
return False
Idempotence : gérer les livraisons en double
Parce que les retries peuvent entraîner la livraison multiple du même événement, les abonnés doivent implémenter un traitement idempotent. L'approche standard consiste à inclure un ID d'événement unique (par exemple, un UUID) dans le payload et à ce que le destinataire stocke les ID traités pendant une certaine période. Si un ID en double est reçu, le destinataire l'acquitte sans le retraiter.
Voici une vérification d'idempotence simple dans un handler de webhook :
processed_events = set()
def handle_webhook(event):
event_id = event['id']
if event_id in processed_events:
return 'already processed'
# Process event
processed_events.add(event_id)
return 'success'
En production, utilisez un stockage persistant comme Redis ou une base de données avec TTL pour suivre les ID traités.
Comparaison : payloads signés vs autres mesures de sécurité
| Approche | Avantages | Inconvénients |
|---|---|---|
| Payloads signés HMAC | Forte intégrité et authenticité ; aucun secret transmis | Nécessite la gestion d'un secret partagé |
| Basic Auth | Simple à implémenter | Identifiants envoyés à chaque requête ; moins sécurisé |
| Liste blanche d'IP | Bloque les sources non autorisées | Les IP peuvent changer ; inadapté aux environnements dynamiques |
| OAuth2 client credentials | Standardisé ; basé sur des tokens | Excessif pour des webhooks simples ; surcharge de gestion des tokens |
Les payloads signés HMAC offrent un bon équilibre entre sécurité et simplicité pour la plupart des cas d'usage de webhooks.
Mettre tout ensemble
Pour construire un système de webhooks fiable et sécurisé :
- Générez un secret unique par abonné et signez chaque payload avec HMAC.
- Incluez un timestamp et un ID d'événement dans les données signées.
- Implémentez des retries avec backoff exponentiel et jitter pour les échecs transitoires.
- Assurez l'idempotence côté destinataire à l'aide des ID d'événement.
- Surveillez les taux de succès des livraisons et alertez en cas d'échec.
Lors du débogage des livraisons de webhooks, inspecter les requêtes et réponses HTTP brutes est inestimable. Si vous travaillez avec des logs Nginx pour tracer les requêtes webhook entrantes, le Nginx Log Analyzer peut vous aider à parser et filtrer les entrées de log pour identifier les livraisons échouées ou les schémas suspects.
FAQ
Qu'est-ce qu'un payload de webhook signé ?
Un payload de webhook signé inclut une signature cryptographique (généralement HMAC) qui permet au destinataire de vérifier l'authenticité et l'intégrité de la requête. L'expéditeur calcule la signature à l'aide d'un secret partagé et l'inclut dans un en-tête ; le destinataire la recalcule et la compare.
Combien de fois dois-je réessayer un webhook échoué ?
Une pratique courante est de 3 à 5 retries avec backoff exponentiel. Ensuite, marquez la livraison comme échouée et notifiez l'abonné. Le nombre exact dépend de votre SLA et de la criticité de l'événement.
Pourquoi l'idempotence est-elle importante pour les webhooks ?
Les retries peuvent provoquer des livraisons en double. L'idempotence garantit que traiter le même événement plusieurs fois a le même effet que le traiter une seule fois, évitant ainsi la corruption de données ou les actions en double.