Validação de origem

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:

ElementoOrigem
Chave secretaObtida na plataforma após o cadastro da URL de notificação.
Assinatura recebidaEnviada no header x-signature de cada webhook.
Body da notificaçãoO 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.loads seguido de json.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

  1. A Grafeno calcula o HMAC-SHA256 do body usando a chave secreta e envia o resultado no header x-signature.
  2. Ao receber o webhook, o cliente recalcula o HMAC-SHA256 do body recebido com a mesma chave secreta.
  3. 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.