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| Composant | Description |
|---|---|
t | Horodatage Unix en millisecondes au moment où la signature a été générée |
v1 | Signature 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 :
| Approche | Fiabilité | Complexité | À utiliser quand |
|---|---|---|---|
| Corps brut | Garantie | Plus élevée | Systèmes de production, exigences de forte fiabilité |
| JSON.stringify | Très élevée | Plus faible | Inté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.0vs1) 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.
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 });
}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.
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 });
}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 :
- Bon secret — Utilise le secret de signature de l’abonnement webhook, pas un jeton d’API
- En-tête complet — Lis l’en-tête
X-Webhook-Signatureen entier, y compris les partiest=etv1= - 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;
}