Skip to Content

Chapitre 13.2 — Webhooks : recevoir & émettre

Partie 13 : Interopérabilité — Chapitre 2/5 Version de référence : Symfony 7.4 (composants Webhook & RemoteEvent).

⏱️ TL;DR

  • Recevoir : les composants Webhook + RemoteEvent structurent la réception. Un parser par fournisseur vérifie la signature et transforme la requête en RemoteEvent typé, consommé par un handler (via Messenger).
  • Endpoint unifié /webhook/{fournisseur} ; plus propre que le traitement manuel (le Stripe « à la main » de 12.7 devient déclaratif).
  • Émettre : quand un événement survient dans BookLab (réservation créée), on notifie des tiers (partenaires) en POST vers leurs URLs, payload signé (HMAC), retries via Messenger.
  • Sécurité dans les deux sens : vérifier les webhooks entrants (signature), signer les sortants.
  • Pour un dev Next.js : c’est le pattern webhook que vous connaissez (entrants signés, sortants avec HMAC + retries), mais outillé côté Symfony.

🎯 Objectifs

  • Recevoir des webhooks avec Webhook + RemoteEvent.
  • Vérifier les signatures entrantes.
  • Émettre des webhooks signés vers des tiers.
  • Sécuriser et fiabiliser les deux flux.

1. Recevoir : Webhook + RemoteEvent

Au chapitre 12.7, on a traité le webhook Stripe à la main (lecture du corps, vérif signature, dispatch). Les composants Webhook + RemoteEvent structurent ce travail pour plusieurs fournisseurs, de façon déclarative.

composer require symfony/webhook symfony/remote-event

