Validacao de Assinatura
Toda requisicao de webhook enviada pelo Tesselys inclui uma assinatura HMAC-SHA256 no header X-Webhook-Signature. Validar essa assinatura e essencial para garantir que o payload foi realmente enviado pelo Tesselys e nao foi adulterado.
Como Funciona
- O Tesselys combina o timestamp e o body da requisicao em uma string
- Gera um hash HMAC-SHA256 usando o secret da assinatura do webhook
- Envia o hash no header
X-Webhook-Signature
Headers relevantes:
| Header | Descricao | Exemplo |
|---|---|---|
X-Webhook-Signature | Assinatura HMAC-SHA256 (prefixo sha256=) | sha256=a1b2c3d4e5f6... |
X-Webhook-Timestamp | Timestamp Unix do envio (segundos) | 1711324800 |
X-Webhook-Id | ID unico do evento | evt_clx123abc456 |
Algoritmo de Validacao
payload_assinado = "{timestamp}.{body}"
assinatura_esperada = HMAC-SHA256(secret, payload_assinado)
comparar(assinatura_recebida, assinatura_esperada)
Passos:
- Extraia o
X-Webhook-Timestampdo header - Concatene o timestamp +
.+ body bruto da requisicao - Calcule o HMAC-SHA256 usando o
secretda assinatura - Compare com o valor recebido em
X-Webhook-Signature(removendo o prefixosha256=) - Opcional: verifique se o timestamp nao e muito antigo (tolerancia de 5 minutos) para prevenir ataques de replay
Implementacao em Node.js
const crypto = require('crypto');
function validateWebhookSignature(req, secret) {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
if (!signature || !timestamp) {
return false;
}
// 1. Verificar tolerancia de tempo (5 minutos)
const currentTime = Math.floor(Date.now() / 1000);
const webhookTime = parseInt(timestamp, 10);
const tolerance = 300; // 5 minutos em segundos
if (Math.abs(currentTime - webhookTime) > tolerance) {
console.warn('Webhook rejeitado: timestamp fora da tolerancia.');
return false;
}
// 2. Calcular assinatura esperada
const rawBody = typeof req.body === 'string'
? req.body
: JSON.stringify(req.body);
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf-8')
.digest('hex');
// 3. Comparar de forma segura (timing-safe)
const receivedSig = signature.replace('sha256=', '');
const expected = Buffer.from(expectedSignature, 'hex');
const received = Buffer.from(receivedSig, 'hex');
if (expected.length !== received.length) {
return false;
}
return crypto.timingSafeEqual(expected, received);
}
// Uso com Express
const express = require('express');
const app = express();
// IMPORTANTE: usar raw body para validacao correta da assinatura
app.post('/webhooks/tesselys', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.TESSELYS_WEBHOOK_SECRET;
const rawBody = req.body.toString('utf-8');
// Sobrescrever body para string crua
req.body = rawBody;
if (!validateWebhookSignature(req, secret)) {
console.error('Assinatura invalida do webhook');
return res.status(401).json({ error: 'Assinatura invalida' });
}
const event = JSON.parse(rawBody);
console.log(`Evento recebido: ${event.event} (ID: ${event.id})`);
// Processar evento de forma assincrona
processWebhookEvent(event).catch(console.error);
// Responder imediatamente com 200
res.status(200).json({ received: true });
});
async function processWebhookEvent(event) {
switch (event.event) {
case 'person.created':
// Sincronizar pessoa no sistema local
break;
case 'deal.won':
// Disparar fluxo de pos-venda
break;
case 'financial.settled':
// Atualizar status de pagamento
break;
default:
console.log(`Evento nao tratado: ${event.event}`);
}
}
app.listen(3000, () => console.log('Webhook listener rodando na porta 3000'));
Implementacao em Python
import hmac
import hashlib
import time
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_a1B2c3D4e5F6g7H8i9J0kLmNoPqRsTuVwXyZ" # Use variavel de ambiente
def validate_webhook_signature(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
"""Valida a assinatura HMAC-SHA256 do webhook Tesselys."""
if not signature or not timestamp:
return False
# 1. Verificar tolerancia de tempo (5 minutos)
current_time = int(time.time())
webhook_time = int(timestamp)
tolerance = 300 # 5 minutos
if abs(current_time - webhook_time) > tolerance:
print("Webhook rejeitado: timestamp fora da tolerancia.")
return False
# 2. Calcular assinatura esperada
signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
expected_signature = hmac.new(
secret.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
# 3. Comparar de forma segura (timing-safe)
received_sig = signature.replace("sha256=", "")
return hmac.compare_digest(expected_signature, received_sig)
@app.route("/webhooks/tesselys", methods=["POST"])
def handle_webhook():
payload = request.get_data()
signature = request.headers.get("X-Webhook-Signature", "")
timestamp = request.headers.get("X-Webhook-Timestamp", "")
if not validate_webhook_signature(payload, signature, timestamp, WEBHOOK_SECRET):
return jsonify({"error": "Assinatura invalida"}), 401
event = json.loads(payload)
print(f"Evento recebido: {event['event']} (ID: {event['id']})")
# Processar evento (idealmente via fila/worker)
process_event(event)
return jsonify({"received": True}), 200
def process_event(event: dict):
event_type = event.get("event")
if event_type == "person.created":
# Sincronizar pessoa no sistema local
pass
elif event_type == "deal.won":
# Disparar fluxo de pos-venda
pass
elif event_type == "financial.settled":
# Atualizar status de pagamento
pass
else:
print(f"Evento nao tratado: {event_type}")
if __name__ == "__main__":
app.run(port=3000)
Troubleshooting
| Problema | Causa provavel | Solucao |
|---|---|---|
| Assinatura sempre invalida | Body foi parseado antes da validacao (middleware JSON) | Use o raw body (bytes) para calcular o HMAC |
| Timestamp fora de tolerancia | Relógio do servidor esta desincronizado | Sincronize com NTP (ntpd ou chrony) |
| Secret incorreto | Secret foi regenerado mas o codigo nao foi atualizado | Atualize o secret na sua aplicacao |
Raw Body
E fundamental usar o body bruto (antes de qualquer parse JSON) para calcular a assinatura. Se seu framework faz parse automatico do JSON, configure o endpoint para receber o body como bytes/string primeiro.