Verificacion de Webhooks
Como verificar que los webhooks son autenticos y no han sido manipulados.
Por que verificar
Cualquiera que conozca la URL de tu webhook podria intentar enviar eventos falsos. Para evitarlo, cada webhook incluye una firma criptografica que debes verificar.
Nunca proceses un webhook sin verificar su firma, especialmente si realiza acciones importantes como crear pedidos o modificar datos.
Como funciona
- Al crear un webhook, se genera un secret unico
- Cuando enviamos un webhook, calculamos un hash HMAC-SHA256 del payload usando tu secret
- Incluimos este hash en el header
X-SalonBookIt-Signature - Tu servidor calcula el mismo hash y lo compara
Formato de la firma
X-SalonBookIt-Signature: sha256=abc123def456...
La firma se compone de:
sha256=- Prefijo que indica el algoritmo- Seguido del hash HMAC-SHA256 en hexadecimal
Obtener tu secret
- Ve a Configuracion → Integraciones
- En la seccion Webhooks, haz clic en el webhook que quieres verificar
- Copia el valor del campo Secret
Usa variables de entorno o un gestor de secretos. Nunca lo incluyas en el codigo fuente.
Verificar la firma
Node.js / JavaScript
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// Extraer el hash de la firma
const sigHash = signature.replace('sha256=', '');
// Calcular el hash esperado
const expectedHash = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
// Comparar de forma segura (evita timing attacks)
return crypto.timingSafeEqual(
Buffer.from(sigHash, 'hex'),
Buffer.from(expectedHash, 'hex')
);
}
// En tu 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('Firma invalida');
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
console.log('Evento valido:', event.type);
// Procesar el evento...
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:
"""Verifica la firma HMAC del webhook."""
if not signature.startswith('sha256='):
return False
sig_hash = signature[7:] # Quitar 'sha256='
expected_hash = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
# Comparacion segura contra 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"Evento valido: {event['type']}")
# Procesar el evento...
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('Evento valido: ' . $event['type']);
// Procesar el evento...
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 "Evento valido: #{event['type']}"
# Procesar el evento...
content_type :json
{ received: true }.to_json
end
Verificar timestamp (opcional)
Para mayor seguridad, puedes verificar que el webhook no sea muy antiguo usando el header X-SalonBookIt-Timestamp:
const timestamp = parseInt(req.headers['x-salonbookit-timestamp']);
const now = Math.floor(Date.now() / 1000);
const tolerance = 300; // 5 minutos
if (Math.abs(now - timestamp) > tolerance) {
console.error('Webhook demasiado antiguo');
return res.status(401).send('Timestamp expired');
}
Esto previene ataques de replay donde alguien intenta reenviar un webhook capturado anteriormente.
Errores comunes
La firma no coincide
- Verifica que estas usando el secret correcto
- Asegurate de usar el payload raw (sin parsear)
- No modifiques el payload antes de verificar
El payload esta modificado
Si tu framework parsea automaticamente el JSON, obtén el body raw antes. En Express:
app.use(express.raw({ type: 'application/json' }))
Problemas de encoding
Asegurate de que el payload se trata como UTF-8 al calcular el hash.