Vérification de signature
Prouver qu'une livraison vient bien de Wajub et n'a pas été rejouée.
Votre URL de webhook est publique : n'importe qui peut y poster un payment.succeeded bien formé.
Vérifier la signature est le seul moyen de distinguer une vraie livraison d'une imitation, et deux
en-têtes la portent.
| En-tête | Ce que c'est |
|---|---|
X-Wajub-Signature | v1= suivi d'un HMAC-SHA256 en hexadécimal |
X-Wajub-Timestamp | Secondes Unix, fait partie de ce qui est signé |
Ce qui est signé
Pas le corps seul. L'horodatage et le corps, joints par un seul point.
signed = "{timestamp}.{raw_body}"
digest = hex(hmac_sha256(signed, endpoint_secret))
header = "v1=" + digestL'horodatage est inclus dans le HMAC exprès : une livraison interceptée ne peut pas être rejouée un jour plus tard, car la déplacer dans le temps casse la signature. Comparer l'horodatage à l'heure actuelle constitue la seconde moitié de cette protection, et les SDKs le font pour vous avec une fenêtre de 300 secondes.
Avec un SDK
Le secret appartient au client, pas à l'appel.
# What Wajub sends you
POST /webhooks/wajub HTTP/1.1
X-Wajub-Signature: v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-Wajub-Timestamp: 1748083260
X-Wajub-Event: payment.succeeded
X-Wajub-Delivery-Id: whd_7Yh2MpL4tRb3nP8sZcXv
Content-Type: application/json
{"id":"evt_…","event":"payment.succeeded","data":{…}}Le secret n'est pas un quatrième argument
constructEvent prend le payload, la signature, l'horodatage, puis une tolérance, jamais
votre secret : le secret se passe au client que vous construisez, une seule fois. La tolérance
est en secondes, ou une time.Duration en Go, une Duration en Java et un TimeSpan en C#. Go et
Java exigent l'argument : passez 0 en Go, ou null en Java, pour la fenêtre par défaut de 300
secondes.
Sans SDK
Trois étapes, dans cet ordre : vérifier le préfixe, recalculer le digest, comparer en temps constant. Ensuite seulement, regardez l'horodatage.
# Reproduce a signature locally from a captured delivery
TIMESTAMP=1748083260
BODY=$(cat delivery.json)
printf '%s.%s' "$TIMESTAMP" "$BODY" \
| openssl dgst -sha256 -hmac "$WAJUB_WEBHOOK_SECRET" -hexLes quatre erreurs classiques
Le corps a été parsé avant d'être hashé
C'est la cause de presque toutes les signatures qui refusent de se vérifier. Votre framework parse le JSON, vous le resérialisez pour le hasher, et les octets reviennent dans un ordre différent ou avec d'autres espaces. Le digest porte sur les octets exacts que nous avons envoyés.
| Framework | Quoi utiliser |
|---|---|
| Express | express.raw({ type: 'application/json' }) sur cette route uniquement |
| Flask | request.get_data(), jamais request.json |
| Laravel | $request->getContent() |
| Go | io.ReadAll(r.Body) avant tout décodeur |
| Route handler Next.js | await request.text() |
La comparaison a levé une exception au lieu de renvoyer false
crypto.timingSafeEqual de Node lève une RangeError quand les deux buffers n'ont pas la même
longueur, et une signature falsifiée a généralement une longueur différente. Sans vérification
préalable de la longueur, votre handler plante précisément sur la requête qu'il devait rejeter.
hash_equals et compare_digest gèrent ce cas eux-mêmes.
Le type d'événement a été lu dans le mauvais champ
Le champ s'appelle `event`
L'enveloppe porte event, pas type. Un switch sur type ne tombe dans aucun cas et rien n'est
livré. Lisez event.event, ou routez sur l'en-tête X-Wajub-Event.
L'horloge a dérivé
La vérification tolère 300 secondes d'écart entre l'horodatage signé et l'horloge de votre serveur. Un conteneur dont l'horloge a dérivé au-delà rejette toutes les livraisons, même avec une signature valide. Si tout s'est mis à échouer d'un coup sans que rien n'ait changé dans votre code, vérifiez NTP avant de vérifier le secret.
Livraisons du cycle de vie Sync
Les événements d'onboarding et de connexion que reçoit une plateforme Sync, dont account.connected,
sont signés de la même façon, avec le même secret d'endpoint, sous leurs propres en-têtes.
| En-tête | Ce que c'est |
|---|---|
X-Sync-Signature-V1 | v1= suivi du HMAC-SHA256 en hexadécimal de {timestamp}.{raw_body} |
X-Sync-Timestamp | Secondes Unix, fait partie de ce qui est signé |
Vérifiez-les comme toute autre livraison, avec la même fenêtre de 300 secondes : passez
X-Sync-Signature-V1 et X-Sync-Timestamp à constructEvent à la place des en-têtes X-Wajub-,
ou à la fonction verify de Sans SDK. Refusez une livraison dont l'horodatage
sort de la fenêtre, même si sa signature correspond. constructEvent renvoie le corps tel quel, et
ces livraisons ont un corps à plat plutôt que l'enveloppe habituelle, décrit dans
le catalogue des événements.
Enregistrez ensuite chaque X-Idempotency-Key traité, et ignorez une livraison dont la clé a déjà
été vue.
X-Sync-Event: account.connected
X-Sync-Timestamp: 1748083260
X-Sync-Signature-V1: v1=5d41f2a8c3e07b9a1f64d2e8b0c7a95e3f18d6b4a2c9e07f1b3d5a8c6e2f4b90
X-Idempotency-Key: 9b74c9897bac770ffc029102a200c5de35d0f7b1a8e7c3b6d4f2a1e0c9b8a7f6Le secret
Chaque endpoint a le sien, qui commence par whsec_, affiché une seule fois à la création de
l'endpoint puis consultable dans Konsole. Le renouveler se fait en un seul appel.
curl -X POST https://api.wajub.com/webhooks/9d3f6a2e-4b1c-4e8a-b7d2-5c9e1f0a3b64/rotate-secret \
-H "Authorization: sk.xxxxxxxxxxxxxxxx…"Wajub génère le nouveau secret : vous ne pouvez donc pas le déployer à l'avance. Il prend effet aussitôt : chaque tentative envoyée ensuite est signée avec lui, y compris les nouvelles tentatives d'événements plus anciens, car une tentative est signée au moment de son envoi. Pour basculer sans perdre de livraison :
- Modifiez votre gestionnaire pour qu'il accepte une signature faite avec l'un ou l'autre de deux secrets, et déployez-le avec le secret actuel seul.
- Renouvelez le secret, et ajoutez le nouveau, affiché une seule fois, comme second secret.
- Quand plus rien n'a été refusé depuis un moment, retirez l'ancien.
Une livraison refusée pendant la bascule n'est pas perdue : elle est retentée, signée avec le nouveau secret.
Pages associées
- Démarrage rapideRecevez d'abord un événement signé sur votre machine.
- Nouvelles tentatives et échecsCe qui se passe quand vous répondez 400.
- Dépannage des webhooksLa signature ne se vérifie jamais, symptôme par symptôme.
- Bonnes pratiques de sécuritéLa place des secrets de webhook parmi vos autres identifiants.