Como validar o webhook
Introduçãoā
Quando você cria um webhook através do endpoint POST /webhooks, você define um secret (chave secreta) que serÔ usado para assinar todas as requisições enviadas pela API para o seu endpoint.
Esta assinatura garante que as requisições recebidas no seu webhook são autênticas e foram realmente enviadas pela WPP-API, protegendo sua aplicação contra falsificações.
Como funciona a validaçãoā
Todas as requisições de webhook enviadas pela WPP-API incluem um cabeçalho HTTP chamado x-signature que contém a assinatura HMAC-SHA256 do corpo da requisição.
Para validar a autenticidade da requisição, você deve:
- Extrair o cabeƧalho
x-signatureda requisição - Calcular a assinatura HMAC-SHA256 do corpo da requisição usando o
secretdefinido na criação do webhook - Comparar a assinatura calculada com o valor do cabeçalho
x-signature - Se as assinaturas coincidirem, a requisição é autêntica
Implementaçãoā
Aqui estão exemplos de como implementar a validação do webhook em diferentes linguagens:
- Node.js
- Python
- PHP
- Java
- Go
- C#
const crypto = require("crypto");
function validateWebhook(request, secret) {
// Extrair a assinatura do cabeƧalho
const signature = request.headers["x-signature"];
if (!signature) {
throw new Error("Header x-signature não encontrado");
}
// Obter o corpo da requisição como string
const body = JSON.stringify(request.body);
// Calcular a assinatura HMAC-SHA256
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
// Comparar as assinaturas usando comparação segura
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
if (!isValid) {
throw new Error("Assinatura invƔlida");
}
return true;
}
// Exemplo de uso com Express
const express = require("express");
const app = express();
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const secret = "seu_secret_aqui"; // O secret definido ao criar o webhook
try {
// Importante: usar req.body como Buffer
const body = req.body.toString("utf8");
const signature = req.headers["x-signature"];
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
if (signature !== expectedSignature) {
return res.status(401).json({ error: "Assinatura invƔlida" });
}
// Assinatura vƔlida, processar o webhook
const payload = JSON.parse(body);
console.log("Webhook vƔlido recebido:", payload);
res.status(200).json({ success: true });
} catch (error) {
console.error("Erro ao validar webhook:", error);
res.status(400).json({ error: error.message });
}
});
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
def validate_webhook(body: str, signature: str, secret: str) -> bool:
"""
Valida a assinatura do webhook
Args:
body: Corpo da requisição como string
signature: Assinatura do cabeƧalho x-signature
secret: Secret definido na criação do webhook
Returns:
True se a assinatura for vƔlida, False caso contrƔrio
"""
# Calcular a assinatura esperada
expected_signature = hmac.new(
secret.encode('utf-8'),
body.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Comparar as assinaturas de forma segura
return hmac.compare_digest(signature, expected_signature)
# Exemplo com Flask
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
secret = 'seu_secret_aqui' # O secret definido ao criar o webhook
# Obter o corpo da requisição como string
body = request.get_data(as_text=True)
# Extrair a assinatura do cabeƧalho
signature = request.headers.get('x-signature')
if not signature:
return jsonify({'error': 'Header x-signature não encontrado'}), 401
# Validar a assinatura
if not validate_webhook(body, signature, secret):
return jsonify({'error': 'Assinatura invƔlida'}), 401
# Assinatura vƔlida, processar o webhook
payload = json.loads(body)
print('Webhook vƔlido recebido:', payload)
return jsonify({'success': True}), 200
if __name__ == '__main__':
app.run(port=3000)
<?php
function validateWebhook($body, $signature, $secret) {
// Calcular a assinatura esperada
$expectedSignature = hash_hmac('sha256', $body, $secret);
// Comparar as assinaturas de forma segura
if (!hash_equals($expectedSignature, $signature)) {
throw new Exception('Assinatura invƔlida');
}
return true;
}
// Exemplo de uso
$secret = 'seu_secret_aqui'; // O secret definido ao criar o webhook
// Obter o corpo da requisição
$body = file_get_contents('php://input');
// Extrair a assinatura do cabeƧalho
$headers = getallheaders();
$signature = $headers['x-signature'] ?? null;
if (!$signature) {
http_response_code(401);
echo json_encode(['error' => 'Header x-signature não encontrado']);
exit;
}
try {
// Validar a assinatura
validateWebhook($body, $signature, $secret);
// Assinatura vƔlida, processar o webhook
$payload = json_decode($body, true);
error_log('Webhook vƔlido recebido: ' . print_r($payload, true));
http_response_code(200);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(401);
echo json_encode(['error' => $e->getMessage()]);
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Arrays;
public class WebhookValidator {
public static boolean validateWebhook(String body, String signature, String secret)
throws Exception {
// Calcular a assinatura esperada
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256");
sha256Hmac.init(secretKey);
byte[] hash = sha256Hmac.doFinal(body.getBytes("UTF-8"));
String expectedSignature = bytesToHex(hash);
// Comparar as assinaturas de forma segura
return MessageDigest.isEqual(
signature.getBytes("UTF-8"),
expectedSignature.getBytes("UTF-8")
);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
// Exemplo com Spring Boot
@RestController
public class WebhookController {
private static final String SECRET = "seu_secret_aqui";
@PostMapping("/webhook")
public ResponseEntity<?> handleWebhook(
@RequestBody String body,
@RequestHeader("x-signature") String signature) {
try {
if (!validateWebhook(body, signature, SECRET)) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Assinatura invƔlida"));
}
// Assinatura vƔlida, processar o webhook
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> payload = mapper.readValue(body, Map.class);
System.out.println("Webhook vƔlido recebido: " + payload);
return ResponseEntity.ok(Map.of("success", true));
} catch (Exception e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", e.getMessage()));
}
}
}
}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
)
func validateWebhook(body []byte, signature string, secret string) bool {
// Calcular a assinatura esperada
h := hmac.New(sha256.New, []byte(secret))
h.Write(body)
expectedSignature := hex.EncodeToString(h.Sum(nil))
// Comparar as assinaturas de forma segura
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
const secret = "seu_secret_aqui" // O secret definido ao criar o webhook
// Ler o corpo da requisição
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Erro ao ler o corpo da requisição", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Extrair a assinatura do cabeƧalho
signature := r.Header.Get("x-signature")
if signature == "" {
http.Error(w, "Header x-signature não encontrado", http.StatusUnauthorized)
return
}
// Validar a assinatura
if !validateWebhook(body, signature, secret) {
http.Error(w, "Assinatura invƔlida", http.StatusUnauthorized)
return
}
// Assinatura vƔlida, processar o webhook
var payload map[string]interface{}
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, "Erro ao decodificar JSON", http.StatusBadRequest)
return
}
log.Printf("Webhook vƔlido recebido: %+v", payload)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
log.Println("Servidor rodando na porta 3000")
log.Fatal(http.ListenAndServe(":3000", nil))
}
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;
public class WebhookValidator
{
public static bool ValidateWebhook(string body, string signature, string secret)
{
// Calcular a assinatura esperada
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
{
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));
var expectedSignature = BitConverter.ToString(hash)
.Replace("-", "")
.ToLower();
// Comparar as assinaturas de forma segura
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(expectedSignature)
);
}
}
}
// Exemplo com ASP.NET Core
[ApiController]
[Route("webhook")]
public class WebhookController : ControllerBase
{
private const string Secret = "seu_secret_aqui";
[HttpPost]
public async Task<IActionResult> HandleWebhook()
{
// Ler o corpo da requisição
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
// Extrair a assinatura do cabeƧalho
if (!Request.Headers.TryGetValue("x-signature", out var signature))
{
return Unauthorized(new { error = "Header x-signature não encontrado" });
}
// Validar a assinatura
if (!WebhookValidator.ValidateWebhook(body, signature, Secret))
{
return Unauthorized(new { error = "Assinatura invƔlida" });
}
// Assinatura vƔlida, processar o webhook
var payload = System.Text.Json.JsonSerializer.Deserialize<object>(body);
Console.WriteLine($"Webhook vƔlido recebido: {payload}");
return Ok(new { success = true });
}
}
Pontos importantesā
1. Use o corpo bruto da requisiçãoā
à fundamental usar o corpo exato da requisição HTTP para calcular a assinatura. Não use uma versão parseada e depois serializada do JSON, pois isso pode alterar a formatação e invalidar a assinatura.
ā Incorreto:
const body = JSON.stringify(req.body); // Pode ter formatação diferente
ā Correto:
// Use middleware que preserve o corpo bruto
app.use(express.raw({ type: "application/json" }));
const body = req.body.toString("utf8");
2. Comparação segura de stringsā
Sempre use funções de comparação de tempo constante para evitar ataques de timing:
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - PHP:
hash_equals() - Java:
MessageDigest.isEqual() - Go:
hmac.Equal() - C#:
CryptographicOperations.FixedTimeEquals()
3. Armazene o secret de forma seguraā
Nunca exponha o secret do webhook no código-fonte ou em repositórios públicos. Use variÔveis de ambiente ou serviços de gerenciamento de secrets:
# .env
WEBHOOK_SECRET=seu_secret_super_seguro_aqui
4. Tratamento de errosā
Sempre retorne status HTTP apropriados:
- 401 Unauthorized: Quando a assinatura for invƔlida ou o header estiver ausente
- 400 Bad Request: Quando o corpo da requisição for invÔlido
- 200 OK: Quando o webhook for processado com sucesso
Testando a validaçãoā
Você pode testar sua implementação gerando manualmente uma assinatura:
# Exemplo usando openssl
echo -n '{"test":"data"}' | openssl dgst -sha256 -hmac "seu_secret_aqui"
Ou usando um script Node.js:
const crypto = require("crypto");
const body = JSON.stringify({ test: "data" });
const secret = "seu_secret_aqui";
const signature = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
console.log("Signature:", signature);
console.log("Body:", body);
console.log("\nCurl command:");
console.log(`curl -X POST http://localhost:3000/webhook \\
-H "Content-Type: application/json" \\
-H "x-signature: ${signature}" \\
-d '${body}'`);
Próximos passosā
Agora que vocĆŖ sabe como validar webhooks, confira:
- Criar um webhook - Como criar e configurar webhooks
- Eventos disponĆveis - Lista completa de eventos que vocĆŖ pode receber
- Listar webhooks - Como consultar seus webhooks configurados