Το ThunderPhone 2.0 είναι εδώ.Ξεκινήστε μόνοι σας, από 2¢/λεπτό.Διαβάστε την ανακοίνωση

Operations

Επαληθεύστε τις υπογραφές webhook

Κάθε αίτημα webhook και εργαλείου που στέλνει το ThunderPhone είναι υπογεγραμμένο. Επαληθεύστε την υπογραφή μία φορά με την παρακάτω διαδικασία και, στη συνέχεια, χρησιμοποιήστε τον ίδιο έλεγχο σε κάθε τελικό σημείο που εκτελείτε.

Κάθε αίτημα που στέλνουμε στον διακομιστή σας — παραδόσεις webhook και επικλήσεις τελικών σημείων εργαλείων — περιλαμβάνει μια υπογραφή HMAC-SHA256 στην κεφαλίδα X-ThunderPhone-Signature. Ρυθμίστε σωστά την επαλήθευση μία φορά και χρησιμοποιήστε τον ίδιο βοηθό σε κάθε χειριστή.

Ο αλγόριθμος

  1. Διαβάστε το ακατέργαστο σώμα του αιτήματος — τα ακριβή byte που σας στείλαμε με POST.
  2. Υπολογίστε hmac_sha256(secret, body).hexdigest().
  3. Συγκρίνετε σε σταθερό χρόνο με το X-ThunderPhone-Signature. (Η αφελής σύγκριση συμβολοσειρών διαρρέει πληροφορίες χρονισμού.)

Υπογράφουμε ακριβώς τα byte που μεταδίδουμε, επομένως η επαλήθευση του ακατέργαστου σώματος λειτουργεί πάντα. Αυτά τα byte είναι επίσης η κανονική σειριοποίηση JSON του ωφέλιμου φορτίου — κλειδιά ταξινομημένα αλφαβητικά, συμπαγείς διαχωριστές (, και : χωρίς κενά), UTF-8. Αυτό σας παρέχει μια δεύτερη, πλήρως ισοδύναμη μέθοδο όταν το framework σας εκθέτει μόνο αναλυμένο 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 δεκαεξαδικοί χαρακτήρες), που επιστρέφεται μία φορά κατά τη δημιουργία
Παλαιού τύπου webhook με ένα URLsecret ανά οργανισμό που επιστρέφεται στο GET /v1/webhook
Επίκληση τελικού σημείου εργαλείου (άμεση κλήση στο endpoint.url σας)Το μυστικό webhook σε επίπεδο οργανισμού (το ίδιο με το παλαιού τύπου webhook με ένα URL) — όχι μυστικό ανά τελικό σημείο

Αποθηκεύστε το μυστικό στον διαχειριστή μυστικών ή σε μεταβλητή περιβάλλοντος — μην το υποβάλετε ποτέ σε commit.

Υλοποιήσεις αναφοράς

Και οι τέσσερις επαληθεύουν το ακατέργαστο σώμα του αιτήματος:

Python
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 "")
Node.js
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),
  );
}
Go
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))
}
Ruby
require "openssl"
 
def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end

Σύνδεση ανά framework

FastAPI
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}
Express
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);
  },
);
Django
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), το αίτημα περιλαμβάνει δύο κεφαλίδες ThunderPhone μαζί με τις ρυθμισμένες endpoint.headers σας:

  • X-ThunderPhone-Call-ID — το αριθμητικό αναγνωριστικό της ενεργής κλήσης.
  • X-ThunderPhone-Signature — HMAC-SHA256, με κλειδί το μυστικό webhook σε επίπεδο οργανισμού, πάνω στα ακριβή byte του σώματος του αιτήματος.

Ο ίδιος βοηθός verify() λειτουργεί χωρίς αλλαγές, με δύο ιδιαιτερότητες:

  1. Τα εργαλεία GET / DELETE δεν έχουν σώμα. Τα ορίσματα μεταφέρονται ως παράμετροι ερωτήματος και η υπογραφή υπολογίζεται πάνω στην κενή συμβολοσειρά byte — επομένως verify(b"", sig, secret) (Python) ή verify(Buffer.alloc(0), sig, secret) (Node). Μην κάνετε hash τη συμβολοσειρά ερωτήματος.
  2. Οι οργανισμοί χωρίς ρυθμισμένο παλαιού τύπου webhook δεν έχουν μυστικό οργανισμού. Σε αυτήν την περίπτωση, οι κλήσεις εργαλείων περιλαμβάνουν μόνο X-ThunderPhone-Call-ID και καμία κεφαλίδα υπογραφής. Ρυθμίστε το παλαιού τύπου 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 σας (κενά μετά τα , / :, κλειδιά με σειρά εισαγωγής) παράγει διαφορετικά byte και καταστρέφει το HMAC. Επαληθεύστε το ακατέργαστο σώμα — ή, αν πρέπει να το επανασειριοποιήσετε, αντιστοιχίστε ακριβώς την κανονική μας μορφή: ταξινομημένα κλειδιά, συμπαγείς διαχωριστές, UTF-8.

Το framework αναλύει αυτόματα το JSON

Το middleware express.json() του Express καταναλώνει τη ροή του σώματος και χάνετε τα ακατέργαστα byte. Χρησιμοποιήστε express.raw() ειδικά στη διαδρομή webhook ή αποθηκεύστε προσωρινά το ακατέργαστο σώμα σε ένα προ-middleware. Το ίδιο ισχύει για NestJS / Koa — ελέγξτε την τεκμηρίωσή τους για το «ακατέργαστο σώμα».

Σύγκριση μη ασφαλής ως προς τον χρόνο

Τα expected === signature σε JS ή expected == signature σε Python είναι συγκρίσεις μεταβλητού χρόνου. Χρησιμοποιήστε crypto.timingSafeEqual ή hmac.compare_digest αντίστοιχα. Η διαφορά στην απόδοση είναι μηδενική.

Λανθασμένο μυστικό για endpoints εργαλείων

Οι άμεσες κλήσεις σε endpoint εργαλείων υπογράφονται με το μυστικό webhook σε επίπεδο οργανισμού (GET /v1/webhook) — όχι με κάποιο μυστικό ανά endpoint από το /v1/developer/webhook-endpoints. Επαναχρησιμοποιήστε την ίδια συνάρτηση verify(), αλλά βεβαιωθείτε ότι τροφοδοτείτε το μυστικό του οργανισμού στις διαδρομές εργαλείων.

Κατακερματισμός του query string σε εργαλεία GET/DELETE

Για μεθόδους εργαλείων χωρίς σώμα, η υπογραφή καλύπτει την κενή ακολουθία byte, διατηρώντας μία καθολική συνταγή: εφαρμόστε HMAC στο ακατέργαστο σώμα του αιτήματος, όποιο κι αν είναι. Ο κατακερματισμός του URL ή του query string δεν θα ταιριάξει ποτέ.

Μη επιστροφή 401 σε ασυμφωνία

Η επιστροφή 200 όταν η επαλήθευση αποτυγχάνει καθιστά τον χειριστή στόχο επανάληψης. Να απαντάτε πάντα με κωδικό εκτός 2xx αν η επαλήθευση αποτύχει.


Επόμενα βήματα