Skip to Content
WebhooksVerifica della firma

Verifica della firma

Verifica sempre le firme dei webhook prima di elaborare il payload. Così ti assicuri che la richiesta provenga davvero da Ignite e non sia stata manomessa.

Perché verificare le firme?

Gli endpoint webhook sono URL pubblicamente raggiungibili. Senza verifica della firma, chiunque potrebbe inviare eventi falsi al tuo endpoint. La verifica della firma garantisce:

  • Autenticità — La richiesta proviene da Ignite
  • Integrità — Il payload non è stato alterato in transito

Formato della firma

Ogni richiesta webhook include un’intestazione X-Webhook-Signature nel formato seguente:

t=1705316400000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
ComponenteDescrizione
tTimestamp Unix in millisecondi al momento della generazione della firma
v1Firma HMAC-SHA256 (codificata in esadecimale)

Algoritmo di verifica

La firma è calcolata su una combinazione del timestamp e del corpo della richiesta. Ci sono due approcci:

Opzione A: corpo grezzo (consigliato)

signed_payload = timestamp + "." + raw_body signature = HMAC-SHA256(signed_payload, signing_secret)

Opzione B: JSON.stringify

signed_payload = timestamp + "." + JSON.stringify(body) signature = HMAC-SHA256(signed_payload, signing_secret)

Dove raw_body sono i byte esatti ricevuti dalla richiesta e JSON.stringify(body) ri-serializza l’oggetto JSON parsato.

Approcci alla verifica

Ci sono due modi per verificare la firma. Scegli quello adatto al tuo caso:

ApproccioAffidabilitàComplessitàQuando usarlo
Corpo grezzoGarantitaMaggioreSistemi in produzione, requisiti di alta affidabilità
JSON.stringifyMolto altaMinoreIntegrazioni rapide, quando l’accesso al corpo grezzo è difficile

Perché la differenza?

La firma è calcolata sui byte esatti inviati da Ignite. Usando JSON.stringify() sul corpo parsato:

  • Di solito funziona: JSON.stringify() è deterministico per lo stesso oggetto JavaScript
  • Possibili problemi: l’ordine delle chiavi in oggetti annidati, la formattazione dei numeri (1.0 vs 1) o l’escape Unicode potrebbero in teoria differire tra serializzatori

In pratica JSON.stringify() funziona in modo affidabile perché sia Ignite sia il tuo codice usano la serializzazione JSON standard. Il corpo grezzo offre però garanzia al 100%.


Opzione A: usare il corpo grezzo (consigliato)

Questo approccio usa i byte esatti ricevuti, così la firma corrisponde sempre.

import { createHmac, timingSafeEqual } from "crypto"; import { NextApiRequest, NextApiResponse } from "next"; // Disable the default body parser to access raw body export const config = { api: { bodyParser: false, }, }; async function getRawBody(req: NextApiRequest): Promise<string> { const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); } return Buffer.concat(chunks).toString('utf-8'); } function isValidSignature(rawBody: string, signatureHeader: string | string[] | undefined, secret: string): boolean { try { if (typeof signatureHeader !== "string") { return false; } const time = signatureHeader.match(/t=(\d+)/)?.[1]; const token = signatureHeader.match(/v1=(\w+)/)?.[1]; if (!time || !token) { return false; } const signedPayload = `${time}.${rawBody}`; const hmac = createHmac("sha256", secret); const calculatedSignature = hmac.update(signedPayload).digest("hex"); return timingSafeEqual(Buffer.from(token), Buffer.from(calculatedSignature)); } catch { return false; } } export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== "POST") { return res.status(405).end(); } const rawBody = await getRawBody(req); if (!isValidSignature(rawBody, req.headers["x-webhook-signature"], process.env.WEBHOOK_SIGNING_SECRET!)) { return res.status(403).end(); } const body = JSON.parse(rawBody); // Process the webhook... return res.status(200).json({ received: true }); }

Opzione B: usare JSON.stringify (più semplice)

Questo approccio ri-serializza il corpo parsato. È più semplice ma dipende da una serializzazione JSON coerente tra Ignite e il tuo codice.

In pratica questo approccio è affidabile perché entrambe le parti usano JSON.stringify() standard senza opzioni di formattazione. Usalo quando l’accesso al corpo grezzo è scomodo o ti serve un’integrazione veloce.

import { createHmac, timingSafeEqual } from "crypto"; import { NextApiRequest, NextApiResponse } from "next"; // Using the default body parser (no config export needed) function isValidSignature(req: NextApiRequest, secret: string): boolean { try { const signature = req.headers["x-webhook-signature"]; if (typeof signature !== "string") { return false; } const time = signature.match(/t=(\d+)/)?.[1]; const token = signature.match(/v1=(\w+)/)?.[1]; if (!time || !token) { return false; } const signedPayload = `${time}.${JSON.stringify(req.body)}`; const hmac = createHmac("sha256", secret); const calculatedSignature = hmac.update(signedPayload).digest("hex"); return timingSafeEqual(Buffer.from(token), Buffer.from(calculatedSignature)); } catch { return false; } } export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== "POST") { return res.status(405).end(); } if (!isValidSignature(req, process.env.WEBHOOK_SIGNING_SECRET!)) { return res.status(403).end(); } const body = req.body; // Process the webhook... return res.status(200).json({ received: true }); }

Validazione del timestamp (facoltativa)

Per prevenire attacchi di replay puoi anche verificare che il timestamp sia recente:

function isTimestampValid(signatureHeader: string, toleranceMs = 300000): boolean { const time = signatureHeader.match(/t=(\d+)/)?.[1]; if (!time) return false; const timestamp = parseInt(time, 10); const now = Date.now(); // Reject if timestamp is more than 5 minutes old (default) return Math.abs(now - timestamp) <= toleranceMs; }

Risoluzione dei problemi

Firma non corrispondente

Se la verifica della firma fallisce, controlla quanto segue:

  1. Segreto corretto — Assicurati di usare il signing secret della sottoscrizione webhook, non un token API
  2. Intestazione completa — Verifica di leggere l’intera intestazione X-Webhook-Signature, incluse le parti t= e v1=
  3. Integrità del corpo — Se usi l’opzione B, assicurati che nessun middleware modifichi il corpo prima della verifica

Debug

Aggiungi log per diagnosticare i problemi di firma:

function isValidSignature(rawBody, signatureHeader, secret) { const time = signatureHeader.match(/t=(\d+)/)?.[1]; const token = signatureHeader.match(/v1=(\w+)/)?.[1]; const signedPayload = `${time}.${rawBody}`; const calculatedSignature = crypto.createHmac("sha256", secret) .update(signedPayload) .digest("hex"); console.log('Received signature:', token); console.log('Calculated signature:', calculatedSignature); console.log('Payload length:', rawBody.length); console.log('First 100 chars:', rawBody.substring(0, 100)); return token === calculatedSignature; }