Skip to Content
WebhooksVérifier les signatures

Vérifier les signatures

N’importe qui connaissant l’URL de votre endpoint peut lui envoyer des requêtes. Skoup signe chaque livraison avec le secret de signature de l’endpoint (whsec_…) : vous pouvez vérifier qu’une requête vient vraiment de Skoup et que son corps n’a pas été modifié en chemin.

L’en-tête de signature

X-Skoup-Signature: t=1790150400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t est le timestamp Unix (secondes) auquel Skoup a signé la livraison.
  • v1 vaut HMAC-SHA256(secret, "{t}.{corps brut}"), encodé en hexadécimal.

Le timestamp fait partie de la chaîne signée : un attaquant ne peut pas rejouer une ancienne livraison avec un timestamp récent.

Pendant une rotation du secret, l’en-tête porte plusieurs valeurs v1, une par secret valide. Une livraison est authentique si l’une d’elles correspond.

Étapes de vérification

  1. Lisez le corps brut de la requête, octet pour octet, avant tout parsing JSON.
  2. Découpez l’en-tête sur , et récupérez t et chaque valeur v1.
  3. Calculez HMAC-SHA256(secret, t + "." + corpsBrut) et encodez-le en hexadécimal.
  4. Comparez-le à chaque valeur v1 avec une comparaison à temps constant.
  5. Rejetez la requête si t s’écarte de plus de 5 minutes de votre horloge.

Cause n°1 des vérifications qui échouent : signer un corps re-sérialisé. Les frameworks qui parsent le JSON avant votre handler modifient espaces et ordre des clés. Signez toujours les octets bruts reçus.

Exemples de code

import crypto from 'node:crypto' import express from 'express' const app = express() const SECRET = process.env.SKOUP_WEBHOOK_SECRET // whsec_… const TOLERANCE_SECONDS = 300 function verifySkoupSignature(rawBody, header, secret) { const parts = header.split(',').map((part) => part.split('=', 2)) const timestamp = parts.find(([key]) => key === 't')?.[1] const signatures = parts.filter(([key]) => key === 'v1').map(([, value]) => value) if (!timestamp || signatures.length === 0) return false if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest() return signatures.some((signature) => { const received = Buffer.from(signature, 'hex') return received.length === expected.length && crypto.timingSafeEqual(received, expected) }) } // express.raw keeps the body as a Buffer: never use express.json() on this route. app.post('/skoup', express.raw({ type: 'application/json' }), (req, res) => { const header = req.get('X-Skoup-Signature') ?? '' if (!verifySkoupSignature(req.body.toString('utf8'), header, SECRET)) { return res.status(400).send('Invalid signature') } const event = JSON.parse(req.body.toString('utf8')) queue.push(event) // process asynchronously res.sendStatus(200) })

Rotation du secret

Sur la page de l’endpoint, Régénérer le secret crée un nouveau secret. Pendant 24 heures, l’ancien reste valide et chaque livraison porte deux signatures v1 — une par secret — : vous déployez le nouveau secret sans perdre un seul event. Au-delà de 24 heures, seul le nouveau signe.

Si un secret a fuité, choisissez Régénérer immédiatement : l’ancien cesse aussitôt de signer.

Déboguer une vérification qui échoue

Chaque livraison, dans Réglages → Développeurs → Webhooks, montre les en-têtes et le corps exacts qui ont été envoyés. Recalculez la signature de ce corps avec votre secret et comparez : si elle correspond, votre code lit un autre corps que celui envoyé par Skoup — en général un corps parsé puis re-sérialisé.

Last updated on