Pular para o conteúdo

Verificar a assinatura

Verifique a assinatura de toda entrega antes de processar. Sem isso, qualquer pessoa que descubra a sua URL pode forjar um transaction.paid.

  1. Leia o corpo cru, byte a byte, antes de qualquer parse de JSON. Reserializar o JSON muda espaços e ordem e invalida a assinatura.
  2. Do cabeçalho X-Auryon-Signature, pegue o t (segundos Unix) e todos os v1.
  3. Monte "<t>.<corpo cru>" e calcule HMAC-SHA256 com a chave do webhook, em hexadecimal.
  4. Aceite se algum v1 for igual, comparando em tempo constante.
  5. Recuse se t estiver a mais de 5 minutos do seu relógio: é a proteção contra reenvio de uma entrega capturada (replay).

A chave do webhook (whsec_…) aparece uma única vez, quando o webhook é criado ou a chave é rotacionada.

import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
/**
* Verifies the X-Auryon-Signature header of a webhook delivery.
* rawBody: the request body exactly as received (string or Buffer), before any JSON parsing.
*/
export function verifyAuryon(rawBody, header, secret, nowSeconds = Math.floor(Date.now() / 1000)) {
let timestamp = null;
const signatures = [];
for (const part of String(header ?? '').split(',')) {
const [key, value = ''] = part.trim().split('=', 2);
if (key === 't' && /^\d+$/.test(value)) timestamp = Number(value);
if (key === 'v1') signatures.push(value);
}
if (timestamp === null || Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS) return false;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest('hex');
return signatures.some(
(s) => s.length === expected.length && timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
);
}

Com Express, leia o corpo cru só nesta rota:

app.post('/webhooks/auryon', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyAuryon(req.body, req.get('X-Auryon-Signature'), process.env.AURYON_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
// enfileire `event` e responda logo
res.sendStatus(200);
});
<?php
const AURYON_TOLERANCE_SECONDS = 300;
/**
* Verifies the X-Auryon-Signature header of a webhook delivery.
* $rawBody: the request body exactly as received — file_get_contents('php://input').
*/
function verify_auryon(string $rawBody, string $header, string $secret, ?int $now = null): bool
{
$now = $now ?? time();
$timestamp = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
if ($key === 't' && ctype_digit($value)) {
$timestamp = (int) $value;
}
if ($key === 'v1') {
$signatures[] = $value;
}
}
if ($timestamp === null || abs($now - $timestamp) > AURYON_TOLERANCE_SECONDS) {
return false;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
foreach ($signatures as $signature) {
if (hash_equals($expected, $signature)) {
return true;
}
}
return false;
}
$raw = file_get_contents('php://input');
if (!verify_auryon($raw, $_SERVER['HTTP_X_AURYON_SIGNATURE'] ?? '', getenv('AURYON_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($raw, true);
http_response_code(200);
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify_auryon(raw_body: bytes, header: str, secret: str, now: int | None = None) -> bool:
"""Verifies the X-Auryon-Signature header of a webhook delivery.
raw_body: the request body exactly as received (bytes), before any JSON parsing.
"""
now = int(time.time()) if now is None else now
timestamp = None
signatures = []
for part in header.split(","):
key, _, value = part.strip().partition("=")
if key == "t" and value.isdigit():
timestamp = int(value)
elif key == "v1":
signatures.append(value)
if timestamp is None or abs(now - timestamp) > TOLERANCE_SECONDS:
return False
expected = hmac.new(
secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return any(hmac.compare_digest(expected, s) for s in signatures)

Com Flask:

@app.post("/webhooks/auryon")
def auryon():
raw = request.get_data()
if not verify_auryon(raw, request.headers.get("X-Auryon-Signature", ""), os.environ["AURYON_WEBHOOK_SECRET"]):
abort(400)
event = json.loads(raw)
return "", 200

Ao rotacionar no painel, durante 24 horas a Auryon assina com a chave nova e com a anterior: o cabeçalho leva dois v1=, o da chave nova primeiro. Por isso a regra é aceitar qualquer v1 que bata. Troque a chave no seu servidor dentro das 24 horas, sem perder nenhum evento.

No formato legado não há essa janela: o token vai no corpo, e a partir da rotação só o novo é enviado.

Use estes valores para testar a sua implementação. Com now igual ao timestamp, a verificação do body abaixo com secret deve dar verdadeiro; com qualquer byte do corpo alterado, ou com now mais de 300 segundos depois, falso. Com signature_header_during_rotation, as duas chaves (secret e previous_secret) devem passar.

{
"secret": "whsec_exemplo",
"previous_secret": "whsec_anterior",
"timestamp": 1781524800,
"body": "{\"id\":\"evt_01JXEXEMPLO0000000000000000\",\"api_version\":\"2026-06-15\",\"type\":\"transaction.paid\",\"created\":1781524800,\"livemode\":true,\"data\":{\"object\":{\"id\":\"txn_exemplo_123\",\"status\":\"APROVADA\",\"payment_method\":\"PIX\",\"payment_method_detail\":null,\"amount\":\"19900\",\"shipping_amount\":\"0\",\"currency\":\"BRL\",\"installments\":1,\"identifier\":\"SEU-PEDIDO-123\",\"metadata\":{\"origem\":\"exemplo\"},\"offer_code\":\"ABC12345\",\"product\":{\"name\":\"Produto de exemplo\"},\"offer\":{\"name\":\"Oferta principal\"},\"customer\":{\"name\":\"Comprador de Exemplo\",\"email\":\"comprador@exemplo.test\"},\"created_at\":\"2026-06-15T12:00:00.000Z\",\"paid_at\":\"2026-06-15T12:00:00.000Z\"}}}",
"signature_header": "t=1781524800,v1=18e77f9767e224aec6941555b5ce2ee0ab482b88e63b0e9b85375077f6977aea",
"signature_header_during_rotation": "t=1781524800,v1=18e77f9767e224aec6941555b5ce2ee0ab482b88e63b0e9b85375077f6977aea,v1=71b3f6f314abc7cbc9d041d8903492f2ab6b0f62cbbb88faedefc460d816a1df"
}

Os três exemplos desta página são testados contra este vetor a cada geração da documentação.