Open in
ویب ہک دستخطوں کی تصدیق کریں
ThunderPhone کی بھیجی گئی ہر ویب ہک اور ٹول درخواست پر ٹائم اسٹیمپ کے ساتھ دستخط ہوتے ہیں۔ یہاں موجود recipe یا SDK helper کے ذریعے اس کی تصدیق کریں اور پانچ منٹ سے زیادہ پرانی دوبارہ بھیجی گئی درخواستوں کو مسترد کریں۔
آپ کے سرور کو بھیجی جانے والی ہر درخواست (webhook ڈیلیوریز، پرانا single-URL webhook، اور tool-endpoint کالز) میں دو دستخط ہیڈرز شامل ہوتے ہیں۔ دونوں ایک ہی راز کے ساتھ HMAC-SHA256 ہیں۔
| ہیڈر | کن چیزوں پر دستخط کرتا ہے | ری پلے سے تحفظ |
|---|---|---|
X-ThunderPhone-Signature-V2 (تجویز کردہ) | ٹائم اسٹیمپ، طریقہ، پاتھ، query اور باڈی | 5 منٹ سے زیادہ پرانی درخواستیں مسترد کریں |
X-ThunderPhone-Signature | صرف باڈی | کوئی نہیں: حاصل کی گئی درخواست ہمیشہ تصدیق ہو جاتی ہے |
نئی انٹیگریشنز میں X-ThunderPhone-Signature-V2 کی تصدیق کریں اور
موجودہ انٹیگریشنز کو اس پر منتقل کریں۔ X-ThunderPhone-Signature اب بھی ہر
درخواست کے ساتھ، بغیر تبدیلی کے، بھیجا جاتا ہے، اس لیے اس پر بنے ہوئے ویریفائرز کام کرتے رہیں گے۔ ہم اسے بھیجنا بند کرنے سے پہلے ایک تاریخ کا اعلان کریں گے۔
دستخط V2
X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4etوہ Unix وقت ہے، سیکنڈز میں، جب ہم نے درخواست بھیجی۔ دوبارہ کوشش کی گئی ترسیل بھیجے جانے پر دوبارہ دستخط ہوتی ہے، اپنے الگtکے ساتھ۔v1نیچے دی گئی دستخط شدہ اسٹرنگ کا hex HMAC-SHA256 ہے، جس کے لیے آپ کا secret استعمال ہوتا ہے۔
قدر، کوما سے جدا شدہ key=value جوڑوں کی فہرست ہے۔ جن keys کو آپ
نہ پہچانیں انہیں نظر انداز کریں، اور اگر کوئی بھی v1 مماثل ہو تو درخواست قبول کریں: ہم آئندہ
کسی تبدیلی کے دوران دوسرا دستخط شامل کر سکتے ہیں۔
دستخط شدہ اسٹرنگ
چار فیلڈز، ہر ایک کے بعد ایک newline (\n, بائٹ 0x0A) سوائے
آخری کے:
<t>\n<METHOD>\n<path and query>\n<body>| فیلڈ | قدر |
|---|---|
t | header سے t کی قدر |
METHOD | بڑے حروف میں HTTP method: POST، GET، PUT، PATCH یا DELETE |
| راستہ اور کوئری | HTTP درخواست لائن سے درخواست کا ہدف: پہلے راستہ، پھر ? اور کوئری اسٹرنگ اگر موجود ہو۔ کوئی scheme، host یا fragment نہیں۔ اسے بالکل ویسا ہی استعمال کریں جیسا موصول ہوا، اسے decode کیے یا parameters کی ترتیب بدلے بغیر |
body | خام درخواست body۔ GET اور DELETE tool calls کی کوئی body نہیں ہوتی، اس لیے ان کی دستخط شدہ اسٹرنگ راستے کے بعد newline پر ختم ہوتی ہے |
تصدیق کرنے کے لیے:
- کسی بھی parsing سے پہلے، header، method، خام راستہ اور کوئری، اور خام body پڑھیں۔
- header سے
tاور ہرv1لیں۔ - اگر
tآپ کی گھڑی سے پہلے یا بعد میں 300 سیکنڈ سے زیادہ ہو تو درخواست مسترد کریں۔ - اپنے secret کے ساتھ دستخط شدہ اسٹرنگ کا hex HMAC-SHA256 حساب کریں۔
- اسے مستقل وقت میں
v1سے موازنہ کریں۔ اگر کچھ بھی مماثل نہ ہو تو401جواب دیں۔
عملی مثال
Secret whsec_example_4c1f2a9b۔ ہم یہ درخواست
t = 1767225600 (2026-01-01 00:00:00 UTC) پر بھیجتے ہیں:
POST /tools/lookup?region=us HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e
{"customer_id":"cust_123"}دستخط شدہ اسٹرنگ یہ چار لائنیں ہیں، body کے بعد کوئی newline نہیں:
1767225600
POST
/tools/lookup?region=us
{"customer_id":"cust_123"}اس کا HMAC-SHA256
03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e ہے۔
اس کے خلاف اپنے implementation کو چیک کریں، یا اسے openssl کے ساتھ دوبارہ بنائیں:
printf '1767225600\nPOST\n/tools/lookup?region=us\n{"customer_id":"cust_123"}' \
| openssl dgst -sha256 -hmac 'whsec_example_4c1f2a9b'اسی سیکنڈ میں /tools/lookup?customer_id=cust_123 پر ایک GET tool call
کی body خالی ہوتی ہے۔ اس کی دستخط شدہ اسٹرنگ
1767225600\nGET\n/tools/lookup?customer_id=cust_123\n ہے اور اس کا دستخط
3ce214cb0b8f2bf39b1d19d4e7265df9dbaff5a5f838d3cc6c753a8f7a4ad280 ہے۔
SDKs کے ساتھ
Python اور TypeScript SDKs میں ایک helper شامل ہے
(thunderphone 0.2.0 یا بعد کا ورژن، @thunderphone/sdk 0.2.0 یا بعد کا ورژن)۔ یہ
5 منٹ کی مدت چیک کرتا ہے، مستقل وقت میں موازنہ کرتا ہے، اور secrets کی فہرست
قبول کرتا ہے تاکہ آپ downtime کے بغیر rotate کر سکیں۔
from thunderphone import verify_signature
ok = verify_signature(
header, # the X-ThunderPhone-Signature-V2 value
secret=SECRET, # or [NEW_SECRET, OLD_SECRET] while rotating
method=method, # "POST"
path=path, # "/tools/lookup?region=us", exactly as received
body=raw_body, # bytes, before any JSON parsing
tolerance=300, # optional; seconds
)import { verifySignature } from "@thunderphone/sdk";
const ok = verifySignature({
header, // the X-ThunderPhone-Signature-V2 value
secret: SECRET, // or [NEW_SECRET, OLD_SECRET] while rotating
method, // "POST"
path, // "/tools/lookup?region=us", exactly as received
body: rawBody, // Buffer or string, before any JSON parsing
toleranceSeconds: 300, // optional
});SDK کے بغیر
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify_v2(header: str, secret: str, method: str, path: str, body: bytes) -> bool:
"""path is the path plus ?query exactly as received; body is the raw body."""
timestamp, signatures = None, []
for item in (header or "").split(","):
key, _, value = item.strip().partition("=")
if key == "t" and value.isdigit() and len(value) <= 15:
timestamp = int(value)
elif key == "v1":
signatures.append(value)
if timestamp is None or abs(time.time() - timestamp) > TOLERANCE_SECONDS:
return False
signed = f"{timestamp}\n{method.upper()}\n{path}\n".encode() + body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected.encode(), s.encode()) for s in signatures)import crypto from "node:crypto";
const TOLERANCE_SECONDS = 300;
// path is the path plus ?query exactly as received; body is the raw Buffer.
export function verifyV2(header, secret, method, path, body) {
let timestamp;
const signatures = [];
for (const item of (header || "").split(",")) {
const [key, value = ""] = item.trim().split("=", 2);
if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
else if (key === "v1") signatures.push(value);
}
if (timestamp === undefined) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const expected = Buffer.from(
crypto
.createHmac("sha256", secret)
.update(`${timestamp}\n${method.toUpperCase()}\n${path}\n`)
.update(body)
.digest("hex"),
);
return signatures.some((sig) => {
const candidate = Buffer.from(sig);
return (
candidate.length === expected.length &&
crypto.timingSafeEqual(candidate, expected)
);
});
}فریم ورک وائرنگ
ہر مثال خام path اور query، اور خام body پڑھتی ہے، پھر SDK helper کو کال کرتی ہے۔
import json
from fastapi import FastAPI, HTTPException, Request
from thunderphone import verify_signature
app = FastAPI()
def raw_path_and_query(request: Request) -> str:
path = (request.scope.get("raw_path") or request.url.path.encode()).decode("latin-1")
query = request.scope.get("query_string", b"").decode("latin-1")
return f"{path}?{query}" if query else path
@app.post("/thunderphone-webhook")
async def hook(request: Request):
body = await request.body() # raw bytes, NOT request.json()
if not verify_signature(
request.headers.get("X-ThunderPhone-Signature-V2"),
secret=SECRET,
method=request.method,
path=raw_path_and_query(request),
body=body,
):
raise HTTPException(status_code=401)
event = json.loads(body)
# … dispatch on event["type"] …
return {"ok": True}import express from "express";
import { verifySignature } from "@thunderphone/sdk";
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 ok = verifySignature({
header: req.header("X-ThunderPhone-Signature-V2"),
secret: process.env.WEBHOOK_SECRET,
method: req.method,
path: req.originalUrl, // path and query as received
body: req.body,
});
if (!ok) 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 HttpResponseForbidden, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from thunderphone import verify_signature
@csrf_exempt
@require_POST
def hook(request):
if not verify_signature(
request.headers.get("X-ThunderPhone-Signature-V2"),
secret=SECRET,
method=request.method,
path=request.get_full_path(),
body=request.body, # raw bytes
):
return HttpResponseForbidden("invalid signature")
event = json.loads(request.body)
# … dispatch on event["type"] …
return JsonResponse({"ok": True})req.originalUrl (Express) اور ASGI کے raw_path اور query_string
(FastAPI) بالکل وہی target ہیں جو موصول ہوا تھا۔ Django کا
get_full_path() اور Flask کا request.path پہلے path کو decode کرتے ہیں، جس سے
عام paths کے لیے ایک ہی string ملتی ہے۔ اگر آپ کے endpoint path میں percent-encoded یا
reserved characters، جیسے %2F، ; یا =، شامل ہیں تو اس کے بجائے اپنے server کی فراہم کردہ
خام target پڑھیں (مثلاً Gunicorn پر request.META["RAW_URI"])۔
کون سا secret؟
دونوں ہیڈرز پر ایک ہی secret سے دستخط کیے جاتے ہیں۔
| ماخذ | Secret |
|---|---|
ویب ہک اینڈپوائنٹ (/v1/developer/webhook-endpoints) | ہر اینڈپوائنٹ کا secret (48 hex حروف) جو تخلیق کے وقت ایک بار واپس کیا جاتا ہے |
| پرانا سنگل-URL ویب ہک | ہر تنظیم کا secret جو GET /v1/webhook پر واپس کیا جاتا ہے |
ٹول اینڈپوائنٹ انوووکیشن (آپ کے endpoint.url پر براہِ راست کال) | تنظیمی سطح کا ویب ہک secret (وہی جو پرانے سنگل-URL ویب ہک کے لیے ہے) — ہر اینڈپوائنٹ کا secret نہیں |
secret کو اپنے secret manager یا env var میں محفوظ کریں — اسے کبھی commit نہ کریں۔
ٹول کالز کی تصدیق
جب ایجنٹ آپ کے کسی فنکشن ٹول کو براہ راست استعمال کرتا ہے (ٹول میں endpoint ہوتا ہے)، تو درخواست آپ کے ترتیب شدہ endpoint.headers کے ساتھ یہ ThunderPhone ہیڈرز بھی رکھتی ہے:
X-ThunderPhone-Call-ID— جاری کال کی عددی شناخت۔X-ThunderPhone-Signature-V2— اوپر دیا گیا ٹائم اسٹیمپ والا دستخط، جو آپ کی تنظیم کی سطح کی ویب ہک خفیہ کلید کے ذریعے بنایا جاتا ہے۔X-ThunderPhone-Signature— سابقہ صرف باڈی والا دستخط، اسی کلید کے ساتھ۔
ویب ہکس سے دو باتیں مختلف ہیں:
GET/DELETEٹولز کی کوئی باڈی نہیں ہوتی۔ آرگیومنٹس کوئری پیرامیٹرز کے طور پر منتقل ہوتے ہیں۔ V2 دستخط انہیں پاتھ اور کوئری کے ذریعے شامل کرتا ہے، اور باڈی فیلڈ خالی ہوتی ہے۔ خام کوئری اسٹرنگ کے ساتھ تصدیق کریں: پیرامیٹرز کو پارس کرکے نئی کوئری اسٹرنگ بنانے سے ان کی ترتیب یا انکوڈنگ بدل سکتی ہے۔ سابقہ ہیڈر خالی بائٹ اسٹرنگ پر دستخط کرتا ہے۔- جن تنظیموں میں سابقہ ویب ہک ترتیب شدہ نہیں ہوتا، ان کے پاس تنظیم کی خفیہ کلید نہیں ہوتی۔ اس صورت میں ٹول کالز میں صرف
X-ThunderPhone-Call-IDہوتا ہے اور کوئی دستخطی ہیڈرز نہیں ہوتے۔ دستخط کی خفیہ کلید حاصل کرنے کے لیے سابقہ ویب ہک (PUT /v1/webhook) ترتیب دیں، یاendpoint.headersکے ذریعے اپنے ہیڈر سے ٹول کالز کی توثیق کریں۔
@app.api_route("/tools/search-appointments", methods=["GET", "POST"])
async def tool(request: Request):
body = await request.body() # b"" for GET/DELETE tools
if not verify_signature(
request.headers.get("X-ThunderPhone-Signature-V2"),
secret=ORG_WEBHOOK_SECRET,
method=request.method,
path=raw_path_and_query(request),
body=body,
):
raise HTTPException(status_code=401)
call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
args = json.loads(body) if body else dict(request.query_params)
...Express میں، باڈی پارسر GET پر req.body کو خالی آبجیکٹ کے طور پر چھوڑ دیتا ہے، اس لیے Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0) منتقل کریں۔
ویب ہک-موڈ ٹول ڈسپیچ (endpoint کے بغیر ٹولز، جو آپ کے تنظیمی ویب ہک
کو telephony.tool / web.tool کے طور پر پہنچائے جاتے ہیں) ایک عام
دستخط شدہ ویب ہک ہے — اوپر دیا گیا معیاری طریقہ لاگو ہوتا ہے۔ درخواست کی
دونوں ساختوں کے لیے فنکشن ٹولز دیکھیں۔
پرانا دستخط: X-ThunderPhone-Signature
اصل ہیڈر صرف خام باڈی کا hex HMAC-SHA256 ہے، جس میں وہی secret استعمال ہوتا ہے۔ اس میں کوئی timestamp نہیں ہوتا، اس لیے جو کوئی ایک دستخط شدہ درخواست حاصل کر لے وہ اسے دوبارہ چلا سکتا ہے۔ انٹیگریشنز کے V2 پر منتقل ہونے کے دوران یہ اب بھی ہر درخواست کے ساتھ بھیجا جاتا ہے۔
- خام درخواست کی باڈی پڑھیں — وہی درست bytes جو ہم نے آپ کو بھیجے ہیں۔
hmac_sha256(secret, body).hexdigest()کا حساب لگائیں۔X-ThunderPhone-Signatureکے مقابلے میں constant time میں موازنہ کریں۔
ہم بالکل انہی bytes پر دستخط کرتے ہیں جو ہم منتقل کرتے ہیں۔ یہ bytes payload کی
canonical JSON serialization بھی ہوتی ہیں — keys حروفِ تہجی کے مطابق ترتیب میں، مختصر separators (بغیر خالی جگہ کے , اور :)، UTF-8۔
اگر آپ کا framework صرف parsed JSON فراہم کرتا ہے، تو اسے canonical انداز میں دوبارہ serialize کریں اور اس پر HMAC لگائیں:
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")خام باڈی کو ترجیح دیں — اس میں ایک مرحلہ کم ہوتا ہے اور بعض زبانوں میں JSON نمبروں کے round-tripping کے مسائل سے محفوظ رہتا ہے۔ یہی بات V2 کے body field پر بھی لاگو ہوتی ہے۔
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عام خامیاں
گھڑی کے وقت کا فرق
5 منٹ کی ونڈو آپ کے سرور کی گھڑی کے مطابق ناپی جاتی ہے۔ اسے NTP کے ساتھ ہم وقت رکھیں؛ 5 منٹ سے زیادہ غلط وقت والی گھڑی ہر درخواست کو مسترد کر دیتی ہے۔
ایک پراکسی راستہ دوبارہ لکھتی ہے
دستخط شدہ راستہ وہی ہے جس کی ہم نے درخواست کی تھی: اس URL کا راستہ جسے آپ نے ThunderPhone میں ترتیب دیا ہے۔ اگر ریورس پراکسی یا API گیٹ وے آپ کی ایپلیکیشن کے درخواست دیکھنے سے پہلے کوئی پری فکس ہٹا یا شامل کر دے، تو تصدیق سے پہلے ہمارے بھیجے ہوئے راستے کو دوبارہ بنائیں (مثلاً، پری فکس واپس شامل کریں)۔
بنیادی فارمیٹنگ کے ساتھ دوبارہ سیریلائز کرنا
باڈی کو پارس کر کے اپنی JSON لائبریری کی بنیادی ترتیبات کے ساتھ دوبارہ ڈمپ کرنے سے
( , / : کے بعد اسپیسز، شامل کرنے کی ترتیب والی کیز) مختلف بائٹس بنتے
ہیں اور HMAC خراب ہو جاتا ہے۔ خام باڈی کی تصدیق کریں — یا اگر آپ کو
دوبارہ سیریلائز کرنا ضروری ہو تو ہماری معیاری شکل سے بالکل مطابقت رکھیں: ترتیب شدہ
کیز، مختصر سیپریٹرز، UTF-8۔
فریم ورک JSON کو خودکار طور پر پارس کرتا ہے
Express کا express.json() middleware باڈی اسٹریم استعمال کر لیتا ہے
اور آپ خام بائٹس کھو دیتے ہیں۔ خاص طور پر webhook روٹ پر express.raw() استعمال کریں،
یا pre-middleware میں خام باڈی کو بفر کریں۔
NestJS / Koa کے لیے بھی یہی صورتحال ہے — ان کی "raw body" دستاویزات دیکھیں۔
ٹائمنگ کے لحاظ سے غیر محفوظ موازنہ
JS میں expected === signature یا Python میں expected == signature
ٹائمنگ کے لحاظ سے متغیر موازنے ہیں۔ بالترتیب crypto.timingSafeEqual
یا hmac.compare_digest استعمال کریں۔ کارکردگی میں فرق نہ ہونے کے برابر ہے۔
ٹول اینڈ پوائنٹس کے لیے غلط سیکرٹ
براہ راست ٹول-اینڈ پوائنٹ کالز پر تنظیمی سطح کا ویب ہک
سیکرٹ (GET /v1/webhook) استعمال کر کے دستخط کیے جاتے ہیں — نہ کہ
/v1/developer/webhook-endpoints کے کسی فی-اینڈ-پوائنٹ سیکرٹ سے۔ وہی ہیلپر دوبارہ استعمال کریں،
لیکن یقینی بنائیں کہ ٹول روٹس پر آپ اسے تنظیمی سیکرٹ فراہم کرتے ہیں۔
GET/DELETE ٹولز میں کوئری اسٹرنگز
V2 کوئری اسٹرنگ پر بالکل اسی طرح دستخط کرتا ہے جیسے اسے بھیجا گیا ہو۔ لیگیسی ہیڈر خالی باڈی پر دستخط کرتا ہے اور کبھی کوئری اسٹرنگ پر نہیں، اس لیے لیگیسی ہیڈر کے لیے URL کا ہیش کبھی مطابقت نہیں کرے گا۔
ونڈو کے اندر نقلیں
ٹائم اسٹیمپ 5 منٹ سے پرانے ری پلے روکتا ہے، اس ونڈو کے اندر موجود دوسری نقل کو نہیں۔
اینڈ پوائنٹ ڈیلیوریز کم از کم ایک بار ہوتی ہیں، اس لیے اسی ایونٹ کی دوبارہ کوشش بھی آ سکتی ہے؛
اینویلپ کے event_id کی بنیاد پر نقلیں ہٹا دیں۔
مطابقت نہ ہونے پر 401 واپس نہ کرنا
ناکام تصدیق پر 200 واپس کرنا ہینڈلر کو replay کا ہدف بناتا ہے۔ تصدیق ناکام ہونے پر ہمیشہ 2xx کے علاوہ جواب دیں۔