Vahvista webhook-allekirjoitukset
Jokainen ThunderPhonen lähettämä webhook- ja työkalupyyntö allekirjoitetaan. Vahvista allekirjoitus kerran tämän ohjeen avulla ja käytä sitten samaa tarkistusta jokaisessa käyttämässäsi päätepisteessä.
Jokaisessa palvelimellesi lähettämässämme pyynnössä — webhook-toimituksissa ja
työkalupäätepisteiden kutsuissa — on HMAC-SHA256-allekirjoitus
X-ThunderPhone-Signature-otsakkeessa. Toteuta vahvistus kerran oikein ja
käytä samaa apufunktiota jokaisessa käsittelijässä.
Algoritmi
- Lue pyynnön raaka runko — täsmälleen ne tavut, jotka POSTasimme sinulle.
- Laske
hmac_sha256(secret, body).hexdigest(). - Vertaa sitä vakioaikaisesti arvoon
X-ThunderPhone-Signature. (Yksinkertainen merkkijonovertailu vuotaa ajoitustietoa.)
Allekirjoitamme täsmälleen lähettämämme tavut, joten raakarungon
vahvistaminen toimii aina. Nämä tavut ovat myös hyötykuorman kanoninen JSON-sarjoitus
— avaimet aakkosjärjestyksessä, tiiviit erottimet
(, ja : ilman välilyöntejä), UTF-8. Tämä tarjoaa toisen, täysin
vastaavan tavan, kun kehys tarjoaa vain jäsennetyn JSONin:
sarjoita kanonisesti uudelleen ja laske siitä HMAC.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")Suosi raakarunkoa — siinä on yksi vaihe vähemmän, eikä se altistu joidenkin kielten JSON-lukujen edestakaisen muunnoksen erikoisuuksille.
Mikä salaisuus?
| Lähde | Salaisuus |
|---|---|
Webhook-päätepiste (/v1/developer/webhook-endpoints) | Päätepistekohtainen secret (48 heksamerkkiä), joka palautetaan kerran luotaessa |
| Vanha yhden URL-osoitteen webhook | Organisaatiokohtainen secret, joka palautetaan pyynnössä GET /v1/webhook |
Työkalupäätepisteen kutsu (suora kutsu osoitteeseen endpoint.url) | Organisaatiotason webhook-salaisuus (sama kuin vanhassa yhden URL-osoitteen webhookissa) — ei päätepistekohtainen salaisuus |
Tallenna salaisuus salaisuuksien hallintaan tai ympäristömuuttujaan — älä koskaan commitoi sitä.
Viitetoteutukset
Kaikki neljä vahvistavat pyynnön raakarungon:
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)
endKehyskohtainen kytkentä
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})Työkalukutsujen varmistaminen
Kun agentti kutsuu jotakin sinun
funktiotyökaluistasi suoraan (työkalulla on
endpoint), pyyntö sisältää kaksi ThunderPhone-otsaketta määrittämiesi
endpoint.headers-otsakkeiden lisäksi:
X-ThunderPhone-Call-ID— käynnissä olevan puhelun numeerinen tunniste.X-ThunderPhone-Signature— HMAC-SHA256, joka käyttää avaimena organisaatiotason webhook-salaisuutta ja lasketaan pyynnön täsmällisistä rungon tavuista.
Sama verify()-apuohjelma toimii sellaisenaan, mutta huomioi kaksi poikkeusta:
GET/DELETE-työkaluilla ei ole runkoa. Argumentit välitetään kyselyparametreina, ja allekirjoitus lasketaan tyhjästä tavujonosta — siisverify(b"", sig, secret)(Python) taiverify(Buffer.alloc(0), sig, secret)(Node). Älä tiivistä kyselymerkkijonoa.- Organisaatioilla, joilla ei ole määritettyä vanhaa webhookia, ei ole
organisaatiosalaisuutta. Tällöin työkalukutsut sisältävät vain
X-ThunderPhone-Call-ID-otsakkeen, eivätkä allekirjoitusotsaketta. Määritä vanha webhook (PUT /v1/webhook) saadaksesi allekirjoitussalaisuuden tai todenna työkalukutsut omalla otsakkeellasiendpoint.headers-asetuksen kautta.
@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)
...Webhook-tilan työkalujen välitys (työkalut ilman endpoint-asetusta,
jotka toimitetaan organisaatiosi webhookiin tyyppeinä telephony.tool /
web.tool) on tavallinen allekirjoitettu webhook — yllä oleva vakiomenettely
pätee. Katso molemmat pyyntömuodot kohdasta
Funktiotyökalut.
Yleiset sudenkuopat
Uudelleensarjoittaminen oletusmuotoilulla
Rungon jäsentäminen ja sen uudelleenkirjoittaminen JSON-kirjastosi
oletusasetuksilla (välilyönnit merkkien , / : jälkeen, lisäysjärjestystä noudattavat avaimet) tuottaa
eri tavut ja rikkoo HMACin. Vahvista raakarunko — tai jos sinun
on sarjoitettava uudelleen, vastaa tarkasti kanonista muotoamme: lajitellut
avaimet, tiiviit erottimet, UTF-8.
Kehys jäsentää JSONin automaattisesti
Expressin express.json()-middleware kuluttaa rungon tietovirran
ja menetät raakatavut. Käytä express.raw()-middlewarea nimenomaan webhook-reitillä
tai puskuroi raakarunko esiväliohjelmassa.
Sama koskee NestJS:ää / Koaa — tarkista niiden "raaka runko" -dokumentaatio.
Ajoitusturvaton vertailu
expected === signature JS:ssä tai expected == signature
Pythonissa ovat ajoituksesta riippuvia vertailuja. Käytä vastaavasti crypto.timingSafeEqual-
tai hmac.compare_digest-funktiota. Suorituskykyero
on olematon.
Väärä salaisuus työkalupäätepisteille
Suorat työkalupäätepistekutsut allekirjoitetaan organisaatiotason webhook-salaisuudella
(GET /v1/webhook) — ei millään päätepistekohtaisella salaisuudella
polusta /v1/developer/webhook-endpoints. Käytä samaa verify()-
funktiota uudelleen, mutta varmista, että syötät sille organisaation salaisuuden työkalureiteillä.
Kyselymerkkijonon tiivistäminen GET/DELETE-työkaluissa
Rungottomissa työkalumenetelmissä allekirjoitus kattaa tyhjän tavujonon, joten käytössä on yksi yleispätevä menettely: laske HMAC pyynnön raakarungolle, mikä se sitten onkin. URL-osoitteen tai kyselymerkkijonon tiiviste ei koskaan täsmää.
401-vastauksen palauttamatta jättäminen, kun allekirjoitus ei täsmää
200-vastauksen palauttaminen epäonnistuneen vahvistuksen jälkeen tekee käsittelijästä uudelleenlähetyshyökkäyksen kohteen. Palauta aina muu kuin 2xx-vastaus, jos vahvistus epäonnistuu.
Seuraavat vaiheet
Toimitussemantiikka, uudelleenyritykset, lähde-IP-osoitteet.
Hallitse useita URL-osoitteita, kierrätä salaisuuksia.
Kaksi työkalukutsun polkua ja niiden pyyntömuodot.
Rakenna täydellinen työkalutuettu integraatio alusta loppuun.