वेबहुक सिग्नेचर सत्यापित करें
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 | प्रति-संगठन secret, जो GET /v1/webhook पर रिटर्न होता है |
टूल-एंडपॉइंट इनवोकेशन (आपके 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— लाइव कॉल की न्यूमेरिक आईडी।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() मिडलवेयर बॉडी स्ट्रीम को
कंज़्यूम कर लेता है और आप रॉ बाइट्स खो देते हैं। खास तौर पर वेबहुक
रूट पर 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 रिस्पॉन्स दें।