Open in
Provjerite potpise webhookova
Svaki webhook i svaki zahtjev alata koji ThunderPhone šalje potpisan je. Jednom provjerite potpis prema ovdje navedenom postupku, a zatim istu provjeru ponovno upotrijebite na svakom endpointu koji pokrećete.
Svaki zahtjev koji šaljemo vašem poslužitelju — isporuke web-dojavnika i
pozivi krajnjih točaka alata — sadrži HMAC-SHA256 potpis u
zaglavlju X-ThunderPhone-Signature. Jednom ispravno implementirajte provjeru i
uključite isti pomoćnik u svaki rukovatelj.
Algoritam
- Pročitajte neobrađeno tijelo zahtjeva — točne bajtove koje smo vam poslali metodom POST.
- Izračunajte
hmac_sha256(secret, body).hexdigest(). - Usporedite u konstantnom vremenu sa zaglavljem
X-ThunderPhone-Signature. (Naivna usporedba nizova otkriva informacije o vremenu.)
Potpisujemo točno one bajtove koje prenosimo, stoga provjera neobrađenog tijela
uvijek funkcionira. Ti bajtovi ujedno su kanonska JSON serijalizacija
korisnog sadržaja — ključevi poredani abecedno, sažeti razdjelnici
(, i : bez razmaka), UTF-8. To vam pruža drugi, potpuno
ekvivalentan postupak kada vaš okvir izlaže samo parsirani JSON:
ponovno ga serijalizirajte kanonski i nad njim izračunajte HMAC.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")Dajte prednost neobrađenom tijelu — to je jedan korak manje i otporno je na specifičnosti ponovnog pretvaranja JSON brojeva u nekim jezicima.
Koja tajna?
| Izvor | Tajna |
|---|---|
Krajnja točka web-dojavnika (/v1/developer/webhook-endpoints) | secret za pojedinu krajnju točku (48 heksadekadskih znakova) vraćen jednokratno pri stvaranju |
| Naslijeđeni web-dojavnik s jednim URL-om | secret za organizaciju vraćen pri GET /v1/webhook |
Poziv krajnje točke alata (izravni poziv vašeg endpoint.url) | Tajna web-dojavnika na razini organizacije (ista kao za naslijeđeni web-dojavnik s jednim URL-om) — ne tajna pojedine krajnje točke |
Pohranite tajnu u upravitelj tajni ili varijablu okruženja — nikada je nemojte predati u repozitorij.
Referentne implementacije
Sve četiri provjeravaju neobrađeno tijelo zahtjeva:
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)
endPovezivanje specifično za okvir
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})Provjera poziva alata
Kada agent izravno pozove jedan od vaših
funkcijskih alata (alat ima
endpoint), zahtjev uz vaša konfigurirana zaglavlja endpoint.headers
sadrži dva zaglavlja ThunderPhonea:
X-ThunderPhone-Call-ID— numerički ID aktivnog poziva.X-ThunderPhone-Signature— HMAC-SHA256 s ključem tajne webhooka na razini organizacije, izračunat nad točnim bajtovima tijela zahtjeva.
Isti pomoćnik verify() radi bez izmjena, uz dvije pojedinosti:
- Alati
GET/DELETEnemaju tijelo. Argumenti se prenose kao parametri upita, a potpis se izračunava nad praznim nizom bajtova — stogaverify(b"", sig, secret)(Python) iliverify(Buffer.alloc(0), sig, secret)(Node). Nemojte raspršivati niz upita. - Organizacije bez konfiguriranog naslijeđenog webhooka nemaju tajnu organizacije. U
tom slučaju pozivi alata sadrže samo
X-ThunderPhone-Call-ID, bez zaglavlja potpisa. Konfigurirajte naslijeđeni webhook (PUT /v1/webhook) da biste dobili tajnu za potpisivanje ili autentificirajte pozive alata vlastitim zaglavljem putemendpoint.headers.
@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)
...Slanje alata u webhook-načinu rada (alati bez endpoint, isporučeni
na webhook vaše organizacije kao telephony.tool / web.tool) običan je
potpisani webhook — primjenjuje se standardni postupak iznad. Pogledajte
Funkcijski alati za oba oblika zahtjeva.
Uobičajene zamke
Ponovna serijalizacija sa zadanim formatiranjem
Raščlanjivanje tijela i njegovo ponovno zapisivanje sa zadanim
postavkama vaše JSON biblioteke (razmaci nakon , / :, ključevi prema redoslijedu umetanja) stvara
različite bajtove i kvari HMAC. Provjerite neobrađeno tijelo — ili, ako ga
morate ponovno serijalizirati, točno uskladite naš kanonski oblik: sortirani
ključevi, sažeti razdjelnici, UTF-8.
Okvir automatski raščlanjuje JSON
Expressov međuprogram express.json() troši tok tijela
i gubite neobrađene bajtove. Upotrijebite express.raw() posebno na webhook
ruti ili spremite neobrađeno tijelo u međuprogramu koji se izvršava ranije.
Isto vrijedi za NestJS / Koa — provjerite njihovu dokumentaciju za „neobrađeno tijelo”.
Usporedba nesigurna na vremenske napade
expected === signature u JS-u ili expected == signature u
Pythonu usporedbe su s promjenjivim vremenom izvršavanja. Upotrijebite crypto.timingSafeEqual
odnosno hmac.compare_digest. Razlika u performansama
je zanemariva.
Pogrešna tajna za krajnje točke alata
Izravni pozivi krajnjih točaka alata potpisuju se pomoću tajne webhooka
na razini organizacije (GET /v1/webhook) — a ne pomoću tajne za pojedinačnu krajnju točku
iz /v1/developer/webhook-endpoints. Ponovno upotrijebite istu funkciju verify(),
ali provjerite prosljeđujete li joj tajnu organizacije na rutama alata.
Sažimanje niza upita za GET/DELETE alate
Za metode alata bez tijela potpis obuhvaća prazan niz bajtova, čime se zadržava jedan univerzalni postupak: izračunajte HMAC za neobrađeno tijelo zahtjeva, kakvo god ono bilo. Sažimanje URL-a ili niza upita nikada se neće podudarati.
Nevraćanje odgovora 401 pri nepodudaranju
Vraćanje odgovora 200 nakon neuspjele provjere čini rukovatelj metom ponavljanja zahtjeva. Uvijek odgovorite statusom koji nije 2xx ako provjera ne uspije.