# Autenticación

> Cómo validar la firma HMAC-SHA256 de los webhooks de Rapid, con protección contra replay y ejemplos listos en Node.js, Python y PHP.

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

En el formato **`v2`**, Rapid firma cada webhook con **HMAC-SHA256** usando tu clave de firma (el `secret`). Rapid **genera** esa clave cuando registras el webhook y la muestra **una sola vez** en el panel, en **Configuración › Webhook**; guárdala en el momento. No hay forma de recuperarla después: si la pierdes, genera otra en **Configuración › API** (la anterior deja de valer en ese mismo instante). Tu aplicación debe validar esta firma antes de procesar el payload; así confirmas que el POST vino de Rapid.

> Las cuentas en formato **`legacy`** (migradas del sistema anterior) **no reciben firma**: la autenticación se hace por los headers `clientid`/`clientkey`, como antes. Mira cuál es tu formato en [Payload](https://doc.rapidchargeback.com/es/canais/webhook/payload/).

## Header de firma

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

Donde `<hmac_hex>` es el HMAC-SHA256, en hexadecimal, de la cadena `${timestamp}.${cuerpo_crudo}`, es decir, el valor del header `X-Webhook-Timestamp` (segundos desde la epoch), un punto y el **cuerpo crudo de la solicitud** (el JSON recibido, byte a byte), usando tu clave de firma.

El timestamp forma parte de la firma para dar **protección contra replay**: sin él, un POST válido capturado podría reenviarse indefinidamente con una firma correcta. Rechaza las solicitudes cuyo `X-Webhook-Timestamp` esté fuera de una ventana de tolerancia (recomendamos **5 minutos**).

---

## Cómo validar

1. Lee el cuerpo crudo de la solicitud. **No lo parsees como JSON antes de validar**: la firma se calcula sobre los bytes exactos recibidos.
2. Arma `signed = timestamp + "." + raw_body`, calcula `HMAC-SHA256(secret, signed)` y codifícalo en hexadecimal
3. Compáralo con el valor de `X-Webhook-Signature` (después del prefijo `sha256=`)
4. Usa una comparación timing-safe para evitar timing attacks

### Ejemplos

Un endpoint completo en cada lenguaje: lee el cuerpo crudo, valida la firma y la ventana de tiempo, y solo entonces lee el JSON. Responde `401` a cualquier entrega que no pase, incluso sin los headers. La clave viene de la variable de entorno `RAPID_WEBHOOK_SECRET`.

**Node.js**

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

const TOLERANCIA_SEGUNDOS = 5 * 60

function validarFirma(cuerpoCrudo, firma, timestamp, secret) {
  // 1. Ventana de tolerancia (anti-replay). Sin esto, un POST antiguo capturado
  //    sigue pasando para siempre.
  const antiguedad = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp))
  if (!Number.isFinite(antiguedad) || antiguedad > TOLERANCIA_SEGUNDOS) return false

  // 2. Firma `timestamp.cuerpo_crudo` (el cuerpo recibido, sin reserializar).
  const esperada = 'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${cuerpoCrudo}`).digest('hex')

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

const app = express()

// express.raw, y no express.json: la firma se calcula sobre el cuerpo crudo.
app.post('/webhooks/rapid', express.raw({ type: 'application/json' }), (req, res) => {
  const cuerpoCrudo = req.body.toString('utf8')
  const ok = validarFirma(
    cuerpoCrudo,
    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(cuerpoCrudo)
  // Procesa el `evento` (preferentemente en una cola) y responde 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 validar_firma(cuerpo_crudo: bytes, firma: str, timestamp: str, secret: str) -> bool:
    # 1. Ventana de tolerancia (anti-replay).
    try:
        antiguedad = abs(int(time.time()) - int(timestamp))
    except (TypeError, ValueError):
        return False
    if antiguedad > TOLERANCIA_SEGUNDOS or not firma:
        return False

    # 2. Firma `timestamp.cuerpo_crudo` (los bytes recibidos, sin reserializar).
    firmado = timestamp.encode() + b"." + cuerpo_crudo
    esperada = "sha256=" + hmac.new(secret.encode(), firmado, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperada.encode(), firma.encode())


app = Flask(__name__)


@app.post("/webhooks/rapid")
def webhook_rapid():
    # get_data(), y no get_json(): la firma se calcula sobre el cuerpo crudo.
    cuerpo_crudo = request.get_data()
    ok = validar_firma(
        cuerpo_crudo,
        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()
    # Procesa el `evento` (preferentemente en una cola) y responde rápido.
    return "", 200
```

**PHP**

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

function validarFirma(string $cuerpoCrudo, string $firma, string $timestamp, string $secret): bool
{
    // 1. Ventana de tolerancia (anti-replay).
    if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > TOLERANCIA_SEGUNDOS) {
        return false;
    }

    // 2. Firma `timestamp.cuerpo_crudo` (los bytes recibidos, sin reserializar).
    $esperada = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $cuerpoCrudo, $secret);
    return hash_equals($esperada, $firma);
}

// php://input, y no json_decode antes: la firma se calcula sobre el cuerpo crudo.
$cuerpoCrudo = file_get_contents('php://input');
$ok = validarFirma(
    $cuerpoCrudo,
    $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '',
    $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '',
    getenv('RAPID_WEBHOOK_SECRET')
);
if (!$ok) {
    http_response_code(401);
    exit;
}

$evento = json_decode($cuerpoCrudo, true);
// Procesa el $evento (preferentemente en una cola) y responde rápido.
http_response_code(200);
```

## Verificador de firma

¿Tu validación no coincide? Pega aquí lo que recibiste y compáralo con la firma esperada.

---

## Rotación del secret

Tú mismo generas una clave nueva en el panel, en **Configuración › API**. Aparece una sola vez, y a partir de ahí los webhooks ya salen firmados con ella, **sin período de superposición**: quien valide con la anterior empieza a rechazar. Cambia la clave en tus servidores enseguida, de preferencia en una ventana de poco tráfico.

Guardar una **URL** nueva no cambia la clave.

---

## Headers heredados

En el formato `legacy`, Rapid envía los headers `clientid` y `clientkey` y **no** envía firma, exactamente como el sistema anterior.

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

En el formato `v2` estos headers **no existen**: validar por `clientid`/`clientkey` significaría recibir la credencial de acceso a la API en cada solicitud (menos seguro que firmar el payload), así que la autenticación de la entrega es solo `X-Webhook-Signature`. Quien está en `legacy` sigue recibiendo los headers por tiempo indefinido, para no romper integraciones existentes.
