ویب ہک دستخطوں کی تصدیق کریں

ہم آپ کے سرور کو بھیجی جانے والی ہر درخواست — ویب ہک ڈیلیوریز اور ٹول-اینڈپوائنٹ انوووکیشنز — میں X-ThunderPhone-Signature ہیڈر کے اندر HMAC-SHA256 دستخط شامل ہوتا ہے۔ تصدیق ایک بار درست کریں اور اسی ہیلپر کو ہر ہینڈلر میں استعمال کریں۔

الگورتھم

  1. درخواست کا اصل باڈی پڑھیں — بالکل وہی بائٹس جو ہم نے آپ کو POST کیے تھے۔
  2. hmac_sha256(secret, body).hexdigest() کا حساب کریں۔
  3. مستقل وقت میں 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 کے دو ہیڈرز لے کر آتی ہے:

وہی verify() ہیلپر بغیر کسی تبدیلی کے کام کرتا ہے، البتہ دو نکات ہیں:

  1. GET / DELETE ٹولز کی کوئی باڈی نہیں ہوتی۔ آرگیومنٹس کوئری پیرامیٹرز کے طور پر جاتے ہیں، اور دستخط خالی بائٹ اسٹرنگ پر شمار کیا جاتا ہے — یعنی verify(b"", sig, secret) (Python) یا verify(Buffer.alloc(0), sig, secret) (Node)۔ کوئری اسٹرنگ کو ہیش نہ کریں۔
  2. ایسے آرگز جن میں لیگیسی ویب ہک ترتیب شدہ نہ ہو، ان کا کوئی آرگ سیکرٹ نہیں ہوتا۔ اس صورت میں ٹول کالز میں صرف 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 جواب دیں۔


اگلے مراحل