Appearance
Seguridad de Webhooks
Resumen de Seguridad
Quralo implementa un sistema de seguridad de doble capa para garantizar que los webhooks sean auténticos y no hayan sido modificados:
- Autenticación Bearer Token: Verifica la identidad del emisor
- Firma HMAC-SHA256: Garantiza la integridad del payload
Autenticación Bearer Token
Funcionamiento
Cada webhook incluye un token de autenticación en el header Authorization:
http
Authorization: Bearer 7mXMLcEwcnTBOJrjhbOJtw3KGrKhFMcrczEFJfSgImplementación
Su endpoint debe validar este token antes de procesar el evento:
javascript
function authenticateRequest(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Missing Authorization header'
});
}
const token = authHeader.replace('Bearer ', '');
if (token !== process.env.QURALO_AUTH_TOKEN) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid authentication token'
});
}
next();
}Verificación de Firma HMAC
¿Qué es HMAC?
HMAC (Hash-based Message Authentication Code) es un mecanismo que usa una función hash criptográfica junto con una clave secreta para verificar tanto la integridad como la autenticidad de un mensaje.
Proceso de Generación
Quralo genera la firma usando:
- Payload: El cuerpo JSON del webhook (como string)
- Secret: Su clave secreta del webhook
- Algoritmo: HMAC-SHA256
javascript
const signature = crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(payload))
.digest('hex');Header de Firma
La firma se incluye en el header X-Webhook-Signature:
http
X-Webhook-Signature: 5f8d7c6b4a3e2f1a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5Implementación de Verificación
javascript
function validateSignature(body, signature, secret) {
// Generar firma esperada
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(body))
.digest('hex');
// Comparación segura para evitar timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
function validateWebhookSignature(req, res, next) {
const signature = req.headers['x-webhook-signature'];
if (!signature) {
return res.status(400).json({
error: 'Bad Request',
message: 'Missing X-Webhook-Signature header'
});
}
try {
if (!validateSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid webhook signature'
});
}
} catch (error) {
return res.status(400).json({
error: 'Bad Request',
message: 'Invalid signature format'
});
}
next();
}Implementación Completa
Node.js/Express
javascript
const express = require('express');
const crypto = require('crypto');
const app = express();
// Middleware para obtener raw body (necesario para verificar firma)
app.use('/webhook', express.raw({ type: 'application/json' }));
function authenticateAndValidate(req, res, next) {
// 1. Validar token de autenticación
const authHeader = req.headers.authorization;
if (!authHeader || authHeader.replace('Bearer ', '') !== process.env.QURALO_AUTH_TOKEN) {
return res.status(401).json({ error: 'Invalid authentication' });
}
// 2. Validar firma HMAC
const signature = req.headers['x-webhook-signature'];
if (!signature) {
return res.status(400).json({ error: 'Missing signature' });
}
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body, 'utf8')
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Parsear JSON después de validar
req.body = JSON.parse(req.body);
next();
}
app.post('/webhook', authenticateAndValidate, (req, res) => {
// Webhook validado, procesar evento
const eventType = req.headers['x-webhook-event'];
const payload = req.body;
console.log('Evento recibido:', eventType);
console.log('Payload:', payload);
res.status(200).json({ success: true });
});PHP
php
<?php
function validateWebhookSecurity($body, $headers) {
$authToken = $_ENV['QURALO_AUTH_TOKEN'];
$webhookSecret = $_ENV['WEBHOOK_SECRET'];
// Validar token de autenticación
$authHeader = $headers['Authorization'] ?? '';
if (str_replace('Bearer ', '', $authHeader) !== $authToken) {
throw new Exception('Invalid authentication token');
}
// Validar firma HMAC
$signature = $headers['X-Webhook-Signature'] ?? '';
if (empty($signature)) {
throw new Exception('Missing webhook signature');
}
$expectedSignature = hash_hmac('sha256', $body, $webhookSecret);
if (!hash_equals($expectedSignature, $signature)) {
throw new Exception('Invalid webhook signature');
}
}
// Uso
try {
$headers = getallheaders();
$body = file_get_contents('php://input');
validateWebhookSecurity($body, $headers);
$data = json_decode($body, true);
$eventType = $headers['X-Webhook-Event'] ?? '';
// Procesar evento
processWebhookEvent($eventType, $data);
http_response_code(200);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(401);
echo json_encode(['error' => $e->getMessage()]);
}
?>Python/Flask
python
import hashlib
import hmac
import json
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
def validate_webhook_security(body, headers):
auth_token = os.environ.get('QURALO_AUTH_TOKEN')
webhook_secret = os.environ.get('WEBHOOK_SECRET')
# Validar token de autenticación
auth_header = headers.get('Authorization', '')
if auth_header.replace('Bearer ', '') != auth_token:
raise ValueError('Invalid authentication token')
# Validar firma HMAC
signature = headers.get('X-Webhook-Signature', '')
if not signature:
raise ValueError('Missing webhook signature')
expected_signature = hmac.new(
webhook_secret.encode(),
body.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_signature, signature):
raise ValueError('Invalid webhook signature')
@app.route('/webhook', methods=['POST'])
def webhook():
try:
body = request.get_data(as_text=True)
headers = dict(request.headers)
validate_webhook_security(body, headers)
data = json.loads(body)
event_type = headers.get('X-Webhook-Event', '')
# Procesar evento
process_webhook_event(event_type, data)
return jsonify({'success': True})
except ValueError as e:
return jsonify({'error': str(e)}), 401
except Exception as e:
return jsonify({'error': 'Internal server error'}), 500Consideraciones Importantes
Orden de Validación
Siempre valide en este orden:
- ✅ Autenticación (Bearer token)
- ✅ Integridad (Firma HMAC)
- ✅ Estructura (Payload válido)
- ✅ Lógica de negocio (Procesar evento)
Raw Body vs Parsed JSON
⚠️ Crítico: Para verificar la firma HMAC, debe usar el cuerpo HTTP sin parsear (raw body), no el JSON parseado, ya que el parsing puede cambiar el formato y invalidar la firma.
javascript
// ❌ Incorrecto
const parsedBody = req.body; // Ya parseado por Express
const signature = crypto.createHmac('sha256', secret)
.update(JSON.stringify(parsedBody)) // Puede diferir del original
.digest('hex');
// ✅ Correcto
app.use(express.raw({ type: 'application/json' })); // Raw body
const signature = crypto.createHmac('sha256', secret)
.update(req.body, 'utf8') // Body original
.digest('hex');Timing Attacks
Use comparaciones de tiempo constante para evitar timing attacks:
javascript
// ❌ Vulnerable a timing attacks
if (signature === expectedSignature) {
// ...
}
// ✅ Seguro contra timing attacks
if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
// ...
}Gestión de Credenciales
Variables de Entorno
bash
# .env
QURALO_AUTH_TOKEN=7mXMLcEwcnTBOJrjhbOJtw3KGrKhFMcrczEFJfSg
WEBHOOK_SECRET=Fg/vvfO7BUP5THm2dJz4ph/FJyzgDMBXM3DGjKkERotación de Credenciales
- Programada: Rote credenciales cada 90 días
- Emergencia: Si sospecha compromiso
- Proceso: Use el dashboard para regenerar, actualice su sistema
Almacenamiento Seguro
- ✅ Variables de entorno
- ✅ Sistemas de gestión de secretos (AWS Secrets Manager, HashiCorp Vault)
- ✅ Archivos de configuración cifrados
- ❌ Nunca en el código fuente
- ❌ Nunca en logs o bases de datos
Monitoreo de Seguridad
Eventos a Monitorear
- 401 Unauthorized: Intentos de acceso no autorizado
- Múltiples fallos: Posibles ataques de fuerza bruta
- Firmas inválidas: Intentos de manipulación
- IPs sospechosas: Accesos desde ubicaciones inusuales
Alertas Recomendadas
javascript
// Ejemplo de logging de seguridad
function logSecurityEvent(event, details) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
type: 'security',
event: event,
details: details,
ip: req.ip,
userAgent: req.get('User-Agent')
}));
}
// Uso
if (!isValidToken(token)) {
logSecurityEvent('invalid_auth_token', { token: token.substring(0, 8) + '...' });
return res.status(401).json({ error: 'Unauthorized' });
}Siguiente Paso
Con la seguridad implementada, continúe con la documentación de eventos para entender los tipos de datos que recibirá.