ویب ہک دستخطوں کی تصدیق کریں
ہم آپ کے سرور کو بھیجی جانے والی ہر درخواست — ویب ہک ڈیلیوریز اور
ٹول-اینڈپوائنٹ انوووکیشنز — میں X-ThunderPhone-Signature ہیڈر کے اندر
HMAC-SHA256 دستخط شامل ہوتا ہے۔ تصدیق ایک بار درست کریں اور
اسی ہیلپر کو ہر ہینڈلر میں استعمال کریں۔
الگورتھم
- درخواست کا اصل باڈی پڑھیں — بالکل وہی بائٹس جو ہم نے آپ کو POST کیے تھے۔
hmac_sha256(secret, body).hexdigest()کا حساب کریں۔- مستقل وقت میں
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 نمبروں کے راؤنڈ-ٹرپ ہونے کی پیچیدگیوں سے محفوظ ہے۔
کون سا secret؟
| ماخذ | secret |
|---|---|
ویب ہک اینڈپوائنٹ (/v1/developer/webhook-endpoints) | ہر اینڈپوائنٹ کا secret (48 ہیکس حروف) جو تخلیق کے وقت ایک بار واپس ملتا ہے |
| پرانا واحد-URL ویب ہک | ہر تنظیم کا secret جو GET /v1/webhook پر واپس ملتا ہے |
ٹول-اینڈپوائنٹ انوووکیشن (آپ کے endpoint.url کو براہ راست کال) | تنظیم کی سطح کا ویب ہک secret (وہی جو پرانے واحد-URL ویب ہک کے لیے ہے) — ہر اینڈپوائنٹ کا secret نہیں |
secret کو اپنے secret manager یا env var میں محفوظ کریں — اسے کبھی commit نہ کریں۔
حوالہ جاتی نفاذ
چاروں اصل درخواست باڈی کی تصدیق کرتے ہیں:
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
فریم ورک کے لحاظ سے کنکشن
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})
ٹول کالز کی تصدیق
جب ایجنٹ آپ کے فنکشن ٹولز میں سے کسی کو براہِ راست استعمال کرتا ہے
(ٹول کے پاس ایک endpoint ہوتا ہے)، تو درخواست آپ کے ترتیب شدہ
endpoint.headers کے ساتھ ThunderPhone کے دو ہیڈرز لے کر آتی ہے:
X-ThunderPhone-Call-ID— جاری کال کا عددی آئی ڈی۔X-ThunderPhone-Signature— آپ کے آرگ سطح کے ویب ہک سیکرٹ سے کلید کردہ HMAC-SHA256، جو درخواست کے عین باڈی بائٹس پر مبنی ہوتا ہے۔
وہی verify() ہیلپر بغیر کسی تبدیلی کے کام کرتا ہے، البتہ دو نکات ہیں:
GET/DELETEٹولز کی کوئی باڈی نہیں ہوتی۔ آرگیومنٹس کوئری پیرامیٹرز کے طور پر جاتے ہیں، اور دستخط خالی بائٹ اسٹرنگ پر شمار کیا جاتا ہے — یعنیverify(b"", sig, secret)(Python) یاverify(Buffer.alloc(0), sig, secret)(Node)۔ کوئری اسٹرنگ کو ہیش نہ کریں۔- ایسے آرگز جن میں لیگیسی ویب ہک ترتیب شدہ نہ ہو، ان کا کوئی آرگ سیکرٹ نہیں ہوتا۔
اس صورت میں ٹول کالز میں صرف
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() مڈل ویئر باڈی اسٹریم استعمال کر لیتا ہے
اور آپ خام بائٹس کھو دیتے ہیں۔ خاص طور پر webhook روٹ پر express.raw() استعمال کریں،
یا پری-مڈل ویئر میں خام باڈی کو بفر کریں۔
NestJS / Koa کے لیے بھی یہی معاملہ ہے — ان کی "خام باڈی" دستاویزات دیکھیں۔
ٹائمنگ کے لحاظ سے غیر محفوظ موازنہ
JS میں expected === signature یا Python میں expected == signature
ٹائمنگ کے لحاظ سے متغیر موازنے ہیں۔ بالترتیب crypto.timingSafeEqual
یا hmac.compare_digest استعمال کریں۔ کارکردگی میں فرق
نہ ہونے کے برابر ہے۔
ٹول اینڈ پوائنٹس کے لیے غلط سیکرٹ
براہ راست ٹول-اینڈ پوائنٹ کالز پر تنظیم کی سطح کے webhook
سیکرٹ (GET /v1/webhook) سے دستخط ہوتے ہیں — نہ کہ
/v1/developer/webhook-endpoints کے کسی فی-اینڈ پوائنٹ سیکرٹ سے۔ وہی verify()
فنکشن دوبارہ استعمال کریں، لیکن یقینی بنائیں کہ آپ ٹول روٹس پر اسے تنظیم کا سیکرٹ دیتے ہیں۔
GET/DELETE ٹولز میں کوئری اسٹرنگ کو ہیش کرنا
باڈی کے بغیر ٹول میتھڈز کے لیے دستخط خالی بائٹ اسٹرنگ پر مشتمل ہوتا ہے، جس سے ایک ہی عمومی طریقہ برقرار رہتا ہے: خام ریکویسٹ باڈی کا HMAC بنائیں، جیسی بھی وہ ہو۔ URL یا کوئری اسٹرنگ کو ہیش کرنا کبھی مطابقت نہیں کرے گا۔
عدم مطابقت پر 401 واپس نہ کرنا
ناکام تصدیق پر 200 واپس کرنا ہینڈلر کو ری پلے ہدف بنا دیتا ہے۔ تصدیق ناکام ہونے پر ہمیشہ غیر-2xx جواب دیں۔