Open in
ওয়েবহুক স্বাক্ষর যাচাই করুন
ThunderPhone পাঠানো প্রতিটি ওয়েবহুক ও টুল অনুরোধ স্বাক্ষরিত থাকে। এখানকার পদ্ধতি দিয়ে একবার স্বাক্ষর যাচাই করুন, তারপর আপনার চালানো প্রতিটি এন্ডপয়েন্টে একই যাচাই পুনরায় ব্যবহার করুন।
আপনার সার্ভারে আমরা পাঠানো প্রতিটি অনুরোধ — webhook ডেলিভারি এবং
টুল-এন্ডপয়েন্ট ইনভোকেশন — 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 সংখ্যার রাউন্ড-ট্রিপিংজনিত অসঙ্গতি থেকে সুরক্ষিত।
কোন সিক্রেট?
| উৎস | সিক্রেট |
|---|---|
Webhook এন্ডপয়েন্ট (/v1/developer/webhook-endpoints) | তৈরির সময় একবার ফেরত দেওয়া প্রতি-এন্ডপয়েন্ট secret (48 হেক্স অক্ষর) |
| লিগ্যাসি একক-URL webhook | GET /v1/webhook-এ ফেরত দেওয়া প্রতি-অর্গ secret |
টুল-এন্ডপয়েন্ট ইনভোকেশন (আপনার endpoint.url-এ সরাসরি কল) | অর্গ-স্তরের webhook সিক্রেট (লিগ্যাসি একক-URL webhook-এর একইটি) — প্রতি-এন্ডপয়েন্ট সিক্রেট নয় |
সিক্রেটটি আপনার সিক্রেট ম্যানেজার বা 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 হেডার বহন করে:
X-ThunderPhone-Call-ID— চলমান কলের সংখ্যাসূচক id।X-ThunderPhone-Signature— সঠিক অনুরোধ-বডির বাইটের ওপর আপনার সংস্থা-স্তরের webhook secret দিয়ে কী করা HMAC-SHA256।
একই verify() হেল্পার অপরিবর্তিতভাবে কাজ করে, তবে দুটি বিষয় মনে রাখুন:
GET/DELETEটুলের কোনো বডি থাকে না। আর্গুমেন্টগুলো কোয়েরি প্যারামিটার হিসেবে যায়, এবং স্বাক্ষরটি খালি বাইট স্ট্রিং-এর ওপর গণনা করা হয় — তাইverify(b"", sig, secret)(Python) অথবাverify(Buffer.alloc(0), sig, secret)(Node) ব্যবহার করুন। কোয়েরি স্ট্রিং হ্যাশ করবেন না।- লিগ্যাসি webhook কনফিগার করা নেই এমন সংস্থার কোনো সংস্থা secret নেই।
সে ক্ষেত্রে টুল কলগুলো শুধু
X-ThunderPhone-Call-IDবহন করে এবং কোনো স্বাক্ষর হেডার থাকে না। সাইনিং secret পেতে লিগ্যাসি webhook (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)
...Webhook-মোডের টুল ডিসপ্যাচে (endpoint ছাড়া টুল, যা আপনার সংস্থার
webhook-এ telephony.tool / web.tool হিসেবে পাঠানো হয়) এটি একটি সাধারণ
স্বাক্ষরিত 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 রেসপন্স দিন।