Webhooks : signatures et nouvelles tentatives
Vous venez d'intégrer une passerelle de paiement et vous devez maintenant mettre à jour votre base de données lorsqu'un paiement réussit. Interroger l'API toutes les quelques secondes est inefficace et lent. Les webhooks résolvent ce problème en poussant les événements vers votre serveur dès qu'ils se produisent. Mais sans sécurité et fiabilité appropriées, les webhooks peuvent devenir une source de bugs et de vulnérabilités. Cet article explique comment implémenter correctement les webhooks, en se concentrant sur les payloads signés et les retries.
Que sont les webhooks ?
Un webhook est un callback HTTP : un événement se produit dans un service, et ce service envoie une requête HTTP POST vers une URL que vous fournissez. Le payload contient généralement des données JSON décrivant l'événement. Par exemple, lorsqu'un client effectue un paiement, Stripe envoie un événement charge.succeeded à votre endpoint.
Les webhooks sont basés sur le push, ce qui réduit la latence et la charge serveur par rapport au polling. Cependant, ils introduisent des défis : comment savoir si la requête est authentique ? Et si votre serveur est indisponible au moment où l'événement se déclenche ?
Pourquoi les payloads signés sont importants
N'importe qui peut envoyer une requête HTTP POST à votre endpoint webhook. Sans vérification, un attaquant pourrait falsifier des événements, entraînant des actions non autorisées comme marquer une commande comme payée. Les payloads signés résolvent ce problème en incluant une signature cryptographique que seul l'émetteur et vous pouvez générer.
La méthode la plus courante est HMAC (Hash-based Message Authentication Code) avec un secret partagé. L'émetteur calcule un hash du payload à l'aide du secret et l'inclut dans un en-tête. Vous recalculez le hash et comparez. S'ils correspondent, la requête est authentique.
Comment vérifier un payload signé
Voici une approche étape par étape avec Node.js et Express :
- Récupérez le corps brut de la requête sous forme de chaîne. Ne le parsez pas en JSON avant la vérification, car le parsing peut altérer la séquence d'octets.
- Extrayez la signature de l'en-tête (par exemple,
X-Signature). - Calculez le HMAC en utilisant votre secret et le corps brut.
- Comparez la signature calculée avec celle reçue en utilisant une comparaison à temps constant pour éviter les attaques temporelles.
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process event
res.status(200).send('OK');
});
Utilisez toujours un secret fort et faites-le tourner périodiquement. Ne l'exposez jamais dans du code côté client.
Implémenter des retries avec backoff
Les webhooks sont livrés via Internet, donc des échecs se produisent. Votre serveur peut être indisponible, ou le réseau peut avoir un hoquet. Un système de webhooks robuste réessaie les livraisons échouées avec un backoff exponentiel.
Généralement, l'émetteur réessaie s'il ne reçoit pas de réponse 2xx dans un délai imparti (par exemple, 5 secondes). Le calendrier de retry peut être : immédiat, puis après 1 minute, 5 minutes, 30 minutes, 2 heures, etc., jusqu'à un nombre maximum de tentatives (par exemple, 5).
En tant que récepteur, vous devez répondre rapidement pour éviter les timeouts. Accusez réception avec un 200 OK et traitez l'événement de manière asynchrone.
Gérer l'idempotence
Parce que les retries peuvent provoquer des livraisons en double, le traitement de vos événements doit être idempotent. Incluez un ID d'événement unique dans le payload et stockez les IDs traités. Avant de traiter, vérifiez si l'ID a déjà été géré.
async function processEvent(event) {
const { id, type, data } = event;
if (await db.processedEvents.findOne({ id })) {
return; // Already processed
}
await db.processedEvents.insertOne({ id });
// Handle event based on type
}
Utilisez une transaction de base de données pour vous assurer que l'événement n'est marqué comme traité qu'après un traitement réussi.
Bonnes pratiques pour la sécurité des webhooks
- Vérifiez toujours les signatures avant le traitement.
- Utilisez HTTPS pour éviter l'interception.
- Validez le timestamp s'il est inclus, pour prévenir les attaques par rejeu.
- Limitez la taille du payload pour éviter les attaques par déni de service.
- Journalisez toutes les tentatives de webhook pour le débogage et l'audit.
Comparaison : Polling vs Webhooks
| Aspect | Polling | Webhooks |
|---|---|---|
| Latence | Élevée (basée sur l'intervalle) | Faible (quasi temps réel) |
| Charge serveur | Élevée (requêtes constantes) | Faible (uniquement sur événements) |
| Complexité | Simple à implémenter | Nécessite sécurité et retries |
| Fiabilité | Dépend de la fréquence de polling | Dépend de la logique de retry |
FAQ
Comment tester les webhooks en local ?
Utilisez un outil comme ngrok pour exposer votre serveur local sur Internet. Configurez le fournisseur de webhook pour envoyer les événements vers votre URL ngrok.
Que faire si mon serveur est indisponible lors de l'envoi d'un webhook ?
L'émetteur doit réessayer avec un backoff. Assurez-vous que votre serveur est hautement disponible et répond rapidement pour éviter les timeouts.
Puis-je utiliser des signatures asymétriques au lieu de HMAC ?
Oui, certains fournisseurs utilisent des signatures RSA ou ECDSA. Vous vérifiez avec leur clé publique. HMAC est plus simple mais nécessite un secret partagé.
Lors du débogage de payloads de webhooks, il est utile de formater le JSON pour le rendre lisible. Essayez notre JSON Formatter pour embellir et valider instantanément les payloads de webhooks.