Validação da origem e consistência dos webhooks.
Para garantir que uma notificação foi realmente enviada pela Grafeno, e não por um terceiro mal-intencionado, cada webhook é assinado. A validação consiste em recalcular a assinatura do lado do cliente e compará-la com a assinatura recebida.
A assinatura é um HMAC-SHA256 calculado sobre o corpo (body) da notificação, usando uma chave secreta compartilhada.
O que você precisa
A validação depende de três elementos:
| Elemento | Origem |
|---|---|
| Chave secreta | Obtida na plataforma após o cadastro da URL de notificação. |
| Assinatura recebida | Enviada no header x-signature de cada webhook. |
| Body da notificação | O corpo (payload) da requisição recebida. |
Importante
Use o corpo bruto (raw). O HMAC deve ser calculado sobre o body exatamente como recebido. Se você desserializar o JSON e serializá-lo novamente (ex.:
json.loadsseguido dejson.dumps), a ordem das chaves e os espaços podem mudar, e a assinatura nunca vai corresponder. Capture o corpo cru da requisição antes de qualquer parsing.
Como funciona
- A Grafeno calcula o HMAC-SHA256 do body usando a chave secreta e envia o resultado no header
x-signature. - Ao receber o webhook, o cliente recalcula o HMAC-SHA256 do body recebido com a mesma chave secreta.
- Se a assinatura recalculada for igual à recebida, a notificação é autêntica. Caso contrário, deve ser rejeitada.
Exemplo em Python
import hmac
import hashlib
import os
# A chave secreta é obtida na plataforma após o cadastro da URL de notificação.
# Mantenha-a fora do código-fonte (ex.: variável de ambiente).
MINHA_CHAVE_SECRETA = os.environ["GRAFENO_WEBHOOK_SECRET"]
def verificar_assinatura(payload: str, assinatura_recebida: str) -> bool:
"""Valida a origem do webhook.
Gera o HMAC-SHA256 do body usando a chave secreta e o compara, de forma
segura (tempo constante), com a assinatura recebida no header x-signature.
"""
assinatura_gerada = hmac.new(
MINHA_CHAVE_SECRETA.encode("utf-8"),
payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
# compare_digest evita ataques de timing (não use == diretamente)
return hmac.compare_digest(assinatura_recebida, assinatura_gerada)
Exemplo de handler (Flask)
O ponto crítico é ler o corpo bruto da requisição (request.get_data()) e a assinatura do header x-signature.
from flask import Flask, request, abort
app = Flask(__name__)
@app.route("/webhooks/grafeno", methods=["POST"])
def receber_webhook():
# Corpo bruto, exatamente como recebido (sem re-serializar)
payload = request.get_data(as_text=True)
assinatura_recebida = request.headers.get("x-signature", "")
if not verificar_assinatura(payload, assinatura_recebida):
# Assinatura inválida: rejeite a requisição
abort(401)
# Assinatura válida: processe o evento com segurança
# evento = request.get_json()
# ...
return "OK", 200
Boas práticas
- Nunca exponha a chave secreta em repositórios, logs ou no front-end. Carregue-a de uma variável de ambiente ou cofre de segredos.
- Use comparação em tempo constante (
hmac.compare_digest) em vez de==, para evitar ataques de timing. - Rejeite (
401) qualquer requisição cuja assinatura não seja validada, antes de processar o conteúdo. - Calcule o HMAC sobre o corpo bruto da requisição, nunca sobre uma versão re-serializada do JSON.