Vérifier les signatures des webhooks
Garantir la provenance des webhooks est essentiel. Un attaquant pourrait vous envoyer un payload falsifié (p. ex. {"event":"quote.paid"}) pour que vous livriez quelque chose sans que le paiement soit réel. C’est pourquoi vous devez valider la signature.
Comment Cord signe
Chaque livraison transmet deux signatures du corps brut, afin que vous puissiez migrer sans fenêtre de coupure :
X-Cord-Signature-V1(recommandée) :t=<horodatage unix>,v1=<hmac-sha256 hexadécimal de "{t}.{corps}">. L’horodatage fait partie de ce qui est signé : vous pouvez donc rejeter une ancienne livraison renvoyée (protection anti-rejeu). Pendant une rotation du secret, elle peut contenir deux pairesv1=; il suffit que l’une d’elles corresponde.X-Cord-Signature(ancienne, conservée pour compatibilité) :sha256=<hmac-sha256 hexadécimal du corps>, sans horodatage.
Outre la signature, chaque livraison comporte X-Cord-Event (nom de l’événement), X-Cord-Event-Id (identifiant stable de l’événement, identique lors des nouvelles tentatives et d’un renvoi manuel — utilisez-le pour dédupliquer), X-Cord-Delivery-Id (change à chaque tentative) et Idempotency-Key (reprend la valeur de X-Cord-Event-Id, pour les frameworks qui la lisent automatiquement).
Avec le SDK officiel (recommandé)
@flouviahq/node inclut le vérificateur déjà écrit : il exige X-Cord-Signature-V1, compare en temps constant et rejette une livraison datant de plus de 300 secondes (paramétrable avec tolerance). L’ancienne signature n’est acceptée que si vous la demandez avec allowLegacySignature: true.
import { constructEvent, CordWebhookSignatureError } from '@flouviahq/node';
// Exemple avec un Route Handler Next.js — req.text() est déjà le corps brut
export async function POST(req: Request) {
try {
const event = await constructEvent(await req.text(), req.headers, process.env.CORD_WEBHOOK_SECRET!);
// event.event, event.data — le catalogue des événements figure dans le guide des webhooks
return new Response('ok');
} catch (err) {
if (err instanceof CordWebhookSignatureError) return new Response('Invalid signature', { status: 400 });
throw err;
}
}
npx @flouviahq/cli init crée cette route pour vous, et npx @flouviahq/cli listen vous permet de la tester sur votre machine avec de vraies signatures.
Vérification manuelle en Node.js (Express)
Si vous n’utilisez pas le SDK, vous pouvez vérifier l’ancienne signature manuellement :
const crypto = require('crypto');
app.post('/webhook/cord', express.raw({ type: 'application/json' }), (req, res) => {
const received = req.headers['x-cord-signature'] || ''; // "sha256=<hex>"
const secret = process.env.CORD_WEBHOOK_SECRET;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.body) // req.body est le Buffer brut, non analysé
.digest('hex');
// Comparaison en temps constant (les deux buffers doivent avoir la même longueur)
const ok = received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) return res.status(401).send('Invalid signature');
const event = req.headers['x-cord-event'];
const payload = JSON.parse(req.body.toString('utf8'));
// ... traitez l'événement (payload.data.id, payload.data.folio, etc.)
res.status(200).send('Received');
});
Point clé : signez sur le corps brut (utilisez express.raw, et non express.json), sinon le hachage ne correspondra pas. Pour vérifier X-Cord-Signature-V1 manuellement, calculez le HMAC sur "{timestamp}.{body}" au lieu du seul corps, et vérifiez que l’horodatage se situe dans votre tolérance — ou utilisez le SDK, qui le fait déjà pour vous.