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.
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-keyidque 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
- Leia o corpo bruto da requisição e os cabeçalhos
webhook-id,webhook-timestamp,webhook-signatureewebhook-keyid. Se algum deles estiver ausente, rejeite a requisição. - Encontre a chave pública cujo
kidé igual awebhook-keyid, atualizando o cache do JWKS se necessário. Se não houver nenhuma, rejeite a requisição. - Monte o conteúdo assinado:
{webhook-id}.{webhook-timestamp}.{corpo bruto}. - Para cada entrada
v1a,…dewebhook-signature, decodifique a assinatura em base64 e verifique-a contra o conteúdo assinado com a chave pública, usando Ed25519. - 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ãov1a(Ed25519). Não existe segredo compartilhado. - Baixar o JWKS a cada requisição. Mantenha-o em cache e atualize apenas quando surgir um
webhook-keyiddesconhecido. - 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