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| Componente | Descrizione |
|---|---|
t | Timestamp Unix in millisecondi al momento della generazione della firma |
v1 | Firma 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:
| Approccio | Affidabilità | Complessità | Quando usarlo |
|---|---|---|---|
| Corpo grezzo | Garantita | Maggiore | Sistemi in produzione, requisiti di alta affidabilità |
| JSON.stringify | Molto alta | Minore | Integrazioni 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.0vs1) 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.
Next.js API Routes
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.
Next.js API Routes
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:
- Segreto corretto — Assicurati di usare il signing secret della sottoscrizione webhook, non un token API
- Intestazione completa — Verifica di leggere l’intera intestazione
X-Webhook-Signature, incluse le partit=ev1= - 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;
}