Webhooks expliqués : signatures et logique de retry

Backend2026-10-25TryQuickToolBox

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 :

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 :

  1. 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.
  2. 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.
  3. 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

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 :

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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é :

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.