ThunderPhone 2.0 ir klāt.Sāciet uzreiz — no 2 centiem minūtē.Lasīt paziņojumu

Operations

Pārbaudiet tīmekļa aizķeres parakstus

Katrs ThunderPhone nosūtītais tīmekļa aizķeres un rīka pieprasījums ir parakstīts. Vienreiz pārbaudiet parakstu, izmantojot šeit sniegto recepti, pēc tam izmantojiet to pašu pārbaudi katrā darbinātajā galapunktā.

Katram pieprasījumam, ko nosūtām uz jūsu serveri — tīmekļa aizķeres piegādēm un rīku galapunktu izsaukumiem — galvenē X-ThunderPhone-Signature ir HMAC-SHA256 paraksts. Vienreiz pareizi ieviesiet pārbaudi un izmantojiet to pašu palīgfunkciju katrā apstrādātājā.

Algoritms

  1. Nolasiet neapstrādāto pieprasījuma pamattekstu — precīzus baitus, ko jums nosūtījām ar POST.
  2. Aprēķiniet hmac_sha256(secret, body).hexdigest().
  3. Salīdziniet konstantā laikā ar X-ThunderPhone-Signature. (Naiva virkņu salīdzināšana atklāj laika informāciju.)

Mēs parakstām tieši tos baitus, ko pārsūtām, tāpēc neapstrādātā pamatteksta pārbaude vienmēr darbojas. Šie baiti ir arī slodzes kanoniskā JSON serializācija — atslēgas sakārtotas alfabētiski, kompakti atdalītāji (, un : bez atstarpēm), UTF-8. Tas sniedz otru, pilnībā līdzvērtīgu risinājumu gadījumos, kad jūsu ietvars nodrošina tikai parsētu JSON: serializējiet kanoniski atkārtoti un aprēķiniet tam HMAC.

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

Dodiet priekšroku neapstrādātajam pamattekstam — tas ir par vienu soli mazāk un nav pakļauts JSON skaitļu atkārtotas konvertēšanas īpatnībām dažās valodās.

Kurš noslēpums?

AvotsNoslēpums
Tīmekļa aizķeres galapunkts (/v1/developer/webhook-endpoints)Katram galapunktam paredzēts secret (48 heksadecimālās rakstzīmes), kas tiek atgriezts vienreiz izveides laikā
Mantotā tīmekļa aizķere ar vienu URLOrganizācijai paredzēts secret, kas tiek atgriezts ar GET /v1/webhook
Rīka galapunkta izsaukums (tiešs izsaukums uz jūsu endpoint.url)Organizācijas līmeņa tīmekļa aizķeres noslēpums (tas pats, kas mantotajai tīmekļa aizķerei ar vienu URL) — nevis katram galapunktam paredzēts noslēpums

Glabājiet noslēpumu savu noslēpumu pārvaldniekā vai vides mainīgajā — nekad to neiekļaujiet repozitorijā.

Atsauces ieviešanas

Visas četras pārbauda neapstrādāto pieprasījuma pamattekstu:

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

Ietvarspecifiska pieslēgšana

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

Rīku izsaukumu verificēšana

Kad balss aģents tieši izsauc kādu no jūsu funkciju rīkiem (rīkam ir endpoint), pieprasījumā līdzās jūsu konfigurētajiem endpoint.headers tiek nosūtītas divas ThunderPhone galvenes:

  • X-ThunderPhone-Call-ID — aktīvā zvana skaitliskais ID.
  • X-ThunderPhone-Signature — HMAC-SHA256, kas izveidots ar jūsu organizācijas līmeņa tīmekļa aizķeres noslēpumu, izmantojot precīzus pieprasījuma pamatteksta baitus.

Tas pats verify() palīgrīks darbojas bez izmaiņām, ar divām niansēm:

  1. GET / DELETE rīkiem nav pamatteksta. Argumenti tiek nodoti kā vaicājuma parametri, un paraksts tiek aprēķināts, izmantojot tukšu baitu virkni — tātad verify(b"", sig, secret) (Python) vai verify(Buffer.alloc(0), sig, secret) (Node). Neveidojiet jaucējvērtību no vaicājuma virknes.
  2. Organizācijām bez konfigurētas mantotās tīmekļa aizķeres nav organizācijas noslēpuma. Šādā gadījumā rīku izsaukumos ir tikai X-ThunderPhone-Call-ID, bet nav paraksta galvenes. Konfigurējiet mantoto tīmekļa aizķeri (PUT /v1/webhook), lai iegūtu parakstīšanas noslēpumu, vai autentificējiet rīku izsaukumus ar savu galveni, izmantojot 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)
    ...

Tīmekļa aizķeres režīma rīku nosūtīšana (rīki bez endpoint, kas tiek piegādāti uz jūsu organizācijas tīmekļa aizķeri kā telephony.tool / web.tool) ir parasta parakstīta tīmekļa aizķere — piemērojama iepriekš norādītā standarta procedūra. Abus pieprasījumu formātus skatiet sadaļā Funkciju rīki.

Biežākās kļūdas

Atkārtota serializēšana ar noklusējuma formatējumu

Pamatteksta parsēšana un atkārtota izvade ar JSON bibliotēkas noklusējuma iestatījumiem (atstarpes pēc , / :, ievietošanas secībā kārtotas atslēgas) rada atšķirīgus baitus un sabojā HMAC. Verificējiet neapstrādāto pamattekstu — vai, ja tas ir jāserializē atkārtoti, precīzi atbilstiet mūsu kanoniskajai formai: sakārtotas atslēgas, kompakti atdalītāji, UTF-8.

Ietvars automātiski parsē JSON

Express express.json() starpprogrammatūra patērē pamatteksta plūsmu, un jūs zaudējat neapstrādātos baitus. Konkrēti tīmekļa aizķeres maršrutā izmantojiet express.raw() vai saglabājiet neapstrādāto pamattekstu buferī pirms starpprogrammatūras. Tas pats attiecas uz NestJS / Koa — skatiet to dokumentāciju par “neapstrādātu pamattekstu”.

Salīdzināšana, kas nav droša pret laika uzbrukumiem

expected === signature JS vai expected == signature valodā Python ir salīdzinājumi ar mainīgu izpildes laiku. Attiecīgi izmantojiet crypto.timingSafeEqual vai hmac.compare_digest. Veiktspējas atšķirība ir niecīga.

Nepareizs noslēpums rīku galapunktiem

Tiešie rīku galapunktu izsaukumi tiek parakstīti ar organizācijas līmeņa tīmekļa aizķeres noslēpumu (GET /v1/webhook) — nevis ar kādu galapunktam specifisku noslēpumu no /v1/developer/webhook-endpoints. Atkārtoti izmantojiet to pašu verify() funkciju, bet pārliecinieties, ka rīku maršrutos tai nododat organizācijas noslēpumu.

Vaicājuma virknes jaucējkodēšana GET/DELETE rīkiem

Rīku metodēm bez pamatteksta paraksts aptver tukšo baitu virkni, saglabājot vienu universālu principu: HMAC neapstrādātajam pieprasījuma pamattekstam, lai kāds tas būtu. URL vai vaicājuma virknes jaucējkodēšana nekad neatbildīs.

401 neatgriešana neatbilstības gadījumā

200 atgriešana neveiksmīgas verifikācijas gadījumā padara apstrādātāju par atkārtotas atskaņošanas uzbrukuma mērķi. Ja verifikācija neizdodas, vienmēr atbildiet ar statusu, kas nav 2xx.


Nākamās darbības