BuildNexion

Verificar la firma

Cada aviso va firmado con el secreto del webhook. Compruébalo siempre antes de hacer nada con él: si no, cualquiera que conozca tu URL podría mandarte avisos falsos.

La cabecera

http
X-BuildNexion-Signature: sha256=5d1f0c9e8b7a6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c

Es sha256= seguido del HMAC-SHA256, en hexadecimal, del cuerpo de la petición tal cual llega, calculado con el secreto del webhook (el whsec_… que se muestra al crearlo).

Cómo comprobarla

  1. Lee el cuerpo en crudo, sin convertirlo todavía a JSON.
  2. Calcula el HMAC-SHA256 de ese cuerpo con tu secreto.
  3. Compáralo con lo que va después de sha256=, con una comparación en tiempo constante.
  4. Si no coincide, responde 401 y no lo proceses.
No vuelvas a convertir el JSON en texto para comprobar la firma: JSON.stringify(JSON.parse(cuerpo)) no da los mismos bytes (espacios, orden de las claves…) y la firma no cuadraría.
import crypto from 'node:crypto'

// cuerpo: el cuerpo en crudo (Buffer o string), ANTES de hacer JSON.parse
export function firmaValida(cuerpo, cabecera, secreto) {
  if (!cabecera || !cabecera.startsWith('sha256=')) return false
  const esperada = crypto.createHmac('sha256', secreto).update(cuerpo).digest()
  const recibida = Buffer.from(cabecera.slice('sha256='.length), 'hex')
  // Comparación en tiempo constante
  return recibida.length === esperada.length && crypto.timingSafeEqual(recibida, esperada)
}

Un endpoint completo

import express from 'express'
import { firmaValida } from './firma.js'

const app = express()

// express.raw, NO express.json: la firma se comprueba sobre el cuerpo en crudo
app.post('/buildnexion/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!firmaValida(req.body, req.get('X-BuildNexion-Signature'), process.env.BUILDNEXION_WEBHOOK_SECRET)) {
    return res.status(401).send('firma no válida')
  }
  const evento = JSON.parse(req.body.toString('utf8'))
  res.sendStatus(200)                                        // responde enseguida…
  encolar(evento, req.get('X-BuildNexion-Delivery'))         // …y procesa después
})

Repeticiones

  • No proceses dos veces lo mismo: guarda el X-BuildNexion-Delivery de lo que ya has procesado. Los reintentos llevan el mismo.
  • Avisos antiguos: la firma no lleva hora, pero created_at va dentro del cuerpo firmado y no se puede cambiar. Si quieres descartar avisos viejos, rechaza los de más de una hora (los reintentos llegan como mucho unos 35 minutos después).

Cambiar el secreto

El secreto no se puede cambiar en un webhook ya creado. Si se ha filtrado, crea otro webhook con la misma URL (tendrá un secreto nuevo), actualiza tu servidor y borra el antiguo. Mientras convivan, cada aviso llegará dos veces, una firmada con cada secreto: acepta los dos durante el cambio y descarta el repetido.