Open in
Patikrinkite žiniatinklio kabliukų parašus
Kiekviena ThunderPhone siunčiama žiniatinklio kabliuko ir įrankio užklausa yra pasirašyta. Vieną kartą patikrinkite parašą pagal čia pateiktą receptą, tada tą patį tikrinimą naudokite kiekviename vykdomame galiniame taške.
Kiekvienoje užklausoje, kurią siunčiame į jūsų serverį — webhook pristatymuose ir
įrankių galinių taškų iškvietimuose — antraštėje
X-ThunderPhone-Signature pateikiamas HMAC-SHA256 parašas. Vieną kartą
teisingai įgyvendinkite tikrinimą ir įtraukite tą patį pagalbinį metodą į kiekvieną apdorojimo funkciją.
Algoritmas
- Nuskaitykite neapdorotą užklausos turinį — tikslius baitus, kuriuos jums išsiuntėme POST užklausa.
- Apskaičiuokite
hmac_sha256(secret, body).hexdigest(). - Palyginkite pastoviu laiku su
X-ThunderPhone-Signature. (Paprastas eilučių palyginimas atskleidžia laiko informaciją.)
Pasirašome būtent tuos baitus, kuriuos perduodame, todėl neapdoroto turinio
tikrinimas visada veikia. Šie baitai taip pat yra kanoninis JSON serializavimas
naudingojo krūvio — raktai surūšiuoti abėcėlės tvarka, glausti skirtukai
(, ir : be tarpų), UTF-8. Tai suteikia jums antrą, visiškai
lygiavertį būdą, kai jūsų sistema pateikia tik išanalizuotą JSON:
iš naujo kanoniškai serializuokite ir apskaičiuokite jo HMAC.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")Pirmenybę teikite neapdorotam turiniui — tai vienu žingsniu mažiau ir jis atsparus JSON skaičių pakartotinio konvertavimo ypatumams kai kuriose kalbose.
Kurį slaptąjį raktą naudoti?
| Šaltinis | Slaptasis raktas |
|---|---|
Webhook galinis taškas (/v1/developer/webhook-endpoints) | Kiekvieno galinio taško secret (48 šešioliktainiai simboliai), grąžinamas tik vieną kartą sukuriant |
| Senas vieno URL webhook | Organizacijos secret, grąžinamas naudojant GET /v1/webhook |
Įrankio galinio taško iškvietimas (tiesioginis iškvietimas į jūsų endpoint.url) | Organizacijos lygmens webhook slaptasis raktas (tas pats, kaip seno vieno URL webhook) — ne atskiro galinio taško slaptasis raktas |
Saugokite slaptąjį raktą slaptųjų raktų tvarkytuvėje arba aplinkos kintamajame — niekada jo neįtraukite į versijų valdymą.
Pavyzdinės įgyvendinimo versijos
Visos keturios tikrina neapdorotą užklausos turinį:
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)
endKonkrečioms sistemoms skirtas prijungimas
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})Įrankių iškvietimų tikrinimas
Kai agentas tiesiogiai iškviečia vieną iš jūsų
funkcijų įrankių (įrankis turi
endpoint), užklausoje kartu su jūsų sukonfigūruotais endpoint.headers
pateikiamos dvi ThunderPhone antraštės:
X-ThunderPhone-Call-ID— vykstančio skambučio skaitinis identifikatorius.X-ThunderPhone-Signature— HMAC-SHA256, kurio raktas yra jūsų organizacijos lygio žiniatinklio kabliuko paslaptis, apskaičiuotas pagal tikslius užklausos turinio baitus.
Tas pats verify() pagalbinis metodas veikia be pakeitimų, tačiau yra du niuansai:
GET/DELETEįrankiai neturi turinio. Argumentai perduodami kaip užklausos parametrai, o parašas apskaičiuojamas pagal tuščią baitų eilutę — todėlverify(b"", sig, secret)(Python) arbaverify(Buffer.alloc(0), sig, secret)(Node). Neskaičiuokite užklausos eilutės maišos.- Organizacijos, kuriose nesukonfigūruotas ankstesnysis žiniatinklio kabliukas, neturi organizacijos paslapties. Tokiu
atveju įrankių iškvietimuose pateikiama tik
X-ThunderPhone-Call-ID, bet nėra parašo antraštės. Sukonfigūruokite ankstesnįjį žiniatinklio kabliuką (PUT /v1/webhook), kad gautumėte pasirašymo paslaptį, arba autentifikuokite įrankių iškvietimus naudodami savo antraštę perendpoint.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)
...Įrankių iškvietimų nukreipimas žiniatinklio kabliuko režimu (įrankiai be endpoint, pateikiami
į jūsų organizacijos žiniatinklio kabliuką kaip telephony.tool / web.tool) yra įprastas
pasirašytas žiniatinklio kabliukas — taikomas pirmiau pateiktas standartinis būdas. Abiejų
užklausų formų aprašą rasite Funkcijų įrankiuose.
Dažnos klaidos
Pakartotinis serializavimas naudojant numatytąjį formatavimą
Išanalizavus turinį ir vėl jį išvedus naudojant numatytuosius JSON bibliotekos
nustatymus (tarpai po , / :, raktai įterpimo tvarka), gaunami
kiti baitai ir HMAC nebeveikia. Tikrinkite neapdorotą turinį — arba, jei
turite jį pakartotinai serializuoti, tiksliai atkartokite mūsų kanoninę formą:
surikiuoti raktai, kompaktiški skirtukai, UTF-8.
Karkasas automatiškai analizuoja JSON
Express express.json() tarpinė programinė įranga sunaudoja turinio srautą,
todėl prarandate neapdorotus baitus. Konkrečiai webhook maršrutui naudokite
express.raw() arba išsaugokite neapdorotą turinį prieš tarpinę programinę įrangą.
Tas pats taikoma NestJS / Koa — peržiūrėkite jų „neapdoroto turinio“ dokumentaciją.
Nesaugus palyginimas pagal vykdymo laiką
expected === signature JS kalboje arba expected == signature
Python kalboje yra nuo vykdymo laiko priklausantys palyginimai. Atitinkamai naudokite crypto.timingSafeEqual
arba hmac.compare_digest. Našumo skirtumo nėra.
Neteisinga paslaptis įrankių galiniams taškams
Tiesioginiai įrankių galinių taškų iškvietimai pasirašomi naudojant organizacijos lygio webhook
paslaptį (GET /v1/webhook) — ne jokia konkretaus galinio taško paslaptimi
iš /v1/developer/webhook-endpoints. Pakartotinai naudokite tą pačią verify()
funkciją, tačiau įsitikinkite, kad įrankių maršrutuose jai perduodate organizacijos paslaptį.
Užklausos eilutės maišos skaičiavimas GET/DELETE įrankiams
Įrankių metodams be turinio parašas apima tuščią baitų eilutę, todėl išlieka vienas universalus būdas: apskaičiuokite HMAC pagal neapdorotą užklausos turinį, kad ir koks jis būtų. URL arba užklausos eilutės maiša niekada nesutaps.
Negrąžinamas 401 neatitikimo atveju
Grąžinus 200, kai patikra nepavyksta, apdorojimo funkcija tampa pakartotinio siuntimo atakų taikiniu. Jei patikra nepavyksta, visada atsakykite ne 2xx kodu.