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. Also1759579200.{"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
Rohen Rumpf nehmen
Genau die Bytes, die angekommen sind, bevor irgendetwas sie als JSON liest.
- 2
Kopf zerlegen
An Kommas trennen, dann am ersten
=.tist einmal da,v1ein- oder zweimal. - 3
Alter prüfen
Weicht
tmehr als 300 Sekunden von deiner Uhr ab, ablehnen. Sonst ließe sich eine mitgeschnittene Zustellung später noch einmal abspielen. - 4
Erwarteten Wert rechnen
HMAC-SHA256 mit dem Geheimnis über
t+.+ Rumpf, als Hex. - 5
Vergleichen
Mit jedem
v1in 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:
| Umgebung | Roher Rumpf |
|---|---|
| Express | express.raw({ type: "application/json" }) an dieser Route |
| Next.js (App Router) | await request.text() |
| Flask | request.get_data() |
| FastAPI | await request.body() |
| Django | request.body |
| PHP | file_get_contents('php://input') |
| Laravel | $request->getContent() |
| Go | io.ReadAll(r.Body) |
| ASP.NET Core | StreamReader ü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=d5a5a8b08d9a25070a9e22c9fa06ad59b4082eb039b25cee3d83319e00d8201aGeheimnis 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.
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-Idals 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.