Introduction
Les webhooks sont des notifications HTTP POST envoyées par un service tiers (comme Stripe, GitHub ou Twilio) vers votre serveur pour signaler des événements en temps réel. Sans protection, n'importe qui peut les falsifier, risquant des injections de données malveillantes ou des abus de ressources.
La validation des signatures résout cela : le service émetteur signe le payload avec une clé secrète partagée via HMAC-SHA256, et votre serveur vérifie cette signature. Ce tutoriel beginner vous guide pas à pas pour implémenter cela en Node.js avec Express.
Pourquoi c'est crucial en 2026 ? Avec la multiplication des API automatisées et des attaques zero-day, une signature valide protège contre 99% des tentatives d'usurpation. À la fin, vous aurez un serveur complet, testable avec curl. Durée : 15 minutes.
Prérequis
- Node.js 20+ installé
- Connaissances basiques de JavaScript (variables, fonctions)
- Un éditeur de code (VS Code recommandé)
- Terminal pour exécuter les commandes
- Pas d'expérience Express requise : tout est expliqué
Initialiser le projet
mkdir webhook-signatures
cd webhook-signatures
npm init -y
npm install express
npm install -D nodemonCette commande crée un dossier projet, initialise package.json, installe Express pour le serveur HTTP et Nodemon pour redémarrer automatiquement lors des changements. Exécutez-la dans votre terminal pour un setup prêt en 30 secondes.
Comprendre les webhooks de base
Un webhook est un POST à /webhook avec un JSON payload. Sans signature, votre serveur l'accepte aveuglément. Imaginez un faux payload déclenchant un virement bancaire : catastrophe !
La signature arrive dans l'en-tête X-Webhook-Signature (format : sha256=hex_hash). Elle est calculée comme crypto.createHmac('sha256', secret).update(payload).digest('hex'). Si elle match, le webhook est authentique.
Serveur webhook basique
const express = require('express');
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
console.log('Payload reçu:', req.body.toString());
res.status(200).send('OK');
});
app.listen(3000, () => {
console.log('Serveur sur http://localhost:3000');
});Ce serveur écoute les POST bruts JSON sur /webhook, log le payload et répond OK. express.raw() est clé : il garde le body sous forme de Buffer pour le hasher plus tard. Testez avec curl pour voir un webhook non sécurisé fonctionner.
Générer une signature pour tester
Pour simuler un service tiers, nous créons une fonction qui génère la signature. Utilisez une clé secrète (stockez-la en env var en prod). Cela mime Stripe ou GitHub.
Fonction générer signature
const crypto = require('crypto');
const secret = 'ma-cle-secrete-super-securisee';
const payload = JSON.stringify({ event: 'user.created', userId: 123 });
const signature = 'sha256=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
console.log('Payload:', payload);
console.log('Signature:', signature);Cette fonction crée un payload JSON, le hache avec HMAC-SHA256 et la préfixe 'sha256='. Exécutez-la avec node generate-signature.js pour obtenir une signature de test. Copiez-les pour le curl suivant : c'est votre "émetteur" simulé.
Valider la signature côté serveur
La magie : comparez la signature reçue avec celle recalculée. Utilisez crypto.timingSafeEqual() pour éviter les timing attacks (où un attaquant mesure le temps de validation).
Fonction vérifier signature
const crypto = require('crypto');
function verifySignature(payload, receivedSignature, secret) {
const expectedSignature = 'sha256=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
);
}
// Test
const payload = Buffer.from('{"event":"test"}');
const sig = 'sha256=computed_hash_here';
const secret = 'ma-cle-secrete-super-securisee';
console.log(verifySignature(payload, sig, secret));Cette fonction recalcule la HMAC du payload reçu, compare byte-à-byte avec timingSafeEqual pour la sécurité. Remplacez computed_hash_here par votre signature générée. Retourne true si valide, false sinon : anti-tampering garanti.
Serveur complet avec validation
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.raw({ type: 'application/json' }));
const WEBHOOK_SECRET = 'ma-cle-secrete-super-securisee';
function verifySignature(payload, receivedSignature) {
const expected = 'sha256=' + crypto.createHmac('sha256', WEBHOOK_SECRET).update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(receivedSignature));
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
if (!signature) {
return res.status(401).send('Signature manquante');
}
if (!verifySignature(req.body, signature)) {
return res.status(401).send('Signature invalide');
}
console.log('Webhook valide:', req.body.toString());
res.status(200).send('OK');
});
app.listen(3000, () => {
console.log('Serveur sécurisé sur http://localhost:3000');
});Serveur final : extrait la signature de l'en-tête, vérifie avec la fonction, traite seulement si valide. Ajoutez votre secret en haut. Lancez avec nodemon server.js. Sans signature valide, 401 : sécurité active !
Tester avec curl valide
curl -X POST http://localhost:3000/webhook \
-H "X-Webhook-Signature: sha256=5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" \
-H "Content-Type: application/json" \
-d '{"event":"payment.succeeded","id":123}'Ce curl simule un webhook valide (adaptez la signature à votre payload/secret via generate-signature.js). Serveur log 'valide' et répond OK. Testez une signature fausse pour voir le 401 : preuve de fonctionnement.
Bonnes pratiques
- Stockez le secret en variable d'environnement :
process.env.WEBHOOK_SECRETau lieu d'hardcodé. - Loggez sans exposer le payload sensible : utilisez un logger structuré comme Winston.
- Limitez la taille du body :
express.json({ limit: '1mb' })contre les DoS. - Ajoutez un timestamp : Vérifiez
X-Webhook-Timestamppour rejeter les anciens (>5min). - Utilisez HTTPS only : Forcez avec un reverse proxy comme Nginx.
Erreurs courantes à éviter
- Parser le body trop tôt :
express.json()modifie le Buffer ; utilisezexpress.raw()pour HMAC. - Comparaison string naive :
===leak via timing attacks ; toujourstimingSafeEqual. - Oublier le préfixe : Signature doit matcher exactement 'sha256=...' .
- Secret faible : Au moins 32 chars aléatoires, régénérez périodiquement.
Pour aller plus loin
- Lisez la doc Stripe Webhooks pour cas réels.
- Implémentez plusieurs signatures (v1,v0) avec
crypto.timingSafeEqualen boucle. - Explorez tRPC ou Hono pour webhooks plus modernes.
- Formations Learni : Maîtrisez Node.js sécurité en profondeur.