breezeauth.com
PT
EntrarComece grátis
WebhooksVerificar assinaturas

Verificar as assinaturas dos webhooks

Como o BreezeAuth assina cada mensagem seguindo o Standard Webhooks, como o seu endpoint deve conferir a assinatura antes de confiar nela e quais endereços IP liberar.

Nesta página
  1. Standard Webhooks
  2. Cabeçalhos enviados em cada mensagem
  3. O que é assinado
  4. Chaves públicas
  5. Cache e troca de chaves
  6. Passo a passo
  7. Exemplos de código
  8. Node.js
  9. Python
  10. Liberar os endereços IP do BreezeAuth
  11. Sobre o timestamp
  12. Erros comuns

O seu endpoint de webhook precisa estar acessível pela internet para que o BreezeAuth consiga entregar as mensagens. A menos que você restrinja quem pode acessá-lo, qualquer pessoa que descubra a URL pode enviar requisições para ele, inclusive mensagens forjadas. O BreezeAuth oferece duas camadas de proteção:

  • Assinaturas. O BreezeAuth assina cada mensagem com uma chave privada que só o BreezeAuth possui. O seu endpoint confere a assinatura com a chave pública correspondente, que o BreezeAuth publica. Isso é obrigatório.
  • Endereços IP fixos. O BreezeAuth envia os webhooks a partir de um conjunto publicado de endereços IP, que você pode liberar no seu firewall para que requisições de qualquer outra origem nunca cheguem ao seu endpoint. Isso é opcional, e recomendado.

Importante: verifique a assinatura antes de confiar em qualquer coisa da mensagem. Uma mensagem com assinatura ausente ou inválida deve ser rejeitada e nunca processada.

Esta página é técnica. Se você está configurando webhooks no console de administração, compartilhe-a com os desenvolvedores que vão construir o seu endpoint.

Standard Webhooks

O BreezeAuth segue a especificação Standard Webhooks, um padrão aberto para envio de webhooks que define os cabeçalhos, o conteúdo assinado e o formato da assinatura. O BreezeAuth usa o esquema assimétrico da especificação, identificado pelo prefixo v1a, com assinaturas Ed25519.

Assinaturas assimétricas significam que não há um segredo compartilhado para configurar, guardar ou trocar do seu lado: você só precisa das chaves públicas do BreezeAuth, que são públicas por definição.

Além do padrão, o BreezeAuth envia um cabeçalho não padronizado, webhook-keyid, que informa qual chave pública foi usada para assinar a mensagem. Ele torna a busca da chave direta e a troca de chaves transparente.

Cabeçalhos enviados em cada mensagem

Cabeçalho Exemplo Descrição
webhook-id 6c1f0f8e-4f5b-4a53-9d0e-2b7c9a1e3f42 Identificador único da mensagem. Mesmo valor de uuid no corpo, e o mesmo em todas as tentativas.
webhook-timestamp 1791901925 Quando o evento aconteceu, em segundos desde a época Unix. Mesmo instante de timestamp no corpo, e o mesmo em todas as tentativas.
webhook-signature v1a,K5oZfzN95Z9UVu1EsfQmfVNQ… A assinatura, precedida da versão (v1a). Codificada em base64.
webhook-keyid 2026-10-webhooks Extensão do BreezeAuth. Identificador (kid) da chave que gerou a assinatura.
Content-Type application/json O corpo é sempre JSON.

O cabeçalho webhook-signature segue o formato do padrão: uma lista de entradas versão,assinatura separadas por espaço. Hoje o BreezeAuth envia uma única entrada v1a, mas o seu código deve aceitar uma lista e considerar a mensagem válida se qualquer entrada v1a for verificada.

O que é assinado

A assinatura cobre o identificador da mensagem, o timestamp e o corpo exato, unidos por pontos:

{webhook-id}.{webhook-timestamp}.{corpo bruto da requisição}

Por exemplo:

6c1f0f8e-4f5b-4a53-9d0e-2b7c9a1e3f42.1791901925.{"uuid":"6c1f0f8e-4f5b-4a53-9d0e-2b7c9a1e3f42","organizationsUuid":…}

O BreezeAuth assina esses bytes com Ed25519 e coloca o resultado, codificado em base64, em webhook-signature, depois de v1a,.

Importante: use os bytes brutos do corpo exatamente como foram recebidos. Se o seu framework interpretar o JSON e você serializá-lo de novo, espaços ou a ordem das chaves podem mudar e a assinatura deixará de conferir. A maioria dos frameworks oferece uma forma de ler o corpo bruto; os exemplos abaixo mostram como.

Chaves públicas

O BreezeAuth publica as chaves públicas de webhook como um JSON Web Key Set (JWKS) em:

https://well-known.breezeauth.com/webhooks/jwks.json

O arquivo contém uma ou mais chaves Ed25519, cada uma identificada pelo seu kid:

