ওয়েবহুক স্বাক্ষর যাচাই করুন

আপনার সার্ভারে আমরা পাঠানো প্রতিটি অনুরোধ — webhook ডেলিভারি এবং tool-endpoint ইনভোকেশন — 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
Webhook endpoint (/v1/developer/webhook-endpoints)তৈরি করার সময় একবার ফেরত দেওয়া প্রতি-endpoint secret (48 hex অক্ষর)
লিগ্যাসি একক-URL webhookGET /v1/webhook-এ ফেরত দেওয়া প্রতি-org secret
Tool-endpoint ইনভোকেশন (আপনার endpoint.url-এ সরাসরি কল)org-স্তরের webhook secret (লিগ্যাসি একক-URL webhook-এর মতো একইটি) — প্রতি-endpoint secret নয়

secret আপনার secret manager বা env var-এ সংরক্ষণ করুন — কখনও কমিট করবেন না।

রেফারেন্স ইমপ্লিমেন্টেশন

চারটিই অপরিবর্তিত রিকোয়েস্ট বডি যাচাই করে:

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 টুলের কোনো বডি নেই। আর্গুমেন্টগুলো query parameter হিসেবে যায়, এবং signature খালি বাইট স্ট্রিং-এর ওপর গণনা করা হয় — তাই verify(b"", sig, secret) (Python) অথবা verify(Buffer.alloc(0), sig, secret) (Node) ব্যবহার করুন। query string হ্যাশ করবেন না
  2. লিগ্যাসি webhook কনফিগার না থাকা org-এর কোনো org secret থাকে না। সে ক্ষেত্রে টুল কলগুলোতে শুধু X-ThunderPhone-Call-ID থাকে এবং কোনো signature header থাকে না। signing secret পেতে লিগ্যাসি webhook (PUT /v1/webhook) কনফিগার করুন, অথবা endpoint.headers-এর মাধ্যমে নিজের header ব্যবহার করে টুল কল প্রমাণীকরণ করুন।
@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-মোডের টুল dispatch (যেসব টুলের endpoint নেই এবং আপনার org webhook-এ telephony.tool / web.tool হিসেবে সরবরাহ করা হয়) একটি সাধারণ signed webhook — উপরের মানক পদ্ধতিটি প্রযোজ্য। উভয় অনুরোধের ধরন সম্পর্কে জানতে ফাংশন টুল দেখুন।

সাধারণ সমস্যাগুলি

ডিফল্ট ফরম্যাটিং দিয়ে পুনরায় সিরিয়ালাইজ করা

বডি পার্স করে আপনার JSON লাইব্রেরির ডিফল্ট সেটিংস দিয়ে পুনরায় ডাম্প করলে (, / :-এর পরে স্পেস, ইনসার্শন-ক্রমযুক্ত কী) ভিন্ন বাইট তৈরি হয় এবং HMAC নষ্ট হয়। র' বডি যাচাই করুন — অথবা পুনরায় সিরিয়ালাইজ করতেই হলে, আমাদের ক্যানোনিকাল ফর্মটি হুবহু মেলান: সাজানো কী, কমপ্যাক্ট সেপারেটর, UTF-8।

ফ্রেমওয়ার্ক স্বয়ংক্রিয়ভাবে JSON পার্স করে

Express-এর express.json() মিডলওয়্যার বডি স্ট্রিম ব্যবহার করে ফেলে এবং আপনি র' বাইট হারান। নির্দিষ্টভাবে ওয়েবহুক রুটে express.raw() ব্যবহার করুন, অথবা প্রি-মিডলওয়্যারে র' বডি বাফার করুন। NestJS / Koa-এর ক্ষেত্রেও একই বিষয় — তাদের "র' বডি" ডকুমেন্টেশন দেখুন।

টাইমিং-অনিরাপদ তুলনা

JS-এ expected === signature বা Python-এ expected == signature টাইমিং-পরিবর্তনশীল তুলনা। যথাক্রমে crypto.timingSafeEqual বা hmac.compare_digest ব্যবহার করুন। কর্মদক্ষতার পার্থক্য নগণ্য।

টুল এন্ডপয়েন্টের জন্য ভুল সিক্রেট

সরাসরি টুল-এন্ডপয়েন্ট কল সংস্থা-স্তরের ওয়েবহুক সিক্রেট (GET /v1/webhook) দিয়ে সাইন করা হয় — /v1/developer/webhook-endpoints-এর কোনো এন্ডপয়েন্ট-ভিত্তিক সিক্রেট দিয়ে নয়। একই verify() ফাংশন পুনর্ব্যবহার করুন, তবে নিশ্চিত করুন যে টুল রুটে আপনি সংস্থার সিক্রেট দিচ্ছেন।

GET/DELETE টুলে কোয়েরি স্ট্রিং হ্যাশ করা

বডিবিহীন টুল মেথডের ক্ষেত্রে সিগনেচারটি খালি বাইট স্ট্রিংকে কভার করে, যাতে একটি সার্বজনীন পদ্ধতি বজায় থাকে: র' রিকোয়েস্ট বডি যা-ই হোক, সেটির HMAC করুন। URL বা কোয়েরি স্ট্রিং হ্যাশ করলে কখনও মিলবে না।

অমিল হলে 401 ফেরত না দেওয়া

যাচাইকরণ ব্যর্থ হলে 200 ফেরত দিলে হ্যান্ডলারটি রিপ্লে আক্রমণের লক্ষ্য হয়ে ওঠে। যাচাইকরণ ব্যর্থ হলে সবসময় নন-2xx উত্তর দিন।


পরবর্তী ধাপ