Le mécanisme :

  1. Le tiers POST vers /webhook/{fournisseur} (route fournie par le composant).
  2. Un RequestParser propre au fournisseur vérifie la signature et transforme la requête en RemoteEvent (un objet typé : nom + payload).
  3. Un consumer (#[AsRemoteEventConsumer('stripe')]) reçoit l’événement — via Messenger (donc async, avec retries).
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer; use Symfony\Component\RemoteEvent\Consumer\ConsumerInterface; use Symfony\Component\RemoteEvent\RemoteEvent; #[AsRemoteEventConsumer('stripe')] final class StripeWebhookConsumer implements ConsumerInterface { public function consume(RemoteEvent $event): void { if ($event->getName() === 'checkout.session.completed') { $bookingId = (int) $event->getPayload()['metadata']['bookingId']; // confirmer la réservation (idempotent, chapitre 12.2) } } }

L’avantage sur le manuel : la vérification et le parsing sont centralisés dans le parser, le consumer se concentre sur le métier, et tout passe par Messenger (fiabilité). On gère ainsi Stripe, les événements Mailer (bounces), et n’importe quel fournisseur, de façon uniforme.

2. Vérifier les signatures entrantes

Chaque RequestParser valide l’authenticité (signature HMAC, secret partagé). C’est non négociable (rappel 12.7) : sans vérification, un attaquant POSTe de faux événements. Le composant rejette une requête non signée/invalide avant qu’elle n’atteigne votre consumer.

3. Émettre : notifier des tiers

BookLab peut jouer le rôle inverse : notifier des partenaires quand un événement survient (une réservation créée, un paiement confirmé) — un webhook sortant. Les partenaires enregistrent une URL, et on POST l’événement chez eux.

final class OutgoingWebhookNotifier { public function __construct( private readonly HttpClientInterface $client, private readonly MessageBusInterface $bus, ) {} public function notify(WebhookSubscription $sub, string $eventName, array $payload): void { // déléguer l'envoi à un worker (retries, isolation) : $this->bus->dispatch(new DeliverWebhook($sub->getId(), $eventName, $payload)); } }

Le handler effectue l’envoi signé :

#[AsMessageHandler] final class DeliverWebhookHandler { public function __invoke(DeliverWebhook $message): void { $sub = $this->subs->find($message->subscriptionId); $body = json_encode(['event' => $message->eventName, 'data' => $message->payload]); // SIGNER le payload (HMAC) : le partenaire pourra vérifier l'origine $signature = hash_hmac('sha256', $body, $sub->getSecret()); $this->client->request('POST', $sub->getUrl(), [ 'headers' => [ 'Content-Type' => 'application/json', 'X-BookLab-Signature' => $signature, 'X-BookLab-Event' => $message->eventName, ], 'body' => $body, ]); // en cas d'échec (4xx/5xx), Messenger réessaie (retries) } }

Points essentiels pour un webhook sortant de qualité :

  • Signer le payload (HMAC-SHA256 avec un secret partagé) → le partenaire vérifie l’origine.
  • Retries via Messenger (le partenaire peut être momentanément down).
  • Idempotence côté partenaire : inclure un event_id unique.
  • Timeout court et journalisation des livraisons (succès/échec).

⚠️ Piège SSRF : quand vous POSTez vers une URL fournie par un tiers (une URL de webhook qu’il a enregistrée), un attaquant pourrait enregistrer une URL interne (http://169.254.169.254/..., un service interne) pour vous faire attaquer votre propre réseau (SSRF). Validez/filtrez les URLs de webhook (schéma https, pas d’IP privées/locales), et isolez le trafic sortant. Ne postez jamais aveuglément vers une URL utilisateur.

4. Le pattern complet pour BookLab

Recevoir et émettre des webhooks fiables (signés, retriés, idempotents) est exactement ce qu’attend une plateforme sérieuse — et un argument de vente (Partie 17).

✏️ Exercices

Exercice 1. Quel avantage le composant Webhook/RemoteEvent apporte-t-il par rapport au traitement manuel du webhook Stripe (chapitre 12.7) ?

✅ Solution

Il structure et centralise : un RequestParser par fournisseur gère la vérification de signature et le parsing en événement typé (RemoteEvent), le consumer se concentre sur le métier, et tout passe par Messenger (async, retries). On traite ainsi plusieurs fournisseurs (Stripe, Mailer, …) de façon uniforme et déclarative, au lieu de dupliquer la logique de vérification/dispatch à la main dans chaque endpoint.

Exercice 2. Pour un webhook sortant, citez trois exigences de qualité.

✅ Solution

(1) Signer le payload (HMAC-SHA256 avec secret partagé) pour que le partenaire vérifie l’origine. (2) Retries via Messenger (le partenaire peut être momentanément indisponible). (3) Idempotence côté partenaire (inclure un event_id unique). Bonus : timeout court, journalisation des livraisons, validation de l’URL (anti-SSRF).

Exercice 3. Pourquoi valider l’URL fournie par un partenaire avant d’y envoyer un webhook ?

✅ Solution

Pour éviter le SSRF : un attaquant pourrait enregistrer une URL interne (métadonnées cloud 169.254.169.254, un service interne) afin de vous faire émettre des requêtes vers votre propre réseau depuis votre serveur. Il faut valider/filtrer l’URL (schéma https, rejeter les IP privées/loopback/link-local) et idéalement isoler le trafic sortant. On ne POST jamais aveuglément vers une URL contrôlée par l’utilisateur.

🧠 Quiz de révision

1. Quels composants structurent la réception de webhooks ?

Webhook (route + parser) et RemoteEvent (événement typé + consumer via Messenger).

2. Que fait un RequestParser ?

Il vérifie la signature du webhook entrant et le transforme en RemoteEvent typé, consommé ensuite par un handler.

3. Comment fiabilise-t-on l’émission d’un webhook sortant ?

Via Messenger (envoi dans un worker avec retries), un payload signé (HMAC) et un event_id pour l’idempotence côté partenaire.

4. Quelle faille guette l’émission vers une URL fournie par un tiers ?

Le SSRF : il faut valider l’URL (https, pas d’IP internes/privées) pour ne pas attaquer son propre réseau.

5. Faut-il vérifier la signature des webhooks entrants ?

Oui, toujours : sans vérification, un attaquant peut POSTer de faux événements. Le parser rejette les requêtes non signées/invalides.


Prochain chapitre : 13.3 — Pont Symfony ↔ WordPress — consommer WordPress en headless.