{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "alg": "EdDSA",
      "kid": "2026-10-webhooks",
      "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
    }
  ]
}

Use a chave cujo kid corresponde ao cabeçalho webhook-keyid da mensagem.

Cache e troca de chaves

O BreezeAuth troca as suas chaves de assinatura periodicamente. Novas chaves são publicadas no JWKS antes de serem usadas, e as antigas continuam publicadas por um tempo depois de deixarem de ser usadas. Para lidar com isso sem interrupções:

  • Mantenha o JWKS em cache na memória; não é preciso baixá-lo a cada mensagem.
  • Quando chegar uma mensagem com um webhook-keyid que não está no seu cache, baixe o JWKS de novo uma vez e procure a chave outra vez.
  • Limite essas atualizações, por exemplo a no máximo uma por minuto, para que mensagens forjadas com identificadores de chave aleatórios não façam o seu endpoint sobrecarregar a URL do JWKS.
  • Se a chave continuar desconhecida após a atualização, rejeite a mensagem.

Passo a passo

  1. Leia o corpo bruto da requisição e os cabeçalhos webhook-id, webhook-timestamp, webhook-signature e webhook-keyid. Se algum deles estiver ausente, rejeite a requisição.
  2. Encontre a chave pública cujo kid é igual a webhook-keyid, atualizando o cache do JWKS se necessário. Se não houver nenhuma, rejeite a requisição.
  3. Monte o conteúdo assinado: {webhook-id}.{webhook-timestamp}.{corpo bruto}.
  4. Para cada entrada v1a,… de webhook-signature, decodifique a assinatura em base64 e verifique-a contra o conteúdo assinado com a chave pública, usando Ed25519.
  5. Se nenhuma entrada for verificada, rejeite a requisição com um status 4xx. Caso contrário, a mensagem é legítima: continue com as verificações descritas em Receber eventos com confiabilidade.

Exemplos de código

Muitas bibliotecas de Standard Webhooks implementam apenas o esquema simétrico v1 (HMAC com segredo compartilhado). A menos que a sua biblioteca suporte explicitamente v1a com chaves públicas Ed25519, verifique a assinatura com a biblioteca de criptografia da sua linguagem, como nos exemplos abaixo.

Node.js

Usa apenas o módulo nativo node:crypto (Node.js 18 ou superior) e o Express.

import express from 'express'
import { createPublicKey, verify } from 'node:crypto'

const JWKS_URL = 'https://well-known.breezeauth.com/webhooks/jwks.json'
const JWKS_REFRESH_COOLDOWN_MS = 60_000

let publicKeys = new Map()
let lastRefreshAt = 0

async function getPublicKey(keyId) {
  if (!publicKeys.has(keyId) && Date.now() - lastRefreshAt > JWKS_REFRESH_COOLDOWN_MS) {
    // Chave desconhecida: o BreezeAuth pode ter trocado as chaves, então atualize o cache (no máximo uma vez por minuto).
    lastRefreshAt = Date.now()
    const { keys } = await fetch(JWKS_URL).then((response) => response.json())
    publicKeys = new Map(keys.map((jwk) => [jwk.kid, createPublicKey({ key: jwk, format: 'jwk' })]))
  }
  return publicKeys.get(keyId)
}

export async function isValidSignature(rawBody, headers) {
  const id = headers['webhook-id']
  const timestamp = headers['webhook-timestamp']
  const signatureHeader = headers['webhook-signature']
  const keyId = headers['webhook-keyid']
  if (!id || !timestamp || !signatureHeader || !keyId) return false

  const publicKey = await getPublicKey(keyId)
  if (!publicKey) return false

  const signedContent = Buffer.concat([Buffer.from(`${id}.${timestamp}.`), rawBody])

  // O cabeçalho pode conter várias assinaturas separadas por espaço; basta uma assinatura v1a válida.
  return signatureHeader.split(' ').some((entry) => {
    const [version, signature] = entry.split(',')
    return version === 'v1a' && !!signature && verify(null, signedContent, publicKey, Buffer.from(signature, 'base64'))
  })
}

const app = express()

// express.raw() mantém o corpo como Buffer, exatamente como foi recebido.
app.post('/webhooks/breezeauth', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!(await isValidSignature(req.body, req.headers))) {
    return res.status(401).end()
  }

  const event = JSON.parse(req.body.toString('utf8'))
  // Próximos passos: conferir event.organizationsUuid, ignorar duplicatas, enfileirar, responder 2xx.
  // Veja "Receber eventos com confiabilidade".
  res.status(204).end()
})

Python

Usa Flask, requests e cryptography.

import base64
import json
import time

import requests
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from flask import Flask, request

JWKS_URL = "https://well-known.breezeauth.com/webhooks/jwks.json"
JWKS_REFRESH_COOLDOWN_SECONDS = 60

public_keys = {}
last_refresh_at = 0.0


def b64url_decode(value):
    return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))


