Vérification des webhooks
Comment vérifier que les webhooks sont authentiques et n'ont pas été manipulés.
Pourquoi vérifier
Toute personne connaissant l'URL de votre webhook pourrait essayer d'envoyer de faux événements. Pour éviter cela, chaque webhook inclut une signature cryptographique que vous devez vérifier.
Ne traitez jamais un webhook sans vérifier sa signature, surtout s'il effectue des actions importantes comme créer des commandes ou modifier des données.
Comment ça fonctionne
- Lors de la création d'un webhook, un secret unique
- Lorsque nous envoyons un webhook, nous calculons un hash HMAC-SHA256 du payload en utilisant votre secret
- Nous incluons ce hash dans le header
X-SalonBookIt-Signature - Votre serveur calcule le même hash et le compare
Format de la signature
X-SalonBookIt-Signature: sha256=abc123def456...
La signature se compose de :
sha256=- Préfixe indiquant l'algorithme- Suivi du hash HMAC-SHA256 en hexadécimal
Obtenir votre secret
- Allez à Configuration → Intégrations
- Dans la section Webhooks, cliquez sur le webhook que vous souhaitez vérifier
- Copiez la valeur du champ Secret
Utilisez des variables d'environnement ou un gestionnaire de secrets. Ne l'incluez jamais dans le code source.
Vérifier la signature
Node.js / JavaScript
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// Extraire le hash de la signature
const sigHash = signature.replace('sha256=', '');
// Calculer le hash attendu
const expectedHash = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
// Comparer de manière sécurisée (évite les timing attacks)
return crypto.timingSafeEqual(
Buffer.from(sigHash, 'hex'),
Buffer.from(expectedHash, 'hex')
);
}
// Dans votre endpoint
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-salonbookit-signature'];
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
console.error('Signature invalide');
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
console.log('Événement valide :', event.type);
// Traiter l'événement...
res.json({ received: true });
});
Python
import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
"""Vérifie la signature HMAC du webhook."""
if not signature.startswith('sha256='):
return False
sig_hash = signature[7:] # Enlever 'sha256='
expected_hash = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
# Comparaison sécurisée contre les timing attacks
return hmac.compare_digest(sig_hash, expected_hash)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-SalonBookIt-Signature', '')
payload = request.get_data()
if not verify_signature(payload, signature, WEBHOOK_SECRET):
abort(401, 'Invalid signature')
event = request.get_json()
print(f"Événement valide : {event['type']}")
# Traiter l'événement...
return {'received': True}
PHP
<?php
$webhookSecret = getenv('WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SALONBOOKIT_SIGNATURE'] ?? '';
function verifySignature(string $payload, string $signature, string $secret): bool
{
if (strpos($signature, 'sha256=') !== 0) {
return false;
}
$sigHash = substr($signature, 7);
$expectedHash = hash_hmac('sha256', $payload, $secret);
return hash_equals($expectedHash, $sigHash);
}
if (!verifySignature($payload, $signature, $webhookSecret)) {
http_response_code(401);
die('Invalid signature');
}
$event = json_decode($payload, true);
error_log('Événement valide : ' . $event['type']);
// Traiter l'événement...
http_response_code(200);
echo json_encode(['received' => true]);
Ruby
require 'openssl'
require 'sinatra'
WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']
def verify_signature(payload, signature, secret)
return false unless signature.start_with?('sha256=')
sig_hash = signature[7..]
expected_hash = OpenSSL::HMAC.hexdigest('sha256', secret, payload)
Rack::Utils.secure_compare(expected_hash, sig_hash)
end
post '/webhook' do
payload = request.body.read
signature = request.env['HTTP_X_SALONBOOKIT_SIGNATURE'] || ''
unless verify_signature(payload, signature, WEBHOOK_SECRET)
halt 401, 'Invalid signature'
end
event = JSON.parse(payload)
puts "Événement valide : #{event['type']}"
# Traiter l'événement...
content_type :json
{ received: true }.to_json
end
Vérifier le timestamp (optionnel)
Pour plus de sécurité, vous pouvez vérifier que le webhook n'est pas trop ancien en utilisant le header X-SalonBookIt-Timestamp:
const timestamp = parseInt(req.headers['x-salonbookit-timestamp']);
const now = Math.floor(Date.now() / 1000);
const tolerance = 300; // 5 minutes
if (Math.abs(now - timestamp) > tolerance) {
console.error('Webhook trop ancien');
return res.status(401).send('Timestamp expired');
}
Cela prévient les attaques de replay où quelqu'un tente de renvoyer un webhook capturé précédemment.
Erreurs courantes
La signature ne correspond pas
- Vérifiez que vous utilisez le bon secret
- Assurez-vous d'utiliser le payload raw (sans parser)
- Ne modifiez pas le payload avant de vérifier
Le payload est modifié
Si votre framework parse automatiquement le JSON, obtenez le body brut avant. Dans Express :
app.use(express.raw({ type: 'application/json' }))
Problèmes d'encodage
Assurez-vous que le payload est traité comme UTF-8 lors du calcul du hash.