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. Quiconque la trouve peut y poster un payment.succeeded bien
formé et vous regarder livrer une commande que personne n'a payée. La signature est ce qui distingue
une vraie livraison de cette imitation, et c'est la seule chose qui le fasse.
Deux en-têtes portent la preuve.
| 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 la tolérance en secondes.
Passer votre secret à cette quatrième position n'authentifie rien : en Node et en Python, il
atterrit là où un nombre était attendu et la protection contre le rejeu ne fonctionne plus
correctement, et en PHP il lève directement une TypeError. Le secret se passe au client que vous
construisez, une seule fois.
Go est la seule exception à la valeur par défaut : son quatrième argument n'en a pas, passez donc 0
et le SDK applique la même fenêtre 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. Plusieurs README de SDK et le type TypeScript
WebhookEvent de Node indiquent encore type : l'erreur compile sans problème et échoue à
l'exécution, votre switch 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.
Le 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/wh_7Yh2MpL4tRb3nP8sZcXv/rotate-secret \
-H "Authorization: sk.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…"Le nouveau secret prend effet immédiatement : déployez-le donc avant de le renouveler, pas après.
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, et à quelle fréquence.
- 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.