ThunderPhone 2.0 jau čia.Viską atlikite savarankiškai – nuo 2 ct/min.Skaityti pranešimą

Operations

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

  1. Nuskaitykite neapdorotą užklausos turinį — tikslius baitus, kuriuos jums išsiuntėme POST užklausa.
  2. Apskaičiuokite hmac_sha256(secret, body).hexdigest().
  3. 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?

ŠaltinisSlaptasis 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 webhookOrganizacijos 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į:

Python
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 "")
Node.js
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),
  );
}
Go
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))
}
Ruby
require "openssl"
 
def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end

Konkrečioms sistemoms skirtas prijungimas

FastAPI
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}
Express
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);
  },
);
Django
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:

  1. GET / DELETE įrankiai neturi turinio. Argumentai perduodami kaip užklausos parametrai, o parašas apskaičiuojamas pagal tuščią baitų eilutę — todėl verify(b"", sig, secret) (Python) arba verify(Buffer.alloc(0), sig, secret) (Node). Neskaičiuokite užklausos eilutės maišos.
  2. 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ę per endpoint.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.


Tolesni veiksmai