Webhook-aláírások ellenőrzése
A ThunderPhone által küldött minden webhook- és eszközkérés aláírt. Ellenőrizze az aláírást egyszer az itt található útmutatóval, majd használja újra ugyanezt az ellenőrzést minden futtatott végponton.
Minden, a szerverére küldött kérésünk — webhook-kézbesítés és
eszközvégpont-meghívás — HMAC-SHA256-aláírást tartalmaz a
X-ThunderPhone-Signature fejlécben. Állítsa be egyszer helyesen az
ellenőrzést, majd használja ugyanazt a segédfüggvényt minden kezelőben.
Az algoritmus
- Olvassa be a kérés nyers törzsét — azokat a pontos bájtokat, amelyeket POST-kéréssel küldtünk Önnek.
- Számítsa ki:
hmac_sha256(secret, body).hexdigest(). - Hasonlítsa össze konstans időben az
X-ThunderPhone-Signatureértékével. (Az egyszerű sztring-összehasonlítás időzítési információkat szivárogtat.)
Pontosan azokat a bájtokat írjuk alá, amelyeket továbbítunk, ezért a nyers törzs
ellenőrzése mindig működik. Ezek a bájtok egyben a hasznos adat kanonikus JSON-szerializációját
is jelentik — a kulcsok ábécérendben vannak, az elválasztók tömörek
(szóköz nélkül: , és :), a kódolás pedig UTF-8. Ez egy második, teljesen
egyenértékű megoldást ad, ha a keretrendszere csak a feldolgozott JSON-t teszi elérhetővé:
szerializálja újra kanonikusan, majd erre számítsa a HMAC-et.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")Részesítse előnyben a nyers törzset — ezzel egy lépéssel kevesebb, és nem érintik az egyes nyelvekben előforduló JSON-számok oda-vissza szerializálásának sajátosságai.
Melyik titkos kulcs?
| Forrás | Titkos kulcs |
|---|---|
Webhook-végpont (/v1/developer/webhook-endpoints) | A végpontonkénti secret (48 hexadecimális karakter), amelyet a rendszer egyszer, a létrehozáskor ad vissza |
| Régi, egy URL-es webhook | A szervezetenkénti secret, amelyet a GET /v1/webhook ad vissza |
Eszközvégpont-meghívás (közvetlen hívás az Ön endpoint.url címére) | A szervezeti szintű webhook-titkos kulcs (ugyanaz, mint a régi, egy URL-es webhook esetén) — nem végpontonkénti titkos kulcs |
Tárolja a titkos kulcsot a titkoskulcs-kezelőjében vagy környezeti változóban — soha ne commitolja.
Referenciamegvalósítások
Mind a négy a nyers kéréstörzset ellenőrzi:
import hashlib
import hmac
def verify(body: bytes, signature: str, secret: str) -> bool:
"""Constant-time HMAC-SHA256 verification."""
expected = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature or "")import crypto from "node:crypto";
export function verify(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
if (!signature || expected.length !== signature.length) return false;
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func Verify(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}require "openssl"
def verify(body, signature, secret)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
Rack::Utils.secure_compare(expected, signature.to_s)
endKeretrendszer-specifikus integráció
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
@app.post("/thunderphone-webhook")
async def hook(request: Request):
body = await request.body() # raw bytes, NOT request.json()
sig = request.headers.get("X-ThunderPhone-Signature", "")
if not verify(body, sig, SECRET):
raise HTTPException(status_code=401)
import json
event = json.loads(body)
# … dispatch on event["type"] …
return {"ok": True}import express from "express";
const app = express();
app.post(
"/thunderphone-webhook",
// IMPORTANT: parse as raw; do NOT use express.json() here.
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.header("X-ThunderPhone-Signature") || "";
if (!verify(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
// … dispatch on event.type …
res.sendStatus(204);
},
);import json
from django.http import JsonResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
@csrf_exempt
@require_POST
def hook(request):
body = request.body # raw bytes
sig = request.headers.get("X-ThunderPhone-Signature", "")
if not verify(body, sig, SECRET):
return HttpResponseForbidden("invalid signature")
event = json.loads(body)
# … dispatch on event["type"] …
return JsonResponse({"ok": True})Eszközhívások ellenőrzése
Amikor az ügynök közvetlenül meghívja valamelyik
függvényeszközét (az eszköz rendelkezik
endpoint értékkel), a kérés az Ön konfigurált endpoint.headers
értékei mellett két ThunderPhone fejlécet is tartalmaz:
X-ThunderPhone-Call-ID— az élő hívás numerikus azonosítója.X-ThunderPhone-Signature— HMAC-SHA256, amelynek kulcsa az Ön szervezeti szintű webhooktitka, és amely a kérés törzsének pontos bájtjaira van számítva.
Ugyanaz a verify() segédfüggvény változatlanul használható, két eltéréssel:
- A
GET/DELETEeszközöknek nincs törzsük. Az argumentumok lekérdezési paraméterekként érkeznek, az aláírás pedig az üres bájtsorozatra számítódik — tehátverify(b"", sig, secret)(Python), illetveverify(Buffer.alloc(0), sig, secret)(Node) használata szükséges. Ne hashelje a lekérdezési karakterláncot. - Az örökölt webhookot nem konfiguráló szervezeteknek nincs szervezeti titkuk. Ebben
az esetben az eszközhívások csak az
X-ThunderPhone-Call-IDfejlécet tartalmazzák, aláírási fejlécet nem. Konfigurálja az örökölt webhookot (PUT /v1/webhook), hogy aláíró titkot kapjon, vagy hitelesítse az eszközhívásokat saját fejléccel azendpoint.headershasználatával.
@app.post("/tools/search-appointments")
async def tool(request: Request):
body = await request.body() # b"" for GET/DELETE tools
sig = request.headers.get("X-ThunderPhone-Signature", "")
call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
if not verify(body, sig, ORG_WEBHOOK_SECRET):
raise HTTPException(status_code=401)
args = json.loads(body)
...A webhook-módú eszközdiszpécselés (az endpoint nélküli eszközök, amelyek
telephony.tool / web.tool formájában érkeznek az Ön szervezeti webhookjára) hagyományos
aláírt webhook — a fenti standard eljárás alkalmazandó. Mindkét kérésformátumról lásd:
Függvényeszközök.
Gyakori buktatók
Újraszerializálás alapértelmezett formázással
A törzs elemzése, majd újra kiírása a JSON-könyvtár
alapértelmezett beállításaival (szóközök a , / : után, beillesztési sorrendű kulcsok)
eltérő bájtokat eredményez, és érvényteleníti a HMAC-et. Ellenőrizze a nyers törzset — vagy ha
újra kell szerializálnia, pontosan egyezzen meg a kanonikus formánkkal: rendezett
kulcsok, tömör elválasztók, UTF-8.
A keretrendszer automatikusan elemzi a JSON-t
Az Express express.json() middleware-e felhasználja a törzsfolyamot,
így elvesznek a nyers bájtok. Kifejezetten a webhook
útvonalon használja az express.raw()-t, vagy pufferelje a nyers törzset egy
előzetes middleware-ben. Ugyanez vonatkozik a NestJS-re / Koára is — tekintse meg a „raw body” dokumentációjukat.
Időzítés szempontjából nem biztonságos összehasonlítás
A JS-ben használt expected === signature, illetve a
Pythonban használt expected == signature időzítésfüggő összehasonlítások. Használja rendre a crypto.timingSafeEqual
vagy a hmac.compare_digest függvényt. A teljesítménykülönbség
elhanyagolható.
Rossz titok a toolvégpontokhoz
A közvetlen toolvégpont-hívásokat a szervezeti szintű webhook-titok
(GET /v1/webhook) írja alá — nem pedig a /v1/developer/webhook-endpoints
végpontspecifikus titkai. Használja újra ugyanazt a verify()
függvényt, de ügyeljen arra, hogy a toolútvonalakon a szervezeti titkot adja át neki.
A lekérdezési karakterlánc hashelése GET/DELETE tooloknál
A törzs nélküli toolmetódusok esetén az aláírás az üres bájtkarakterláncot fedi le, így egyetlen univerzális módszer használható: a nyers kéréstörzs HMAC-je, bármi is legyen az. Az URL vagy a lekérdezési karakterlánc hashelése soha nem fog egyezni.
Nem 401-es válasz eltérés esetén
Ha sikertelen ellenőrzéskor 200-as választ ad vissza, a kezelő ismétlési célponttá válik. Ha az ellenőrzés sikertelen, mindig 2xx-től eltérő választ adjon.