ThunderPhone 2.0 jest już dostępny.Uruchom samodzielnie — od 2 centów/min.Przeczytaj komunikat

Operations

Weryfikowanie podpisów webhooków

Każdy webhook i każde żądanie narzędzia wysyłane przez ThunderPhone są podpisane. Zweryfikuj podpis raz, korzystając z zamieszczonej tutaj instrukcji, a następnie używaj tej samej weryfikacji w każdym obsługiwanym punkcie końcowym.

Każde żądanie, które wysyłamy na Twój serwer — dostarczenia webhooków i wywołania endpointów narzędzi — zawiera podpis HMAC-SHA256 w nagłówku X-ThunderPhone-Signature. Skonfiguruj weryfikację raz, a następnie użyj tego samego pomocnika w każdym handlerze.

Algorytm

  1. Odczytaj surowe ciało żądania — dokładne bajty, które wysłaliśmy metodą POST.
  2. Oblicz hmac_sha256(secret, body).hexdigest().
  3. Porównaj w czasie stałym z X-ThunderPhone-Signature. (Naiwne porównanie ciągów ujawnia informacje o czasie.)

Podpisujemy dokładnie te bajty, które przesyłamy, dlatego weryfikacja surowego ciała zawsze działa. Te bajty są również kanoniczną serializacją JSON payloadu — klucze posortowane alfabetycznie, zwarte separatory (, i : bez spacji), UTF-8. Daje to drugi, w pełni równoważny sposób, gdy Twój framework udostępnia tylko sparsowany JSON: ponownie serializuj kanonicznie i oblicz HMAC dla wyniku.

# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")

Preferuj surowe ciało — to o jeden krok mniej i nie jest podatne na problemy z ponownym przetwarzaniem liczb JSON w niektórych językach.

Który sekret?

ŹródłoSekret
Endpoint webhooka (/v1/developer/webhook-endpoints)Sekret secret dla danego endpointu (48 znaków szesnastkowych), zwracany jednorazowo podczas tworzenia
Starszy webhook z pojedynczym adresem URLSekret secret dla organizacji zwracany przez GET /v1/webhook
Wywołanie endpointu narzędzia (bezpośrednie wywołanie Twojego endpoint.url)Sekret webhooka na poziomie organizacji (ten sam co dla starszego webhooka z pojedynczym adresem URL) — nie sekret dla pojedynczego endpointu

Przechowuj sekret w menedżerze sekretów lub zmiennej środowiskowej — nigdy nie zapisuj go w repozytorium.

Implementacje referencyjne

Wszystkie cztery weryfikują surowe ciało żądania:

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

Integracja specyficzna dla frameworka

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})

Weryfikowanie wywołań narzędzi

Gdy agent wywołuje bezpośrednio jedno z Twoich narzędzi funkcyjnych (narzędzie ma endpoint), żądanie zawiera dwa nagłówki ThunderPhone obok skonfigurowanych przez Ciebie endpoint.headers:

  • X-ThunderPhone-Call-ID — numeryczny identyfikator trwającego połączenia.
  • X-ThunderPhone-Signature — HMAC-SHA256 z kluczem w postaci Twojego sekretu webhooka na poziomie organizacji, obliczany na podstawie dokładnych bajtów treści żądania.

Ten sam pomocnik verify() działa bez zmian, z dwoma wyjątkami:

  1. Narzędzia GET / DELETE nie mają treści. Argumenty są przekazywane jako parametry zapytania, a podpis jest obliczany na podstawie pustego ciągu bajtów — więc verify(b"", sig, secret) (Python) lub verify(Buffer.alloc(0), sig, secret) (Node). Nie haszuj ciągu zapytania.
  2. Organizacje bez skonfigurowanego starszego webhooka nie mają sekretu organizacji. W takim przypadku wywołania narzędzi zawierają tylko X-ThunderPhone-Call-ID i nie mają nagłówka podpisu. Skonfiguruj starszy webhook (PUT /v1/webhook), aby uzyskać sekret podpisywania, lub uwierzytelniaj wywołania narzędzi własnym nagłówkiem za pośrednictwem 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)
    ...

Dystrybucja wywołań narzędzi w trybie webhooka (narzędzia bez endpoint, dostarczane do webhooka organizacji jako telephony.tool / web.tool) to zwykły podpisany webhook — obowiązuje standardowa procedura opisana powyżej. Zobacz Narzędzia funkcyjne, aby poznać oba formaty żądań.

Typowe pułapki

Ponowna serializacja z domyślnym formatowaniem

Przetworzenie treści i ponowne zapisanie jej przy użyciu domyślnych ustawień biblioteki JSON (spacje po , / :, klucze w kolejności wstawiania) daje inne bajty i powoduje błąd HMAC. Weryfikuj surową treść — lub jeśli musisz ją ponownie serializować, dokładnie dopasuj nasz kanoniczny format: posortowane klucze, zwarte separatory, UTF-8.

Framework automatycznie przetwarza JSON

Oprogramowanie pośredniczące Express express.json() zużywa strumień treści i tracisz surowe bajty. Użyj express.raw() konkretnie dla trasy webhooka albo buforuj surową treść w oprogramowaniu pośredniczącym poprzedzającym parser. Tak samo jest w NestJS / Koa — sprawdź ich dokumentację dotyczącą surowego ciała żądania.

Porównanie niebezpieczne czasowo

expected === signature w JS lub expected == signature w Pythonie to porównania zależne od czasu wykonania. Użyj odpowiednio crypto.timingSafeEqual lub hmac.compare_digest. Różnica w wydajności jest zerowa.

Nieprawidłowy sekret dla endpointów narzędzi

Bezpośrednie wywołania endpointów narzędzi są podpisywane za pomocą sekretu webhooka na poziomie organizacji (GET /v1/webhook) — nie za pomocą sekretu konkretnego endpointu z /v1/developer/webhook-endpoints. Użyj ponownie tej samej funkcji verify(), ale upewnij się, że na trasach narzędzi przekazujesz jej sekret organizacji.

Haszowanie ciągu zapytania w narzędziach GET/DELETE

W przypadku metod narzędzi bez treści podpis obejmuje pusty ciąg bajtów, co zachowuje jeden uniwersalny schemat: oblicz HMAC surowego ciała żądania, niezależnie od jego zawartości. Haszowanie adresu URL lub ciągu zapytania nigdy nie będzie zgodne.

Brak zwracania 401 przy niezgodności

Zwracanie 200 po nieudanej weryfikacji sprawia, że procedura obsługi staje się celem ataków typu replay. Zawsze odpowiadaj kodem innym niż 2xx, jeśli weryfikacja się nie powiedzie.


Kolejne kroki