Stripe affiche No signatures found matching the expected signature for payload. GitHub affiche We could not verify your signature. Shopify affiche HMAC validation failed. Trois erreurs differentes, mais la cause racine est generalement l une des sept choses suivantes, et 90% du temps c est la premiere.
Cet article classe les sept causes par frequence, avec un fix copiable et une commande de debug locale d une ligne pour chacune.
Resume en 30 secondes
- Signature webhook = HMAC-SHA256(secret, raw_body). La formule ne change jamais; ce qui change c est le format du header et les details d encodage.
- Les 7 causes par frequence: body parse au lieu de raw / mauvais secret / mauvais encodage (hex vs base64) / tolerance timestamp / secret committe dans le code / utilisation de === pour comparer / secret fuite dans les logs
- Chaque fournisseur utilise un format et un encodage differents: GitHub utilise sha256= plus hex, Shopify utilise du base64 brut, Slack utilise v0= plus hex, Stripe utilise t=…,v1= plus hex
- Utilisez le Validateur de Signature Webhook de Piick dans votre navigateur pour reproduire n importe quelle signature localement et voir si elle correspond, les 5 fournisseurs supportes
Les 7 causes, de la plus courante a la plus subtile
Cause 1: Vous verifiez le body parse comme JSON, pas le raw body
C est la cause racine de 50% des echecs de signature. Tous les fournisseurs signent les raw bytes, mais vous signez JSON.stringify(req.body). Express, Flask et Next.js parsent le body par defaut puis le re-stringifient, l ordre des cles ou les espaces changent, et la signature ne correspond plus.
Le fix:
- Express: utilisez express.raw() au lieu de express.json(), dans le handler passez req.body (Buffer) directement au verificateur
- Flask: utilisez request.get_data(as_text=False), pas request.json
- Next.js API route: ajoutez export const config avec api bodyParser false
Debug: imprimez les 30 premiers caracteres de req.body et Content-Length, puis comparez avec les donnees POST originales.
Cause 2: Mauvais secret
En local vous utilisez le secret ephemere du CLI Stripe (imprime par stripe listen). En production vous utilisez celui du dashboard. Le meme endpoint a des strings de secret differentes dans deux environnements.
Le fix: configurez chaque endpoint dans chaque environnement de maniere independante. Lisez les env vars depuis un secret manager. Ne faites jamais de hard-code dans le code.
Debug: verifiez la meme valeur de secret a trois endroits — dashboard, Stripe CLI, env du code — et confirmez qu elles correspondent exactement.
Cause 3: Mauvais encodage
90% des fournisseurs utilisent hex, mais Shopify est le seul a utiliser base64. Si vous envoyez hex a Shopify, la signature ne correspondra jamais.
Le fix: suivez le spec du fournisseur strictement, ne basculez pas hex / base64 manuellement. Utilisez le dropdown de fournisseur du validateur webhook de Piick.
Debug: comparez la longueur du hex genere (64 caracteres) avec la longueur du base64 (44 caracteres). Longueur correcte signifie generalement format correct.
Cause 4: Tolerance timestamp non configuree ou mal configuree (Stripe uniquement)
Stripe a envoye le webhook il y a 5 minutes, mais votre serveur le rejette toujours. Stripe inclut un timestamp dans t=…, et le SDK n accepte que les requetes dans ±5 minutes par defaut.
Le fix: Stripe.webhooks.constructEvent(payload, sig, secret, tolerance=300). 300 secondes suffisent pour la production, mais ne mettez pas trop grand (c est votre fenetre de protection contre le replay).
Debug: imprimez la difference now - t. Si elle est superieure a 300, l horloge de votre serveur derive. Synchronisez NTP.
Cause 5: Secret committe dans le code
Le secret fuit, et un attaquant forge des webhooks avec le vrai secret.
Le fix: rotez le secret immediatement (env var depuis un secret manager). Utilisez git log -p plus grep -i whsec pour trouver les fuites historiques.
Debug: traitez le secret comme un mot de passe — ne le laissez jamais entrer dans le controle de version.
Cause 6: Comparaison des signatures avec ===
Fonctionnellement ca marche, mais theoriquement vulnerable aux attaques de timing distantes. Les attaquants envoient des requetes repetees et mesurent les differences de temps de reponse pour cracker la signature octet par octet. Le crypto.timingSafeEqual de Node et le hmac.compare_digest de Python sont des comparaisons constant-time, immunes a cela.
Le fix:
- Node: crypto.timingSafeEqual(Buffer.from(sig1), Buffer.from(sig2))
- Python: hmac.compare_digest(sig1, sig2)
Debug: grep le code pour === et ==. Ou vous comparez des signatures, passez a la version timing-safe.
Cause 7: Secret fuit dans les logs pendant le debug
Fonctionne en local, echoue pour tous les webhooks en production. Cause: vous avez logue le secret pendant le debug (ou committe), puis l avez rote. Les webhooks signent maintenant avec le nouveau secret, mais l env var de production n a pas ete mise a jour.
Le fix: loguez uniquement le statut de correspondance, pas le secret ni la signature. console.log(sig verify, matched: true).
Debug: git log -p grep pour les mots-cles du secret. Confirmez qu aucun commit historique ne l a fuite.
Comment reproduire n importe quelle signature localement
- Utilisez l onglet Compute du Validateur de Signature Webhook de Piick. Remplissez secret, raw body et fournisseur, le header de signature genere va directement dans votre config webhook.
- Utilisez l onglet Verify pour coller le header recu. Voyez si ca correspond, feedback instantane, pas de console.log ni de redemarrage de service.
- Cet outil est totalement local (Web Crypto API du navigateur), ne telecharge pas payload / secret, sur pour utiliser de vrais secrets pendant le debug.
Cheat sheet format de signature des 5 fournisseurs
| Fournisseur | Header | Encodage | Payload signe |
|---|---|---|---|
| Stripe | Stripe-Signature: t=[ts],v1=[hex] | hex | [ts].[raw body] |
| GitHub | X-Hub-Signature-256: sha256=[hex] | hex | raw body |
| Shopify | X-Shopify-Hmac-SHA256: [base64] | base64 | raw body |
| Slack | X-Slack-Signature: v0=[hex] | hex | v0:[ts]:[raw body] |
| Generic | personnalise | hex | raw body |
- Shopify est le seul a utiliser base64, les autres utilisent hex
- Seuls Stripe et Slack incluent un timestamp
- Seuls GitHub (sha256=) et Slack (v0=) ont un prefixe
- Voir la reference complete en choisissant un fournisseur dans webhook-signature-validator. Chacun vient avec un payload d exemple integre.
Pratiques recommandees
- Utilisez express.raw() / request.get_data() pour traiter les webhooks, jamais JSON.stringify(parsedBody)
- Suivez l encodage du fournisseur strictement. Stripe / GitHub / Slack = hex, Shopify = base64
- Comparez les signatures avec timingSafeEqual / compare_digest, pas avec ===
- Configurez la tolerance timestamp a 5 minutes pour Stripe pour vous defendre des replay attacks
- Les secrets passent toujours par les env vars, jamais dans le code, jamais dans les logs
Vous voulez verifier une signature vous-meme? Utilisez le Validateur de Signature Webhook de Piick dans votre navigateur pour calculer et verifier les signatures, les 5 fournisseurs couverts, les donnees restent dans le navigateur. Combinez-le avec le Decodeur JWT pour une couverture complete de votre stack d outils d auth et securite webhook.