Verifikasi tanda tangan webhook
Setiap permintaan webhook dan alat yang dikirim ThunderPhone ditandatangani. Verifikasi tanda tangan sekali menggunakan panduan di sini, lalu gunakan kembali pemeriksaan yang sama pada setiap endpoint yang Anda jalankan.
Setiap permintaan yang kami kirim ke server Anda — pengiriman webhook dan
pemanggilan endpoint alat — membawa tanda tangan HMAC-SHA256 di header
X-ThunderPhone-Signature. Lakukan verifikasi dengan benar sekali, lalu
gunakan helper yang sama di setiap handler.
Algoritme
- Baca body permintaan mentah — byte persis yang kami POST kepada Anda.
- Hitung
hmac_sha256(secret, body).hexdigest(). - Bandingkan dalam waktu konstan dengan
X-ThunderPhone-Signature. (Perbandingan string naif membocorkan informasi waktu.)
Kami menandatangani byte persis yang kami kirimkan, sehingga memverifikasi body
mentah selalu berfungsi. Byte tersebut juga merupakan serialisasi JSON kanonis
dari payload — kunci diurutkan secara alfabetis, pemisah ringkas
(, dan : tanpa spasi), UTF-8. Ini memberi Anda resep kedua yang sepenuhnya
setara ketika framework Anda hanya menyediakan JSON yang sudah diurai:
serialisasikan ulang secara kanonis dan lakukan HMAC terhadapnya.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")Utamakan body mentah — langkahnya lebih sedikit dan kebal terhadap keunikan round-trip angka JSON dalam beberapa bahasa.
Rahasia yang mana?
| Sumber | Rahasia |
|---|---|
Endpoint webhook (/v1/developer/webhook-endpoints) | secret per endpoint (48 karakter heksadesimal) yang dikembalikan sekali saat pembuatan |
| Webhook URL tunggal lama | secret per organisasi yang dikembalikan pada GET /v1/webhook |
Pemanggilan endpoint alat (panggilan langsung ke endpoint.url Anda) | Rahasia webhook tingkat organisasi (sama dengan webhook URL tunggal lama) — bukan rahasia per endpoint |
Simpan rahasia di pengelola rahasia atau variabel environment Anda — jangan pernah melakukan commit.
Implementasi referensi
Keempatnya memverifikasi body permintaan mentah:
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)
endIntegrasi khusus framework
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})Memverifikasi panggilan alat
Saat agen memanggil salah satu
alat fungsi Anda secara langsung (alat tersebut memiliki
endpoint), permintaan membawa dua header ThunderPhone bersama
endpoint.headers yang Anda konfigurasi:
X-ThunderPhone-Call-ID— ID numerik panggilan aktif.X-ThunderPhone-Signature— HMAC-SHA256, menggunakan rahasia webhook tingkat organisasi Anda sebagai kunci, atas byte isi permintaan yang persis sama.
Helper verify() yang sama dapat digunakan tanpa perubahan, dengan dua hal berikut:
- Alat
GET/DELETEtidak memiliki isi. Argumen dikirim sebagai parameter kueri, dan tanda tangan dihitung atas string byte kosong — jadiverify(b"", sig, secret)(Python) atauverify(Buffer.alloc(0), sig, secret)(Node). Jangan hash string kueri. - Organisasi tanpa webhook lama yang dikonfigurasi tidak memiliki rahasia organisasi.
Dalam kasus tersebut, panggilan alat hanya membawa
X-ThunderPhone-Call-IDdan tidak ada header tanda tangan. Konfigurasikan webhook lama (PUT /v1/webhook) untuk mendapatkan rahasia penandatanganan, atau autentikasi panggilan alat dengan header Anda sendiri melaluiendpoint.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)
...Pengiriman alat dalam mode webhook (alat tanpa endpoint, dikirimkan
ke webhook organisasi Anda sebagai telephony.tool / web.tool) adalah webhook
bertanda tangan biasa — resep standar di atas berlaku. Lihat
Alat Fungsi untuk kedua bentuk permintaan.
Kesalahan umum
Serialisasi ulang dengan pemformatan default
Mengurai body lalu membuangnya kembali dengan pengaturan default
library JSON Anda (spasi setelah , / :, kunci berdasarkan urutan penyisipan) menghasilkan
byte yang berbeda dan merusak HMAC. Verifikasi body mentah — atau jika
Anda harus melakukan serialisasi ulang, samakan persis dengan bentuk kanonis kami:
kunci diurutkan, pemisah ringkas, UTF-8.
Framework mengurai JSON secara otomatis
Middleware express.json() Express mengonsumsi stream body
sehingga Anda kehilangan byte mentah. Gunakan express.raw() khusus pada rute webhook,
atau buffer body mentah dalam middleware sebelumnya.
Hal yang sama berlaku untuk NestJS / Koa — periksa dokumentasi "raw body" mereka.
Perbandingan yang tidak aman terhadap waktu
expected === signature di JS atau expected == signature di
Python adalah perbandingan dengan waktu yang bervariasi. Gunakan crypto.timingSafeEqual
atau hmac.compare_digest secara berurutan. Perbedaan performanya
tidak ada.
Rahasia yang salah untuk endpoint tool
Panggilan langsung ke endpoint tool ditandatangani dengan rahasia webhook tingkat
organisasi (GET /v1/webhook) — bukan dengan rahasia per-endpoint
dari /v1/developer/webhook-endpoints. Gunakan kembali fungsi verify()
yang sama, tetapi pastikan Anda memberinya rahasia organisasi pada rute tool.
Meng-hash string kueri pada tool GET/DELETE
Untuk metode tool tanpa body, tanda tangan mencakup string byte kosong, sehingga hanya diperlukan satu resep universal: HMAC body permintaan mentah, apa pun isinya. Meng-hash URL atau string kueri tidak akan pernah cocok.
Tidak mengembalikan 401 saat tidak cocok
Mengembalikan 200 saat verifikasi gagal menjadikan handler sebagai target replay. Selalu respons dengan non-2xx jika verifikasi gagal.