Patikrinkite žiniatinklio kabliukų parašus
Kiekviena užklausa, kurią siunčiame į jūsų serverį – žiniatinklio kabliuko pristatymai ir įrankio galinio taško iškvietimai – turi HMAC-SHA256 parašą X-ThunderPhone-Signature antraštėje. Kartą tinkamai įgyvendinkite tikrinimą ir prijunkite tą pačią pagalbinę funkciją prie kiekvienos tvarkyklės.
Algoritmas
- Nuskaitykite neapdorotą užklausos turinį – tikslius baitus, kuriuos jums išsiuntėme POST metodu.
- Apskaičiuokite
hmac_sha256(secret, body).hexdigest(). - Palyginkite pastoviuoju laiku su
X-ThunderPhone-Signature. (Naivus eilučių palyginimas atskleidžia laiko informaciją.)
Pasirašome būtent baitus, kuriuos perduodame, todėl neapdoroto turinio tikrinimas
visada veikia. Tie baitai taip pat yra kanoninis JSON serializavimas
apkrovos – raktai surikiuoti abėcėlės tvarka, glausti skirtukai
(, ir : be tarpų), UTF-8. Tai suteikia antrą, visiškai
lygiavertį būdą, kai jūsų sistema pateikia tik išanalizuotą JSON:
pakartotinai serializuokite kanoniškai 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 išvengsite JSON skaičių pakartotinio konvertavimo ypatumų kai kuriose programavimo kalbose.
Kuris slaptasis raktas?
| Šaltinis | Slaptasis raktas |
|---|---|
Žiniatinklio kabliuko galinis taškas (/v1/developer/webhook-endpoints) | Vienam galiniam taškui skirtas secret (48 šešioliktainiai simboliai), grąžinamas vieną kartą kuriant |
| Senas vieno URL žiniatinklio kabliukas | Organizacijai skirtas secret, grąžinamas naudojant GET /v1/webhook |
Įrankio galinio taško iškvietimas (tiesioginis iškvietimas į jūsų endpoint.url) | Organizacijos lygio žiniatinklio kabliuko slaptasis raktas (tas pats kaip seno vieno URL žiniatinklio kabliuko) – ne vienam galiniam taškui skirtas slaptasis raktas |
Saugokite slaptąjį raktą savo slaptųjų raktų tvarkytuvėje arba aplinkos kintamajame – niekada jo neįtraukite į kodo saugyklą.
Etaloninės realizacijos
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)
end
Konkrečių sistemų 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— skaitinis vykstančio skambučio ID.X-ThunderPhone-Signature— HMAC-SHA256, pasirašytas naudojant jūsų organizacijos lygio žiniatinklio kabliuko paslaptį, 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ėl naudokiteverify(b"", sig, secret)(Python) arbaverify(Buffer.alloc(0), sig, secret)(Node). Neskaičiuokite maišos iš užklausos eilutės.- Organizacijos, neturinčios sukonfigūruoto ankstesnės versijos žiniatinklio kabliuko, neturi organizacijos paslapties. Tokiu
atveju įrankių iškvietimuose pateikiamas tik
X-ThunderPhone-Call-ID, o parašo antraštės nėra. Sukonfigūruokite ankstesnės versijos ž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šsiuntimas žiniatinklio kabliuko režimu (įrankiai be endpoint, pateikiami
į jūsų organizacijos žiniatinklio kabliuką kaip telephony.tool / web.tool) yra įprastas
pasirašytas žiniatinklio kabliukas — taikoma pirmiau pateikta standartinė procedūra. Abiejų
užklausų formų aprašą rasite Funkcijų įrankiai.
Dažnos klaidos
Pakartotinis serializavimas naudojant numatytąjį formatavimą
Išanalizavus užklausos turinį ir pakartotinai jį išvedus naudojant JSON bibliotekos
numatytuosius nustatymus (tarpai po , / :, įterpimo tvarka išdėstyti raktai), gaunami
skirtingi baitai ir sugadinamas HMAC. Patikrinkite neapdorotą užklausos turinį arba, jei
privalote jį pakartotinai serializuoti, tiksliai atitikite mūsų kanoninę formą: surikiuoti
raktai, kompaktiški skirtukai, UTF-8.
Sistema automatiškai analizuoja JSON
Express express.json() tarpinė programinė įranga sunaudoja užklausos turinio srautą,
todėl prarandate neapdorotus baitus. Konkrečiai webhook maršrutui naudokite
express.raw() arba išsaugokite neapdorotą užklausos turinį prieš tarpinę programinę įrangą.
Tas pats taikoma NestJS / Koa — peržiūrėkite jų „neapdoroto užklausos turinio“ dokumentaciją.
Nuo vykdymo laiko priklausantis palyginimas
expected === signature JS kalboje arba expected == signature Python kalboje yra
palyginimai, kurių vykdymo laikas skiriasi. Atitinkamai naudokite
crypto.timingSafeEqual arba hmac.compare_digest. Našumo skirtumas
neegzistuoja.
Neteisingas įrankių galinių taškų slaptasis raktas
Tiesioginiai įrankių galinių taškų iškvietimai pasirašomi naudojant organizacijos lygmens webhook
slaptąjį raktą (GET /v1/webhook) — ne naudojant atskiro galinio taško slaptąjį raktą
iš /v1/developer/webhook-endpoints. Pakartotinai naudokite tą pačią verify()
funkciją, tačiau įsitikinkite, kad įrankių maršrutuose jai perduodate organizacijos slaptąjį raktą.
Užklausos eilutės maišos skaičiavimas GET/DELETE įrankiams
Įrankių metodams be užklausos turinio parašas apima tuščią baitų eilutę, todėl išlieka vienas universalus metodas: apskaičiuokite HMAC neapdorotam užklausos turiniui, kad ir koks jis būtų. URL arba užklausos eilutės maiša niekada neatitiks.
401 negrąžinimas esant neatitikimui
Grąžinus 200, kai patikra nepavyksta, apdorojimo programa tampa pakartotinio atkūrimo taikiniu. Jei patikra nepavyksta, visada atsakykite ne 2xx kodu.
Kiti veiksmai
Pristatymo semantika, pakartotiniai bandymai, šaltinio IP adresai.
Tvarkykite kelis URL, keiskite slaptuosius raktus.
Du įrankių iškvietimo keliai ir jų užklausų formos.
Sukurkite išsamią integraciją su įrankiais nuo pradžios iki pabaigos.