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.

Vérifiez toujours les signatures

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

  1. Lors de la création d'un webhook, un secret unique
  2. Lorsque nous envoyons un webhook, nous calculons un hash HMAC-SHA256 du payload en utilisant votre secret
  3. Nous incluons ce hash dans le header X-SalonBookIt-Signature
  4. 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

  1. Allez à Configuration → Intégrations
  2. Dans la section Webhooks, cliquez sur le webhook que vous souhaitez vérifier
  3. Copiez la valeur du champ Secret
Gardez le secret en sécurité

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

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

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
<?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

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:

JavaScript
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.