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
- Soma body ghafi ya ombi — byte halisi tulizokutumia kupitia POST.
- Kokotoa
hmac_sha256(secret, body).hexdigest(). - 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?
| Chanzo | Siri |
|---|---|
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 moja | secret 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:
X-ThunderPhone-Call-ID— kitambulisho cha nambari cha simu inayoendelea.X-ThunderPhone-Signature— HMAC-SHA256, yenye ufunguo wa siri ya webhook ya kiwango cha org, juu ya baiti kamili za mwili wa ombi.
Kisaidizi kilekile cha verify() hufanya kazi bila kubadilishwa, kwa mambo mawili ya kuzingatia:
- Zana za
GET/DELETEhazina mwili. Hoja husafirishwa kama vigezo vya query, na sahihi hukokotolewa juu ya mfuatano tupu wa baiti — hivyoverify(b"", sig, secret)(Python) auverify(Buffer.alloc(0), sig, secret)(Node). Usiweke hash kwenye mfuatano wa query. - Org zisizo na webhook ya zamani iliyosanidiwa hazina siri ya org. Katika
hali hiyo miito ya zana hubeba
X-ThunderPhone-Call-IDpekee 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 kupitiaendpoint.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.