API FlavosoWebhooks et événementsVérifier les signatures

Vérifier les signatures

Chaque livraison porte Flavoso-Signature, un HMAC-SHA256 calculé sur l'horodatage et le corps brut. Code de vérification pour Node.js, Python, PHP, Go et C#.

Le principe

Chaque livraison porte l'en-tête Flavoso-Signature: t=1759579200,v1=5257a8…. t est l'heure en secondes depuis 1970 et v1 un HMAC-SHA256, en hexadécimal minuscule :

  • Clé : votre secret de signature, la chaîne entière y compris whsec_, en UTF-8.
  • Message : t, un point, le corps brut. Soit 1759579200.{"id":"evt_…",…}.

Vérifiez la signature avant de vous fier au contenu. Vous savez alors que la livraison vient de Flavoso et n'a pas été modifiée en route. C'est le même principe que chez Stripe : si vous l'avez déjà implémenté pour Stripe, vous le connaissez déjà.

Étape par étape

  1. 1

    Prendre le corps brut

    Exactement les octets reçus, avant toute analyse JSON.

  2. 2

    Découper l'en-tête

    Séparez les parties aux virgules, puis chaque partie au premier =. t est présent une fois, v1 une ou deux fois.

  3. 3

    Vérifier l'âge

    Si t s'écarte de votre horloge de plus de 300 secondes, refusez la livraison. Sinon, une livraison interceptée pourrait être rejouée plus tard.

  4. 4

    Calculer la valeur attendue

    HMAC-SHA256 avec le secret sur t + . + corps, en hexadécimal.

  5. 5

    Comparer

    Comparez avec chaque v1 en temps constant. Si l'un correspond, la livraison est authentique.

Code de vérification

// flavoso-signature.js
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the body exactly as received. Never JSON.stringify(req.body) again.
export function verifyFlavosoSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = header.split(",").map((part) => part.trim().split("="));
  const t = Number(parts.find(([key]) => key === "t")?.[1]);
  const signatures = parts.filter(([key]) => key === "v1").map(([, value]) => value);
  if (!Number.isInteger(t) || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`, "utf8").digest("hex");
  return signatures.some(
    (s) => s.length === expected.length && timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
  );
}

Obtenir le corps brut

La plupart des frameworks analysent le JSON d'eux-mêmes. Si vous retransformez ensuite le corps en texte, les caractères changent (espaces, ordre, caractères accentués) et la signature ne correspond plus. Voici comment obtenir les octets :

EnvironnementCorps brut
Expressexpress.raw({ type: "application/json" }) sur cette route
Next.js (App Router)await request.text()
Flaskrequest.get_data()
FastAPIawait request.body()
Djangorequest.body
PHPfile_get_contents('php://input')
Laravel$request->getContent()
Goio.ReadAll(r.Body)
ASP.NET CoreUn StreamReader sur Request.Body, avant tout model binding
// app/api/flavoso/webhook/route.js (App Router)
import { verifyFlavosoSignature } from "@/lib/flavoso-signature";

export async function POST(request) {
  const raw = await request.text(); // exactly as received, before any JSON parsing
  const header = request.headers.get("flavoso-signature") ?? "";
  if (!verifyFlavosoSignature(raw, header, process.env.FLAVOSO_WEBHOOK_SECRET)) {
    return new Response("invalid signature", { status: 400 });
  }
  await enqueue(JSON.parse(raw)); // answer fast, work in the background
  return new Response(null, { status: 200 });
}

Vecteur de test

Ce vecteur vous permet de tester votre vérification sans Flavoso : avec ce secret, ce corps et t comme « maintenant », elle doit accepter la signature, et la refuser dès qu'un caractère du corps est modifié.

# Your verification must accept this example with t = 1759579200 as "now".
# openssl prints the same v1 (after "SHA2-256(stdin)="):
printf '%s' '1759579200.{"id":"evt_test","type":"ping"}' \
  | openssl dgst -sha256 -hmac 'whsec_test_secret'

# secret:  whsec_test_secret
# body:    {"id":"evt_test","type":"ping"}
# header:  Flavoso-Signature: t=1759579200,v1=d5a5a8b08d9a25070a9e22c9fa06ad59b4082eb039b25cee3d83319e00d8201a

Changer de secret

Après « Créer un nouveau secret de signature », Flavoso signe pendant 24 heures à la fois avec le nouveau et avec l'ancien secret, le nouveau en premier : l'en-tête porte alors deux v1. Il suffit que l'un corresponde. Déployez le nouveau secret pendant ce délai ; ensuite, seul le nouveau est valable.

Si un secret a fuité, renouvelez-le immédiatement. Une livraison signée avec l'ancien reste alors possible pendant 24 heures, pas plus.

Erreurs fréquentes

  • Le corps a été analysé comme du JSON puis retransformé en texte.
  • Le mauvais secret : le webhook de votre application et les webhooks propres à un établissement ont des secrets différents.
  • L'horloge du serveur est déréglée. Synchronisez-la avec NTP.
  • Comparaison avec un hexadécimal en majuscules, ou espaces non supprimés autour des parties de l'en-tête.
  • Flavoso-Event-Id considéré comme une preuve. L'identifiant n'a rien de secret ; seule la signature fait foi.

Une question reste ouverte ? Écrivez à support@flavoso.com en indiquant l'identifiant de X-Request-Id. Il nous permet de retrouver votre requête dans le journal.