Skip to Content
WebhooksVérification de la signature

Vérification de signature

Vérifie toujours les signatures des webhooks avant de traiter la charge utile. Cela garantit que la requête provient bien d’Ignite et n’a pas été altérée.

Pourquoi vérifier les signatures ?

Les endpoints webhook sont des URL accessibles publiquement. Sans vérification de signature, n’importe qui pourrait envoyer de faux événements vers ton endpoint. La vérification de signature garantit :

  • Authenticité — La requête provient d’Ignite
  • Intégrité — La charge utile n’a pas été modifiée en transit

Format de la signature

Chaque requête webhook inclut un en-tête X-Webhook-Signature au format suivant :

t=1705316400000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
ComposantDescription
tHorodatage Unix en millisecondes au moment où la signature a été générée
v1Signature HMAC-SHA256 (encodée en hexadécimal)

Algorithme de vérification

La signature est calculée à partir d’une combinaison de l’horodatage et du corps de la requête. Il existe deux approches :

Option A : corps brut (recommandé)

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

Option B : JSON.stringify

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

Ici, raw_body correspond aux octets exacts reçus dans la requête, et JSON.stringify(body) resérialise l’objet JSON parsé.

Approches de vérification

Deux approches permettent de vérifier la signature. Choisis celle qui correspond à ton cas :

ApprocheFiabilitéComplexitéÀ utiliser quand
Corps brutGarantiePlus élevéeSystèmes de production, exigences de forte fiabilité
JSON.stringifyTrès élevéePlus faibleIntégrations rapides, accès au corps brut difficile

Pourquoi cette différence ?

La signature porte sur les octets exacts envoyés par Ignite. Avec JSON.stringify() sur le corps parsé :

  • En général ça fonctionne : JSON.stringify() est déterministe pour un même objet JavaScript
  • Risques théoriques : l’ordre des clés dans des objets imbriqués, le format des nombres (1.0 vs 1) ou l’échappement Unicode peuvent différer entre sérialiseurs

En pratique, JSON.stringify() reste fiable car Ignite et ton code utilisent une sérialisation JSON standard. Le corps brut offre toutefois une garantie à 100 %.


Option A : corps brut (recommandé)

Cette approche utilise les octets exacts reçus, ce qui assure que la signature correspond toujours.

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 }); }

Option B : JSON.stringify (plus simple)

Cette approche resérialise le corps parsé. Elle est plus simple mais suppose une sérialisation JSON cohérente entre Ignite et ton code.

En pratique cette approche est fiable car les deux côtés utilisent un JSON.stringify() standard sans options de formatage. Utilise-la quand l’accès au corps brut est contraignant ou pour une intégration rapide.

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 }); }

Validation d’horodatage (facultatif)

Pour limiter les attaques par rejeu, tu peux aussi vérifier que l’horodatage est récent :

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; }

Dépannage

Signature incorrecte

Si la vérification de signature échoue, vérifie les points suivants :

  1. Bon secret — Utilise le secret de signature de l’abonnement webhook, pas un jeton d’API
  2. En-tête complet — Lis l’en-tête X-Webhook-Signature en entier, y compris les parties t= et v1=
  3. Intégrité du corps — Avec l’option B, assure-toi qu’aucun middleware ne modifie le corps avant la vérification

Débogage

Ajoute des journaux pour diagnostiquer les problèmes de signature :

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; }