# Autenticação

> Como validar a assinatura HMAC-SHA256 dos webhooks da Rapid, com proteção contra replay e exemplos prontos em Node.js, Python e PHP.

Página: https://doc.rapidchargeback.com/canais/webhook/autenticacao/

No formato **`v2`** a Rapid assina cada webhook com **HMAC-SHA256** usando a sua chave de assinatura (o `secret`). A Rapid **gera** essa chave quando você cadastra o webhook e a mostra **uma única vez** no painel, em `Configurações > Webhook`; guarde na hora. Não há como recuperar a chave depois: se perder, peça uma nova ao suporte da Rapid (a anterior deixa de valer no mesmo instante). Sua aplicação deve validar essa assinatura antes de processar o payload, é assim que você confirma que o POST veio da Rapid.

> Contas no formato **`legacy`** (migradas do sistema anterior) **não recebem assinatura**: a autenticação é feita pelos headers `clientid`/`clientkey`, como antes. Veja qual é o seu formato em [Payload](https://doc.rapidchargeback.com/canais/webhook/payload/).

## Header de assinatura

```
X-Webhook-Signature: sha256=<hmac_hex>
```

Onde `<hmac_hex>` é o HMAC-SHA256, em hexadecimal, da string `${timestamp}.${corpo_bruto}`, ou seja o valor do header `X-Webhook-Timestamp` (segundos desde a epoch), um ponto, e o **corpo bruto da requisição** (o JSON recebido, byte a byte), usando a sua chave de assinatura.

O timestamp entra na assinatura para dar **proteção contra replay**: sem ele, um POST válido capturado poderia ser reenviado indefinidamente com assinatura correta. Rejeite requisições cujo `X-Webhook-Timestamp` esteja fora de uma janela de tolerância (recomendamos **5 minutos**).

---

## Como validar

1. Leia o corpo bruto da requisição. **Não parse como JSON antes de validar**: a assinatura é calculada sobre os bytes exatos recebidos.
2. Monte `signed = timestamp + "." + raw_body`, calcule `HMAC-SHA256(secret, signed)` e codifique em hexadecimal
3. Compare com o valor em `X-Webhook-Signature` (após o prefixo `sha256=`)
4. Use comparação timing-safe para evitar timing attacks

### Exemplos

Um endpoint completo em cada linguagem: lê o corpo bruto, valida a assinatura e a janela de tempo, e só então lê o JSON. Responde `401` para qualquer entrega que não passe, inclusive sem os headers. A chave vem da variável de ambiente `RAPID_WEBHOOK_SECRET`.

**Node.js**

```javascript
import crypto from 'node:crypto'
import express from 'express'

const TOLERANCIA_SEGUNDOS = 5 * 60

function validaAssinatura(corpoBruto, assinatura, timestamp, secret) {
  // 1. Janela de tolerância (anti-replay). Sem isto, um POST antigo capturado
  //    continua passando pra sempre.
  const idade = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp))
  if (!Number.isFinite(idade) || idade > TOLERANCIA_SEGUNDOS) return false

  // 2. Assina `timestamp.corpo_bruto` (o corpo recebido, sem reserializar).
  const esperada = 'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${corpoBruto}`).digest('hex')

  const a = Buffer.from(esperada)
  const b = Buffer.from(assinatura ?? '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

const app = express()

// express.raw, e não express.json: a assinatura é calculada sobre o corpo bruto.
app.post('/webhooks/rapid', express.raw({ type: 'application/json' }), (req, res) => {
  const corpoBruto = req.body.toString('utf8')
  const ok = validaAssinatura(
    corpoBruto,
    req.get('X-Webhook-Signature'),
    req.get('X-Webhook-Timestamp'),
    process.env.RAPID_WEBHOOK_SECRET,
  )
  if (!ok) return res.status(401).end()

  const evento = JSON.parse(corpoBruto)
  // Processe o `evento` (de preferência numa fila) e responda rápido.
  res.status(200).end()
})

app.listen(process.env.PORT ?? 3000)
```

**Python**

```python
import hashlib
import hmac
import os
import time

from flask import Flask, request

TOLERANCIA_SEGUNDOS = 5 * 60


def valida_assinatura(corpo_bruto: bytes, assinatura: str, timestamp: str, secret: str) -> bool:
    # 1. Janela de tolerância (anti-replay).
    try:
        idade = abs(int(time.time()) - int(timestamp))
    except (TypeError, ValueError):
        return False
    if idade > TOLERANCIA_SEGUNDOS or not assinatura:
        return False

    # 2. Assina `timestamp.corpo_bruto` (os bytes recebidos, sem reserializar).
    assinado = timestamp.encode() + b"." + corpo_bruto
    esperada = "sha256=" + hmac.new(secret.encode(), assinado, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperada.encode(), assinatura.encode())


app = Flask(__name__)


@app.post("/webhooks/rapid")
def webhook_rapid():
    # get_data(), e não get_json(): a assinatura é calculada sobre o corpo bruto.
    corpo_bruto = request.get_data()
    ok = valida_assinatura(
        corpo_bruto,
        request.headers.get("X-Webhook-Signature", ""),
        request.headers.get("X-Webhook-Timestamp", ""),
        os.environ["RAPID_WEBHOOK_SECRET"],
    )
    if not ok:
        return "", 401

    evento = request.get_json()
    # Processe o `evento` (de preferência numa fila) e responda rápido.
    return "", 200
```

**PHP**

```php
<?php
const TOLERANCIA_SEGUNDOS = 5 * 60;

function validaAssinatura(string $corpoBruto, string $assinatura, string $timestamp, string $secret): bool
{
    // 1. Janela de tolerância (anti-replay).
    if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > TOLERANCIA_SEGUNDOS) {
        return false;
    }

    // 2. Assina `timestamp.corpo_bruto` (os bytes recebidos, sem reserializar).
    $esperada = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $corpoBruto, $secret);
    return hash_equals($esperada, $assinatura);
}

// php://input, e não json_decode antes: a assinatura é calculada sobre o corpo bruto.
$corpoBruto = file_get_contents('php://input');
$ok = validaAssinatura(
    $corpoBruto,
    $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '',
    $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '',
    getenv('RAPID_WEBHOOK_SECRET')
);
if (!$ok) {
    http_response_code(401);
    exit;
}

$evento = json_decode($corpoBruto, true);
// Processe o $evento (de preferência numa fila) e responda rápido.
http_response_code(200);
```

---

## Rotação de secret

A rotação é feita pelo suporte da Rapid, a pedido. A partir dela os webhooks novos já saem assinados com a chave nova, **sem período de overlap**: quem validar com a anterior passa a recusar. Combine a troca nos seus servidores e a rotação em sequência, de preferência em janela de baixo tráfego.

Salvar uma **URL** nova não mexe na chave.

---

## Headers legados

No formato `legacy`, a Rapid envia os headers `clientid` e `clientkey` e **não** envia assinatura, exatamente como o sistema anterior.

```
clientid: <client_id>
clientkey: <client_secret>
```

No formato `v2` esses headers **não existem**: validar por `clientid`/`clientkey` significaria receber a credencial de acesso à API em cada request (menos seguro que assinar o payload), então a autenticação da entrega é só `X-Webhook-Signature`. Quem está em `legacy` continua recebendo os headers por tempo indeterminado, para não quebrar integrações existentes.
