~/keydrop/tools/webhooks
Genera un kit completo de variables y snippets para firmar y verificar webhooks.
Webhook security kit
Variables sugeridas
Los valores se generan en tu navegador con Web Crypto.
- sec
WEBHOOK_SECRETSecreto principal para firmar/verificar peticiones.
whsec_YhkqbU1ls0Ixy1wsGQ7eqXU7zh5W4XnrBVtmSiPKMNfey6bO
- sec
WEBHOOK_SIGNING_SECRETVariante explícita para integraciones tipo Stripe (whsec_…).
whsec_FxMuBLCdw0R64SdFc6BfhxHSPUAJL8550gzJbprw7LF7NfNY
- sec
HMAC_SECRETSecreto HMAC genérico cuando hay varios consumidores.
whsec_HmBQxertEbnjBgw4DDqJcMiA17PMx-3e8a6a-sTiO-DLhgeJ
- sec
INTERNAL_WEBHOOK_TOKENToken interno para webhooks entre servicios privados.
whsec_rqqP3rOBRLjIlF_IO4t-R7lSD7L60AMRCx6IbffVvdFkZVEb
- num
WEBHOOK_TIMESTAMP_TOLERANCE_SECONDSVentana en segundos para aceptar firmas con timestamp. Recomendado 300.
300
- num
WEBHOOK_REPLAY_WINDOW_SECONDSTiempo máximo de cache de IDs de webhooks recibidos para detectar replays.
600
Snippets
// app/api/webhook/route.ts
import { NextResponse } from 'next/server';
import crypto from 'node:crypto';
const SECRET = process.env.WEBHOOK_SECRET ?? '';
const TOLERANCE = Number(process.env.WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS ?? 300);
export async function POST(request: Request) {
const ts = request.headers.get('x-webhook-timestamp');
const sig = request.headers.get('x-webhook-signature');
if (!ts || !sig) return NextResponse.json({ error: 'missing_headers' }, { status: 400 });
const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(ts));
if (!Number.isFinite(skew) || skew > TOLERANCE) {
return NextResponse.json({ error: 'expired' }, { status: 400 });
}
const raw = await request.text();
const expected = crypto.createHmac('sha256', SECRET).update(`${ts}.${raw}`).digest('hex');
const sigBuf = Buffer.from(sig, 'hex');
const expBuf = Buffer.from(expected, 'hex');
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
return NextResponse.json({ error: 'invalid_signature' }, { status: 401 });
}
// payload válido: procesa el evento aquí
return NextResponse.json({ ok: true });
}Recomendaciones
- !No pases secretos por query params, usa cabeceras.
- !Valida siempre el timestamp y rechaza eventos antiguos.
- !Usa comparación timing-safe (`crypto.timingSafeEqual` en Node).
- !No loguees el cuerpo del webhook ni la firma cruda.
- !Rota el secreto periódicamente y permite ventana doble durante la rotación.
- !Restringe los webhooks a IPs del proveedor cuando sea posible.
Longitud del secreto
48