Thibitisha sahihi za webhook

Kila ombi tunalotuma kwa seva yako — utumaji wa webhook na uanzishaji wa endpoint za zana — hubeba sahihi ya HMAC-SHA256 kwenye kichwa cha X-ThunderPhone-Signature. Sanidi uthibitishaji kwa usahihi mara moja na utumie helper hiyo hiyo katika kila handler.

Algorithm

  1. Soma body ghafi ya ombi — byte halisi tulizokutumia kupitia POST.
  2. Kokotoa hmac_sha256(secret, body).hexdigest().
  3. Linganisha kwa muda usiobadilika na X-ThunderPhone-Signature. (Ulinganishaji wa kawaida wa string hufichua taarifa za muda.)

Tunasaini byte halisi tunazotuma, kwa hivyo kuthibitisha body ghafi hufanya kazi kila wakati. Byte hizo pia ni usawazishaji wa JSON wa kanoni wa payload — funguo zimepangwa kialfabeti, vitenganishi vimebanwa (, na : bila nafasi), UTF-8. Hilo hukupa mbinu ya pili iliyo sawa kabisa wakati framework yako inaonyesha JSON iliyochanganuliwa pekee: sawazisha upya kwa kanoni na ufanye HMAC kwenye hiyo.

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

Pendelea body ghafi — ni hatua moja pungufu na haathiriwi na hitilafu za kubadilisha JSON number kwenda na kutoka katika baadhi ya lugha.

Siri ipi?

ChanzoSiri
Endpoint ya webhook (/v1/developer/webhook-endpoints)secret ya kila endpoint (herufi 48 za hex) inayorejeshwa mara moja wakati wa kuunda
Webhook ya urithi yenye URL mojasecret ya kila org inayorejeshwa kwenye GET /v1/webhook
Uanzishaji wa endpoint ya zana (mwito wa moja kwa moja kwa endpoint.url yako)Siri ya webhook ya kiwango cha org (ileile ya webhook ya urithi yenye URL moja) — si siri ya kila endpoint

Hifadhi siri katika secret manager yako au env var — usiiweke kamwe kwenye commit.

Utekelezaji wa marejeleo

Zote nne huthibitisha body ghafi ya ombi:

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

Uunganishaji mahususi wa framework

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

Kuthibitisha miito ya zana

Ejenti inapoitisha moja kwa moja mojawapo ya zana zako za function (zana ina endpoint), ombi hubeba header mbili za ThunderPhone pamoja na endpoint.headers ulizosanidi:

Kisaidizi kilekile cha verify() hufanya kazi bila kubadilishwa, kwa mambo mawili ya kuzingatia:

  1. Zana za GET / DELETE hazina mwili. Hoja husafirishwa kama vigezo vya query, na sahihi hukokotolewa juu ya mfuatano tupu wa baiti — hivyo verify(b"", sig, secret) (Python) au verify(Buffer.alloc(0), sig, secret) (Node). Usiweke hash kwenye mfuatano wa query.
  2. Org zisizo na webhook ya zamani iliyosanidiwa hazina siri ya org. Katika hali hiyo miito ya zana hubeba X-ThunderPhone-Call-ID pekee na haina header ya sahihi. Sanidi webhook ya zamani (PUT /v1/webhook) ili kupata siri ya kutia sahihi, au thibitisha miito ya zana kwa header yako mwenyewe kupitia 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)
    ...

Utumaji wa zana katika hali ya webhook (zana zisizo na endpoint, zinazowasilishwa kwenye webhook ya org yako kama telephony.tool / web.tool) ni webhook ya kawaida iliyotiwa sahihi — utaratibu wa kawaida hapo juu unatumika. Tazama Zana za Function kwa miundo yote miwili ya ombi.

Makosa ya kawaida

Kusajilisha upya kwa uumbizaji chaguomsingi

Kuchanganua body na kuitoa tena kwa chaguomsingi za maktaba yako ya JSON (nafasi baada ya , / :, funguo katika mpangilio wa kuingizwa) huzalisha baiti tofauti na kuvunja HMAC. Thibitisha body ghafi — au ikiwa ni lazima uisajili upya, linganisha kabisa na umbo letu la kawaida: funguo zilizopangwa, vitenganishi finyu, UTF-8.

Framework huchanganua JSON kiotomatiki

Middleware ya express.json() ya Express hutumia mtiririko wa body na unapoteza baiti ghafi. Tumia express.raw() mahsusi kwenye route ya webhook, au hifadhi body ghafi kwenye pre-middleware. Hali ni sawa kwa NestJS / Koa — angalia nyaraka zao za "raw body".

Ulinganisho usio salama kwa muda

expected === signature katika JS au expected == signature katika Python ni ulinganisho unaotofautiana kwa muda. Tumia crypto.timingSafeEqual au hmac.compare_digest mtawalia. Tofauti ya utendaji haipo.

Secret isiyo sahihi kwa endpoint za tool

Miito ya moja kwa moja ya endpoint za tool husainiwa kwa secret ya webhook ya kiwango cha org (GET /v1/webhook) — si kwa secret yoyote ya kila endpoint kutoka /v1/developer/webhook-endpoints. Tumia tena function ileile ya verify(), lakini hakikisha unaipa secret ya org kwenye route za tool.

Kuhashi query string kwenye tool za GET/DELETE

Kwa mbinu za tool zisizo na body, signature hufunika string tupu ya baiti, ikidumisha utaratibu mmoja wa jumla: HMAC body ghafi ya request, iwe ni chochote. Kuhashi URL au query string hakutalingana kamwe.

Kutorejesha 401 wakati kuna kutolingana

Kurejesha 200 wakati uthibitishaji umeshindikana hufanya handler kuwa lengo la replay. Daima jibu kwa non-2xx ikiwa uthibitishaji utashindwa.


Hatua zinazofuata