Vérifier la signature
Chaque webhook est signé avec votre clé secrète. Vérifiez la signature avant de traiter quoi que ce soit : sans cela, n’importe qui connaissant votre URL pourrait vous faire croire à un paiement.
En-têtes envoyés
Section intitulée « En-têtes envoyés »| En-tête | Contenu |
|---|---|
X-Afriflow-Timestamp |
Horodatage Unix (secondes) de l’envoi |
X-Afriflow-Signature |
t=<horodatage>,v1=<HMAC-SHA256 en hexadécimal> |
Algorithme
Section intitulée « Algorithme »-
Récupérez le corps de la requête brut, exactement tel que reçu (pas le JSON reparsé puis re-sérialisé).
-
Concaténez
<horodatage>.<corps brut>. -
Calculez le HMAC-SHA256 de cette chaîne avec votre clé secrète
af_live_sec_.... -
Comparez en temps constant avec la valeur
v1de l’en-tête. Refusez si l’horodatage a plus de 5 minutes (protection contre le rejeu).
Exemples
Section intitulée « Exemples »import crypto from "node:crypto";import express from "express";
const app = express();
function verifierSignature(corpsBrut, entetes, secret) { const ts = entetes["x-afriflow-timestamp"]; const sig = (entetes["x-afriflow-signature"] || "") .split(",").find((p) => p.startsWith("v1="))?.slice(3); if (!ts || !sig) return false; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 5 minutes const attendu = crypto.createHmac("sha256", secret).update(`${ts}.${corpsBrut}`).digest("hex"); return attendu.length === sig.length && crypto.timingSafeEqual(Buffer.from(attendu), Buffer.from(sig));}
// Le corps doit rester BRUT : express.raw, pas express.jsonapp.post("/webhooks/cartflox", express.raw({ type: "application/json" }), (req, res) => { const brut = req.body.toString("utf8"); if (!verifierSignature(brut, req.headers, process.env.CARTFLOX_SECRET_KEY)) { return res.status(400).send("signature invalide"); } const evt = JSON.parse(brut); if (evt.event === "payment.completed") { // Marquez la commande evt.data.metadata.order_id comme payée. // Rendez ce traitement idempotent : le même événement peut arriver deux fois. } res.sendStatus(200); // répondez vite, traitez le reste en tâche de fond});$brut = file_get_contents("php://input");$ts = $_SERVER["HTTP_X_AFRIFLOW_TIMESTAMP"] ?? "";$sig = "";foreach (explode(",", $_SERVER["HTTP_X_AFRIFLOW_SIGNATURE"] ?? "") as $partie) { if (str_starts_with($partie, "v1=")) $sig = substr($partie, 3);}$attendu = hash_hmac("sha256", $ts . "." . $brut, getenv("CARTFLOX_SECRET_KEY"));if ($ts === "" || $sig === "" || abs(time() - (int) $ts) > 300 || !hash_equals($attendu, $sig)) { http_response_code(400); exit("signature invalide");}$evt = json_decode($brut, true);if ($evt["event"] === "payment.completed") { // Commande payée : $evt["data"]["metadata"]["order_id"]}http_response_code(200);import hmac, hashlib, os, timefrom flask import Flask, request, abort
app = Flask(__name__)SECRET = os.environ["CARTFLOX_SECRET_KEY"].encode()
@app.post("/webhooks/cartflox")def webhook(): brut = request.get_data() # corps brut, surtout pas request.json ts = request.headers.get("X-Afriflow-Timestamp", "") parties = dict(p.split("=", 1) for p in request.headers.get("X-Afriflow-Signature", "").split(",") if "=" in p) attendu = hmac.new(SECRET, f"{ts}.".encode() + brut, hashlib.sha256).hexdigest() if not ts or abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(attendu, parties.get("v1", "")): abort(400) evt = request.get_json(force=True) if evt["event"] == "payment.completed": pass # commande payée : evt["data"]["metadata"]["order_id"] return "", 200