Flavoso APIWebhooks und EreignisseSignaturen prüfen

Signaturen prüfen

Jede Zustellung trägt Flavoso-Signature, ein HMAC-SHA256 über Zeitstempel und rohen Rumpf. Prüfcode für Node.js, Python, PHP, Go und C#.

Das Verfahren

Jede Zustellung trägt den Kopf Flavoso-Signature: t=1759579200,v1=5257a8…. Dabei ist t die Zeit in Sekunden seit 1970 und v1 ein HMAC-SHA256, hexadezimal in Kleinbuchstaben:

  • Schlüssel: dein Signaturgeheimnis, die ganze Zeichenkette mit whsec_, als UTF-8.
  • Nachricht: t, ein Punkt, der rohe Rumpf. Also 1759579200.{"id":"evt_…",…}.

Prüf die Signatur, bevor du dem Inhalt glaubst. Dann weißt du, dass die Zustellung von Flavoso kommt und unterwegs nicht verändert wurde. Dasselbe Verfahren wie bei Stripe: wer es dort gebaut hat, kennt es.

Schritt für Schritt

  1. 1

    Rohen Rumpf nehmen

    Genau die Bytes, die angekommen sind, bevor irgendetwas sie als JSON liest.

  2. 2

    Kopf zerlegen

    An Kommas trennen, dann am ersten =. t ist einmal da, v1 ein- oder zweimal.

  3. 3

    Alter prüfen

    Weicht t mehr als 300 Sekunden von deiner Uhr ab, ablehnen. Sonst ließe sich eine mitgeschnittene Zustellung später noch einmal abspielen.

  4. 4

    Erwarteten Wert rechnen

    HMAC-SHA256 mit dem Geheimnis über t + . + Rumpf, als Hex.

  5. 5

    Vergleichen

    Mit jedem v1 in konstanter Zeit vergleichen. Passt eins, ist die Zustellung echt.

Prüfcode

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

Den rohen Rumpf bekommen

Die meisten Frameworks lesen JSON von selbst ein. Wer den Rumpf danach wieder zu Text macht, bekommt andere Zeichen (Leerzeichen, Reihenfolge, Umlaute) und eine falsche Signatur. So kommst du an die Bytes:

UmgebungRoher Rumpf
Expressexpress.raw({ type: "application/json" }) an dieser 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 CoreStreamReader über Request.Body, vor jedem 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 });
}

Prüfwert

Damit testest du deine Prüfung ohne Flavoso: mit diesem Geheimnis, diesem Rumpf und t als „jetzt“ muss sie die Signatur annehmen, mit einem veränderten Zeichen im Rumpf ablehnen.

# 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

Geheimnis wechseln

Nach „Neues Signaturgeheimnis erzeugen“ signiert Flavoso 24 Stunden lang mit dem neuen und dem alten Geheimnis zugleich, das neue zuerst: der Kopf trägt dann zwei v1. Es genügt, wenn eines passt. Trag in dieser Zeit das neue Geheimnis ein; danach gilt nur noch das neue.

Ist ein Geheimnis an die Öffentlichkeit geraten, erneuere es sofort. Eine Zustellung mit dem alten ist dann noch 24 Stunden möglich, nicht länger.

Häufige Fehler

  • Der Rumpf wurde als JSON gelesen und wieder zu Text gemacht.
  • Das falsche Geheimnis: der Webhook deiner App und die eigenen Webhooks eines Betriebs haben verschiedene.
  • Die Uhr des Servers geht falsch. Mit NTP stellen.
  • Großbuchstaben im Hex verglichen, oder Leerzeichen um die Teile des Kopfes nicht entfernt.
  • Flavoso-Event-Id als Beweis genommen. Die Kennung ist kein Geheimnis; nur die Signatur beweist etwas.

Etwas unklar geblieben? Schreib an support@flavoso.com und nenne die Kennung aus X-Request-Id. Damit finden wir deine Anfrage im Protokoll.