def get_public_key(key_id):
    global public_keys, last_refresh_at
    if key_id not in public_keys and time.time() - last_refresh_at > JWKS_REFRESH_COOLDOWN_SECONDS:
        # Chave desconhecida: o BreezeAuth pode ter trocado as chaves, então atualize o cache (no máximo uma vez por minuto).
        last_refresh_at = time.time()
        jwks = requests.get(JWKS_URL, timeout=5).json()
        public_keys = {
            jwk["kid"]: Ed25519PublicKey.from_public_bytes(b64url_decode(jwk["x"]))
            for jwk in jwks["keys"]
            if jwk.get("kty") == "OKP" and jwk.get("crv") == "Ed25519"
        }
    return public_keys.get(key_id)


def is_valid_signature(raw_body, headers):
    message_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signature_header = headers.get("webhook-signature")
    key_id = headers.get("webhook-keyid")
    if not all([message_id, timestamp, signature_header, key_id]):
        return False

    public_key = get_public_key(key_id)
    if public_key is None:
        return False

    signed_content = f"{message_id}.{timestamp}.".encode() + raw_body

    # O cabeçalho pode conter várias assinaturas separadas por espaço; basta uma assinatura v1a válida.
    for entry in signature_header.split(" "):
        version, _, signature = entry.partition(",")
        if version != "v1a" or not signature:
            continue
        try:
            public_key.verify(base64.b64decode(signature), signed_content)
            return True
        except (InvalidSignature, ValueError):
            continue
    return False


app = Flask(__name__)


@app.post("/webhooks/breezeauth")
def breezeauth_webhook():
    raw_body = request.get_data()  # o corpo exatamente como foi recebido
    if not is_valid_signature(raw_body, request.headers):
        return "", 401

    event = json.loads(raw_body)
    # Próximos passos: conferir event["organizationsUuid"], ignorar duplicatas, enfileirar, responder 2xx.
    # Veja "Receber eventos com confiabilidade".
    return "", 204

Liberar os endereços IP do BreezeAuth

Para uma camada extra de proteção, configure o seu firewall, balanceador de carga ou firewall de aplicação web (WAF) para aceitar requisições ao seu endpoint de webhook somente dos endereços IP de onde o BreezeAuth envia os webhooks (uma lista de permissões, ou allowlist):

3.226.82.18
3.227.201.231

Assim, requisições de qualquer outro endereço são bloqueadas antes de chegar à sua aplicação. Isso esconde o seu endpoint do resto da internet, elimina tráfego indesejado e barra mensagens forjadas antes que o seu código precise rejeitá-las. Se o firewall é gerenciado pela sua equipe de rede ou de TI, compartilhe esta lista com ela.

Importante: a lista de permissões complementa a verificação de assinatura; ela não a substitui. Continue verificando todas as assinaturas e conferindo a organização: as mensagens de todas as organizações do BreezeAuth vêm desses mesmos endereços.

Observação: hoje são dois endereços, e outros podem ser adicionados conforme a infraestrutura do BreezeAuth cresce. Mantenha a sua lista de permissões fácil de atualizar e consulte esta página regularmente. Se as requisições do BreezeAuth forem bloqueadas, as entregas falham e aparecem no histórico de eventos como tempo esgotado ou erro de conexão.

Se o seu endpoint fica atrás de um proxy reverso, CDN ou balanceador de carga, aplique a regra onde o endereço original do cliente é visível: na própria borda, ou usando o endereço do cliente repassado por um proxy que você controla. Nunca confie em um cabeçalho X-Forwarded-For vindo diretamente da internet, porque qualquer pessoa pode defini-lo.

Sobre o timestamp

A especificação Standard Webhooks sugere rejeitar mensagens com timestamp muito antigo, como proteção contra mensagens reenviadas por terceiros (replay). Se você aplicar essa tolerância, lembre-se de que webhook-timestamp é o momento em que o evento aconteceu, e não o momento da tentativa de entrega, e que ele permanece o mesmo em todas as novas tentativas. Use uma janela generosa, de pelo menos alguns minutos, para que novas tentativas legítimas não sejam rejeitadas, e use a detecção de duplicatas como a sua principal proteção contra reenvios.

Erros comuns

  • Verificar um corpo serializado novamente. Sempre verifique os bytes brutos, antes de qualquer interpretação do JSON.
  • Usar uma biblioteca v1 (HMAC). As assinaturas do BreezeAuth são v1a (Ed25519). Não existe segredo compartilhado.
  • Baixar o JWKS a cada requisição. Mantenha-o em cache e atualize apenas quando surgir um webhook-keyid desconhecido.
  • Depender apenas da lista de permissões de IP. Ela reduz a exposição, mas só a assinatura prova que a mensagem é legítima e não foi alterada.
  • Confiar na mensagem logo após verificar a assinatura. A assinatura prova que a mensagem vem do BreezeAuth; você ainda precisa conferir a organização a que ela se destina.

Última atualização: 13 de outubro de 2026