Webhook imzalarını doğrulayın
ThunderPhone
Sunucunuza gönderdiğimiz her istek — webhook teslimatları ve
araç uç noktası çağrıları — X-ThunderPhone-Signature üst bilgisinde bir
HMAC-SHA256 imzası taşır. Doğrulamayı bir kez doğru şekilde uygulayın ve
aynı yardımcı işlevini her işleyiciye ekleyin.
Algoritma
- Ham istek gövdesini okuyun — size POST ettiğimiz tam baytları.
hmac_sha256(secret, body).hexdigest()hesaplayın.X-ThunderPhone-Signatureile sabit zamanda karşılaştırın. (Basit dize karşılaştırması zamanlama bilgilerini sızdırır.)
İlettiğimiz tam baytları imzalarız; bu nedenle ham gövdeyi doğrulamak
her zaman çalışır. Bu baytlar aynı zamanda yükün standart JSON serileştirmesidir —
anahtarlar alfabetik olarak sıralanır, ayırıcılar sıkışıktır
(boşluksuz , ve :), kodlama UTF-8'dir. Çerçeveniz yalnızca ayrıştırılmış JSON'u
sunuyorsa, size tamamen eşdeğer ikinci bir yöntem sağlar:
standart biçimde yeniden serileştirin ve bunun HMAC'ini hesaplayın.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")Ham gövdeyi tercih edin — bir adım daha azdır ve bazı dillerdeki JSON sayı gidiş-dönüş dönüştürme farklılıklarından etkilenmez.
Hangi gizli anahtar?
| Kaynak | Gizli anahtar |
|---|---|
Webhook uç noktası (/v1/developer/webhook-endpoints) | Oluşturulurken bir kez döndürülen, uç nokta başına secret (48 hex karakter) |
| Eski tek URL'li webhook | GET /v1/webhook çağrısında döndürülen, kuruluş başına secret |
Araç uç noktası çağrısı (endpoint.url adresinize doğrudan çağrı) | Kuruluş düzeyindeki webhook gizli anahtarı (eski tek URL'li webhook ile aynı anahtar) — uç nokta başına bir gizli anahtar değildir |
Gizli anahtarı gizli anahtar yöneticinizde veya ortam değişkeninde saklayın — asla sürüm kontrolüne eklemeyin.
Referans uygulamalar
Dördü de ham istek gövdesini doğrular:
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Çerçeveye özgü bağlantı
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})Araç çağrılarını doğrulama
Ajan, işlev araçlarınızdan birini doğrudan çağırdığında
(aracın bir endpoint değeri vardır), istek yapılandırdığınız
endpoint.headers ile birlikte iki ThunderPhone üst bilgisi taşır:
X-ThunderPhone-Call-ID— canlı çağrının sayısal kimliği.X-ThunderPhone-Signature— tam istek gövdesi baytları üzerinden, kuruluş düzeyindeki webhook gizli anahtarınız ile anahtarlanmış HMAC-SHA256.
Aynı verify() yardımcısı, iki farkla değiştirilmeden çalışır:
GET/DELETEaraçlarının gövdesi yoktur. Bağımsız değişkenler sorgu parametreleri olarak iletilir ve imza boş bayt dizisi üzerinden hesaplanır — yaniverify(b"", sig, secret)(Python) veyaverify(Buffer.alloc(0), sig, secret)(Node). Sorgu dizesini karma değerine dönüştürmeyin.- Eski webhook yapılandırılmamış kuruluşların kuruluş gizli anahtarı yoktur.
Bu durumda araç çağrıları yalnızca
X-ThunderPhone-Call-IDtaşır ve imza üst bilgisi içermez. İmzalama gizli anahtarı almak için eski webhook'u (PUT /v1/webhook) yapılandırın veya araç çağrılarınıendpoint.headersaracılığıyla kendi üst bilginizle doğrulayın.
@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)
...Webhook modunda araç yönlendirmesi (endpoint içermeyen ve kuruluş
webhook'unuza telephony.tool / web.tool olarak iletilen araçlar) normal
bir imzalı webhook'tur — yukarıdaki standart yöntem geçerlidir. Her iki
istek biçimi için İşlev Araçları sayfasına bakın.
Yaygın hatalar
Varsayılan biçimlendirmeyle yeniden serileştirme
Gövdeyi ayrıştırıp JSON kitaplığınızın varsayılanlarıyla yeniden
dökmek (, / : sonrasında boşluklar, ekleme sırasına göre anahtarlar)
farklı baytlar üretir ve HMAC'i bozar. Ham gövdeyi doğrulayın — veya
yeniden serileştirmeniz gerekiyorsa kanonik biçimimizle tam olarak eşleştirin:
sıralanmış anahtarlar, sıkıştırılmış ayırıcılar, UTF-8.
Çerçeve JSON'u otomatik ayrıştırıyor
Express'in express.json() ara yazılımı gövde akışını tüketir
ve ham baytları kaybedersiniz. Webhook rotasında özellikle express.raw() kullanın
veya ham gövdeyi bir ön ara yazılımda arabelleğe alın.
NestJS / Koa için de aynı durum geçerlidir — "ham gövde" belgelerini kontrol edin.
Zamanlama açısından güvenli olmayan karşılaştırma
JS'de expected === signature veya Python'da expected == signature
zamanlamaya göre değişen karşılaştırmalardır. Sırasıyla crypto.timingSafeEqual
veya hmac.compare_digest kullanın. Performans farkı yok denecek kadar azdır.
Araç uç noktaları için yanlış gizli anahtar
Doğrudan araç uç noktası çağrıları, kuruluş düzeyi webhook
gizli anahtarı (GET /v1/webhook) ile imzalanır — /v1/developer/webhook-endpoints
içindeki uç noktaya özel gizli anahtarlarla değil. Aynı verify()
işlevini yeniden kullanın, ancak araç rotalarında kuruluş gizli anahtarını verdiğinizden emin olun.
GET/DELETE araçlarında sorgu dizesini karma değerine dönüştürme
Gövdesiz araç yöntemlerinde imza boş bayt dizesini kapsar ve tek bir evrensel yöntem korunur: Ne olursa olsun ham istek gövdesine HMAC uygulayın. URL'yi veya sorgu dizesini karma değerine dönüştürmek asla eşleşmez.
Eşleşmezse 401 döndürmemek
Doğrulama başarısız olduğunda 200 döndürmek, işleyiciyi yeniden oynatma saldırılarının hedefi hâline getirir. Doğrulama başarısız olursa her zaman 2xx olmayan bir yanıt verin.