ThunderPhone 2.0 је стигао.Почните самостално, већ од 2 ¢/мин.Прочитајте објаву

Operations

Проверите потписе веб-хукова

Сваки захтев веб-хука и алатке који ThunderPhone шаље је потписан. Једном проверите потпис помоћу овде наведеног поступка, а затим исту проверу поново користите на свакој крајњој тачки коју покрећете.

Сваки захтев који шаљемо вашем серверу — webhook испоруке и позиви крајњих тачака алата — садржи HMAC-SHA256 потпис у заглављу X-ThunderPhone-Signature. Једном правилно подесите верификацију и укључите исти помоћни програм у сваки обрађивач.

Алгоритам

  1. Прочитајте сирово тело захтева — тачне бајтове које смо вам послали методом POST.
  2. Израчунајте hmac_sha256(secret, body).hexdigest().
  3. Упоредите у константном времену са X-ThunderPhone-Signature. (Наивно поређење стрингова открива информације о времену.)

Потписујемо тачно бајтове које преносимо, тако да верификација сировог тела увек функционише. Ти бајтови су такође канонска JSON серијализација садржаја — кључеви су сортирани по абецеди, раздвајачи су компактни (, и : без размака), а кодирање је UTF-8. То вам даје други, потпуно еквивалентан поступак када ваш оквир излаже само рашчлањени JSON: поново га серијализујте канонски и израчунајте HMAC над њим.

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

Дајте предност сировом телу — има један корак мање и отпорно је на необичности поновног претварања JSON бројева у неким језицима.

Која тајна?

ИзворТајна
Webhook крајња тачка (/v1/developer/webhook-endpoints)secret за сваку крајњу тачку (48 хексадецималних знакова) који се враћа једном при креирању
Застарели webhook са једним URL-омsecret за сваку организацију који се враћа при GET /v1/webhook
Позив крајње тачке алата (директан позив вашег endpoint.url)Webhook тајна на нивоу организације (иста као за застарели webhook са једним URL-ом) — не тајна за појединачну крајњу тачку

Чувајте тајну у менаџеру тајни или променљивој окружења — никада је не предајте у репозиторијум.

Референтне имплементације

Све четири верификују сирово тело захтева:

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

Повезивање специфично за оквир

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

Верификација позива алата

Када агент директно позове један од ваших алата функција (алат има endpoint), захтев садржи два ThunderPhone заглавља уз ваша конфигурисана endpoint.headers:

  • X-ThunderPhone-Call-ID — нумерички ИД активног позива.
  • X-ThunderPhone-Signature — HMAC-SHA256, са кључем који је ваша тајна вредност веб-хука на нивоу организације, над тачним бајтовима тела захтева.

Иста помоћна функција verify() ради без измена, уз две напомене:

  1. Алати GET / DELETE немају тело. Аргументи се преносе као параметри упита, а потпис се израчунава над празним низом бајтова — дакле verify(b"", sig, secret) (Python) или verify(Buffer.alloc(0), sig, secret) (Node). Немојте хеширати ниску упита.
  2. Организације без конфигурисаног застарелог веб-хука немају тајну вредност организације. У том случају позиви алата садрже само X-ThunderPhone-Call-ID и немају заглавље потписа. Конфигуришите застарели веб-хук (PUT /v1/webhook) да бисте добили тајну вредност за потписивање или аутентификујте позиве алата сопственим заглављем преко 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)
    ...

Отпрема алата у режиму веб-хука (алати без endpoint, испоручени на веб-хук ваше организације као telephony.tool / web.tool) је обичан потписани веб-хук — важи стандардни поступак изнад. Погледајте Алати функција за оба облика захтева.

Уобичајене замке

Поновна серијализација са подразумеваним форматирањем

Парсирање тела и његово поновно исписивање помоћу подразумеваних подешавања Ваше JSON библиотеке (размаци после , / :, кључеви по редоследу уметања) даје различите бајтове и нарушава HMAC. Проверите сирово тело — или, ако морате поново да га серијализујете, тачно ускладите наш канонски облик: сортирани кључеви, компактни раздвајачи, UTF-8.

Оквир аутоматски парсира JSON

Express-ов express.json() посреднички софтвер троши ток тела и губите сирове бајтове. Користите express.raw() посебно на webhook рути или баферујте сирово тело у претходном посредничком софтверу. Исто важи за NestJS / Koa — погледајте њихову документацију о „сировом телу“.

Поређење небезбедно у погледу времена

expected === signature у JS или expected == signature у Python-у су поређења променљивог трајања. Користите crypto.timingSafeEqual односно hmac.compare_digest. Разлика у перформансама је занемарљива.

Погрешна тајна за крајње тачке алата

Позиви директно ка крајњим тачкама алата потписују се помоћу webhook тајне на нивоу организације (GET /v1/webhook) — не помоћу било које тајне по крајњој тачки из /v1/developer/webhook-endpoints. Поново употребите исту функцију verify(), али проверите да ли јој прослеђујете тајну организације на рутама алата.

Хеширање упитног низа код GET/DELETE алата

За методе алата без тела, потпис обухвата празан низ бајтова, чиме се задржава један универзални поступак: примените HMAC на сирово тело захтева, какво год да је. Хеширање URL-а или упитног низа никада се неће подударати.

Невраћање 401 при неподударности

Враћање 200 при неуспешној верификацији чини обрађивач метом за поновљене захтеве. Увек одговорите статусом који није 2xx ако верификација не успе.


Следећи кораци