---
title: "Webhook-aláírások ellenőrzése"
description: "A ThunderPhone által küldött minden webhook- és eszközkérés időbélyeggel van aláírva. Ellenőrizze az itt található recepttel vagy SDK-segédprogrammal, és utasítsa el az öt percnél régebbi, ismételten elküldött kéréseket."
---

Minden, a szerverére küldött kérésünk (webhook-kézbesítések, a régi
egyetlen URL-es webhook, valamint eszközvégpont-hívások) két aláírásfejlécet tartalmaz. Mindkettő ugyanazzal a titkos kulccsal készült HMAC-SHA256.

| Fejléc | Mit ír alá | Újrajátszás elleni védelem |
|--------|-------|-------------------|
| `X-ThunderPhone-Signature-V2` (ajánlott) | Időbélyeg, metódus, útvonal, lekérdezés és törzs | Utasítsa el az 5 percnél régebbi kéréseket |
| `X-ThunderPhone-Signature` | Csak a törzs | Nincs: egy elfogott kérés örökké érvényes marad |

Az új integrációkban ellenőrizze az `X-ThunderPhone-Signature-V2` fejlécet, a meglévőket pedig állítsa át erre. Az `X-ThunderPhone-Signature` fejlécet továbbra is változatlanul elküldjük minden kérésben, így az erre épülő ellenőrzők továbbra is működnek. Mielőtt megszüntetnénk a küldését, bejelentjük a dátumot.

## Aláírás V2

```http
X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e
```

- A `t` az Unix-idő másodpercben kifejezve, amikor elküldtük a kérést. Az
  újrapróbált kézbesítést elküldésekor ismét aláírjuk, saját `t` értékkel.
- A `v1` az alábbi aláírt karakterlánc hexadecimális HMAC-SHA256 értéke,
  amely az Ön titkával van kulcsolva.

Az érték `key=value` párok vesszővel elválasztott listája. Hagyja figyelmen
kívül az ismeretlen kulcsokat, és fogadja el a kérést, ha bármelyik `v1`
egyezik: egy jövőbeli módosítás során hozzáadhatunk egy második aláírást.

### Az aláírt karakterlánc

Négy mező, amelyek mindegyikét egy újsor (`\n`, `0x0A` bájt) követi, az
utolsó kivételével:

```text
<t>\n<METHOD>\n<path and query>\n<body>
```

| Mező | Érték |
|-------|-------|
| `t` | A fejléc `t` értéke |
| `METHOD` | A HTTP-metódus nagybetűkkel: `POST`, `GET`, `PUT`, `PATCH` vagy `DELETE` |
| path and query | A HTTP-kérés sorában szereplő kérési cél: az elérési út, majd `?` és a lekérdezési karakterlánc, ha van ilyen. Nem tartalmaz sémát, gazdagépet vagy töredéket. Pontosan a beérkezett formában használja, dekódolás és a paraméterek átrendezése nélkül |
| `body` | A nyers kéréstörzs. A `GET` és `DELETE` eszközhívásoknak nincs törzsük, ezért az aláírt karakterláncuk az elérési út utáni újsorral végződik |

Ellenőrzéshez:

1. Bármilyen elemzés előtt olvassa be a fejlécet, a metódust, a nyers
   elérési utat és lekérdezést, valamint a nyers törzset.
2. Vegye ki a fejlécből a `t` értéket és az összes `v1` értéket.
3. Utasítsa el a kérést, ha a `t` több mint **300 másodperccel** eltér az
   Ön órájától előre vagy hátra.
4. Számítsa ki az aláírt karakterlánc hexadecimális HMAC-SHA256 értékét az Ön titkával.
5. Hasonlítsa össze konstans időben a `v1` értékkel. Ha egyik sem egyezik, válaszoljon `401`-gyel.

### Kidolgozott példa

Titok: `whsec_example_4c1f2a9b`. Ezt a kérést a következő időpontban küldjük:
`t = 1767225600` (2026-01-01 00:00:00 UTC):

```http
POST /tools/lookup?region=us HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e

{"customer_id":"cust_123"}
```

Az aláírt karakterlánc ez a négy sor, a törzs után újsor nélkül:

```text
1767225600
POST
/tools/lookup?region=us
{"customer_id":"cust_123"}
```

A HMAC-SHA256 értéke:
`03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e`.
Ellenőrizze ezzel a megvalósítását, vagy állítsa elő `openssl` használatával:

```bash
printf '1767225600\nPOST\n/tools/lookup?region=us\n{"customer_id":"cust_123"}' \
  | openssl dgst -sha256 -hmac 'whsec_example_4c1f2a9b'
```

Egy ugyanebben a másodpercben indított `GET` eszközhívás a
`/tools/lookup?customer_id=cust_123` címre üres törzzsel rendelkezik. Az
aláírt karakterlánca
`1767225600\nGET\n/tools/lookup?customer_id=cust_123\n`, az aláírása pedig
`3ce214cb0b8f2bf39b1d19d4e7265df9dbaff5a5f838d3cc6c753a8f7a4ad280`.

### Az SDK-kkal

A [Python- és TypeScript SDK-k](/api-reference/sdks) tartalmaznak egy segédfüggvényt
(`thunderphone` 0.2.0 vagy újabb, `@thunderphone/sdk` 0.2.0 vagy újabb). Ez
ellenőrzi az 5 perces időablakot, konstans időben hasonlít össze, és titkok
listáját is elfogadja, így leállás nélkül forgathatja őket.

<CodeGroup>
```python Python
from thunderphone import verify_signature

ok = verify_signature(
    header,            # the X-ThunderPhone-Signature-V2 value
    secret=SECRET,     # or [NEW_SECRET, OLD_SECRET] while rotating
    method=method,     # "POST"
    path=path,         # "/tools/lookup?region=us", exactly as received
    body=raw_body,     # bytes, before any JSON parsing
    tolerance=300,     # optional; seconds
)
```

```javascript Node.js
import { verifySignature } from "@thunderphone/sdk";

const ok = verifySignature({
  header,               // the X-ThunderPhone-Signature-V2 value
  secret: SECRET,       // or [NEW_SECRET, OLD_SECRET] while rotating
  method,               // "POST"
  path,                 // "/tools/lookup?region=us", exactly as received
  body: rawBody,        // Buffer or string, before any JSON parsing
  toleranceSeconds: 300, // optional
});
```
</CodeGroup>

### SDK nélkül

<CodeGroup>
```python Python
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300


def verify_v2(header: str, secret: str, method: str, path: str, body: bytes) -> bool:
    """path is the path plus ?query exactly as received; body is the raw body."""
    timestamp, signatures = None, []
    for item in (header or "").split(","):
        key, _, value = item.strip().partition("=")
        if key == "t" and value.isdigit() and len(value) <= 15:
            timestamp = int(value)
        elif key == "v1":
            signatures.append(value)
    if timestamp is None or abs(time.time() - timestamp) > TOLERANCE_SECONDS:
        return False
    signed = f"{timestamp}\n{method.upper()}\n{path}\n".encode() + body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected.encode(), s.encode()) for s in signatures)
```

```javascript Node.js
import crypto from "node:crypto";

const TOLERANCE_SECONDS = 300;

// path is the path plus ?query exactly as received; body is the raw Buffer.
export function verifyV2(header, secret, method, path, body) {
  let timestamp;
  const signatures = [];
  for (const item of (header || "").split(",")) {
    const [key, value = ""] = item.trim().split("=", 2);
    if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
    else if (key === "v1") signatures.push(value);
  }
  if (timestamp === undefined) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
  const expected = Buffer.from(
    crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}\n${method.toUpperCase()}\n${path}\n`)
      .update(body)
      .digest("hex"),
  );
  return signatures.some((sig) => {
    const candidate = Buffer.from(sig);
    return (
      candidate.length === expected.length &&
      crypto.timingSafeEqual(candidate, expected)
    );
  });
}
```
</CodeGroup>

### Keretrendszer-integráció

Mindegyik példa beolvassa a nyers útvonalat, lekérdezési karakterláncot és törzset, majd meghívja
az SDK segédfüggvényét.

<CodeGroup>
```python FastAPI
import json

from fastapi import FastAPI, HTTPException, Request
from thunderphone import verify_signature

app = FastAPI()


def raw_path_and_query(request: Request) -> str:
    path = (request.scope.get("raw_path") or request.url.path.encode()).decode("latin-1")
    query = request.scope.get("query_string", b"").decode("latin-1")
    return f"{path}?{query}" if query else path


@app.post("/thunderphone-webhook")
async def hook(request: Request):
    body = await request.body()  # raw bytes, NOT request.json()
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=SECRET,
        method=request.method,
        path=raw_path_and_query(request),
        body=body,
    ):
        raise HTTPException(status_code=401)
    event = json.loads(body)
    # … dispatch on event["type"] …
    return {"ok": True}
```

```javascript Express
import express from "express";
import { verifySignature } from "@thunderphone/sdk";

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 ok = verifySignature({
      header: req.header("X-ThunderPhone-Signature-V2"),
      secret: process.env.WEBHOOK_SECRET,
      method: req.method,
      path: req.originalUrl, // path and query as received
      body: req.body,
    });
    if (!ok) return res.sendStatus(401);
    const event = JSON.parse(req.body.toString("utf8"));
    // … dispatch on event.type …
    res.sendStatus(204);
  },
);
```

```python Django
import json

from django.http import HttpResponseForbidden, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from thunderphone import verify_signature


@csrf_exempt
@require_POST
def hook(request):
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=SECRET,
        method=request.method,
        path=request.get_full_path(),
        body=request.body,  # raw bytes
    ):
        return HttpResponseForbidden("invalid signature")
    event = json.loads(request.body)
    # … dispatch on event["type"] …
    return JsonResponse({"ok": True})
```
</CodeGroup>

Az `req.originalUrl` (Express), valamint az ASGI `raw_path` és `query_string`
(FastAPI) pontosan a beérkezett célértéket adják vissza. A Django
`get_full_path()` és a Flask `request.path` először dekódolja az útvonalat, ami
szokásos útvonalak esetén ugyanazt a karakterláncot eredményezi. Ha az Ön végpontjának útvonala
százalékkódolt vagy fenntartott karaktereket tartalmaz, például `%2F`, `;` vagy `=`, inkább
a szerver által biztosított nyers célértéket olvassa be (például a Gunicorn
esetén a `request.META["RAW_URI"]` értékét).

## Melyik titkos kulcs?

Ugyanaz a titkos kulcs írja alá mindkét fejlécet.

| Forrás | Titkos kulcs |
|--------|--------|
| [Webhook-végpont](/hu/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | A végpontonkénti `secret` (48 hexadecimális karakter), amelyet a rendszer egyszer, a létrehozáskor ad vissza |
| [Régi, egy URL-es webhook](/api-reference/organizations#legacy-single-url-webhook) | A szervezetenkénti `secret`, amelyet a `GET /v1/webhook` ad vissza |
| [Eszközvégpont-meghívás](/hu/tools/overview) (közvetlen hívás az Ön `endpoint.url` címére) | A **szervezeti szintű webhook-titkos kulcs** (ugyanaz, mint a régi, egy URL-es webhook esetén) — nem végpontonkénti titkos kulcs |

Tárolja a titkos kulcsot a titkoskulcs-kezelőjében vagy környezeti változóban — soha ne commitolja.

## Eszközhívások ellenőrzése

Amikor az ügynök közvetlenül meghívja az Ön egyik
[függvényeszközét](/hu/tools/overview) (az eszköz rendelkezik
`endpoint` értékkel), a kérés az Ön által konfigurált `endpoint.headers` mellett
a következő ThunderPhone-fejléceket tartalmazza:

- `X-ThunderPhone-Call-ID` — az aktív hívás numerikus azonosítója.
- `X-ThunderPhone-Signature-V2` — a fenti időbélyeggel ellátott aláírás,
  amely az Ön **szervezeti szintű webhooktitkával** van kulcsolva.
- `X-ThunderPhone-Signature` — a korábbi, csak a törzsre vonatkozó aláírás, ugyanazzal a kulccsal.

Két dolog eltér a webhookoktól:

1. A **`GET` / `DELETE` eszközöknek nincs törzsük.** Az argumentumok lekérdezési
   paraméterekként érkeznek. A V2-aláírás ezeket az útvonalon és a lekérdezésen
   keresztül fedi le, a törzsmező pedig üres. Ellenőrizze a nyers lekérdezési karakterlánccal:
   a paraméterek elemzése és új lekérdezési karakterlánc létrehozása megváltoztathatja
   azok sorrendjét vagy kódolását. A régi fejléc az üres bájtsorozatot írja alá.
2. A korábbi webhookot nem konfiguráló **szervezeteknek nincs szervezeti titkuk.** Ebben
   az esetben az eszközhívások csak `X-ThunderPhone-Call-ID` fejlécet hordoznak,
   aláírásfejléceket nem. Állítsa be a korábbi webhookot
   (`PUT /v1/webhook`) aláírási titok beszerzéséhez, vagy hitelesítse az
   eszközhívásokat saját fejlécével az `endpoint.headers` segítségével.

```python
@app.api_route("/tools/search-appointments", methods=["GET", "POST"])
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=ORG_WEBHOOK_SECRET,
        method=request.method,
        path=raw_path_and_query(request),
        body=body,
    ):
        raise HTTPException(status_code=401)
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    args = json.loads(body) if body else dict(request.query_params)
    ...
```

Expressben egy törzsparser a `GET` kérésnél üres objektumként hagyja a `req.body` értékét,
ezért adja át a következőt: `Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0)`.

A webhook-**módú** eszközdiszpécselés (az `endpoint` nélküli eszközök, amelyek
`telephony.tool` / `web.tool` formájában érkeznek az Ön szervezeti webhookjára) hagyományos
aláírt webhook — a fenti standard eljárás alkalmazandó. Mindkét kérésformátumról lásd:
[Függvényeszközök](/hu/tools/overview).

## Régi aláírás: `X-ThunderPhone-Signature`

Az eredeti fejléc a nyers törzs önmagában vett hexadecimális HMAC-SHA256-értéke,
ugyanazzal a titokkal. Nem tartalmaz időbélyeget, ezért bárki, aki elfog egy
aláírt kérést, újra elküldheti azt. A rendszer továbbra is minden kérésnél elküldi,
amíg az integrációk át nem állnak a V2-re.

1. Olvassa be a **nyers** kéréstörzset — az általunk küldött pontos bájtokat.
2. Számítsa ki a következőt: `hmac_sha256(secret, body).hexdigest()`.
3. Hasonlítsa össze **konstans időben** az `X-ThunderPhone-Signature` értékével.

Pontosan azokat a bájtokat írjuk alá, amelyeket továbbítunk. Ezek a bájtok egyben a
hasznos adat **kanonikus JSON-szerializálása** is — a kulcsok ábécésorrendben,
tömör elválasztókkal (`,` és `:` szóközök nélkül), UTF-8 kódolásban.
Ha a keretrendszere csak feldolgozott JSON-t biztosít, szerializálja újra kanonikusan,
és erre számítsa a HMAC-ot:

```python
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
```

A nyers törzset részesítse előnyben — ez eggyel kevesebb lépés, és védett a JSON-számok
oda-vissza alakításából eredő sajátosságokkal szemben egyes nyelvekben. Ugyanez vonatkozik
a V2 törzsmezőjére is.

<CodeGroup>
```python 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 "")
```

```javascript 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 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 Ruby
require "openssl"

def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end
```
</CodeGroup>

## Gyakori buktatók

<AccordionGroup>
  <Accordion title="Óraeltolódás">
    Az 5 perces időablakot a szerver órájához viszonyítjuk. Tartsa azt
    NTP-vel szinkronban; az 5 percnél nagyobb eltérést mutató óra minden
    kérést elutasít.
  </Accordion>

  <Accordion title="Egy proxy átírja az útvonalat">
    Az aláírt útvonal az általunk kért útvonal: a ThunderPhone-ban
    beállított URL útvonala. Ha egy fordított proxy vagy API-átjáró
    eltávolít vagy hozzáad egy előtagot, mielőtt az alkalmazása megkapná a kérést,
    az ellenőrzés előtt állítsa helyre az általunk küldött útvonalat
    (például tegye vissza az előtagot).
  </Accordion>

  <Accordion title="Újraszerializálás alapértelmezett formázással">
    A törzs elemzése és újbóli kiírása a JSON-könyvtár
    alapértelmezéseivel (szóközök a `,` / `:` után, beillesztési sorrendű kulcsok)
    eltérő bájtokat eredményez, és érvényteleníti a HMAC-et. Ellenőrizze a nyers
    törzset — vagy ha újra kell szerializálnia, pontosan egyezzen meg a kanonikus
    formánkkal: rendezett kulcsok, tömör elválasztók, UTF-8.
  </Accordion>

  <Accordion title="A keretrendszer automatikusan elemzi a JSON-t">
    Az Express `express.json()` middleware-e felhasználja a törzsfolyamot,
    így elvesznek a nyers bájtok. Kifejezetten a webhook
    útvonalon használja az `express.raw()`-t, vagy pufferelje a nyers törzset egy
    előzetes middleware-ben. Ugyanez vonatkozik a NestJS-re / Koára is — tekintse meg a „raw body” dokumentációjukat.
  </Accordion>

  <Accordion title="Időzítés szempontjából nem biztonságos összehasonlítás">
    A JS-ben használt `expected === signature`, illetve a
    Pythonban használt `expected == signature` időzítésfüggő összehasonlítások. Használja rendre a `crypto.timingSafeEqual`
    vagy a `hmac.compare_digest` függvényt. A teljesítménykülönbség
    elhanyagolható.
  </Accordion>

  <Accordion title="Helytelen titok az eszközvégpontokhoz">
    A közvetlen eszközvégpont-hívásokat a **szervezeti szintű webhook-titokkal**
    (`GET /v1/webhook`) írjuk alá — nem a
    `/v1/developer/webhook-endpoints` bármely végpontonkénti titkával. Használja újra
    ugyanazt a segédfüggvényt, de ügyeljen arra, hogy az eszközútvonalakon a szervezeti
    titkot adja át neki.
  </Accordion>

  <Accordion title="Lekérdezési karakterláncok a GET/DELETE eszközökön">
    A V2 pontosan az elküldött formában írja alá a lekérdezési karakterláncot. A régi fejléc
    az üres törzset írja alá, a lekérdezési karakterláncot soha, ezért az URL kivonatolása
    a régi fejléchez soha nem fog egyezni.
  </Accordion>

  <Accordion title="Duplikátumok az időablakon belül">
    Az időbélyeg az 5 percnél régebbi ismételt lejátszásokat akadályozza meg, nem pedig az
    ezen időablakon belüli második példányt. A végpontokra történő kézbesítés legalább egyszeri,
    ezért ugyanazon esemény újrapróbálkozása is megérkezhet; szüntesse meg a duplikációt
    a burkoló `event_id` mezője alapján.
  </Accordion>

  <Accordion title="Nem 401-es válasz eltérés esetén">
    Ha sikertelen ellenőrzéskor 200-as választ ad vissza, a kezelő ismétlési
    célponttá válik. Ha az ellenőrzés sikertelen, mindig 2xx-től eltérő választ adjon.
  </Accordion>
</AccordionGroup>

---

## Következő lépések

<CardGroup cols={2}>
  <Card title="Webhookok áttekintése" icon="bolt" href="/hu/webhooks/overview">
    Kézbesítési szemantika, újrapróbálkozások, forrás IP-címek.
  </Card>
  <Card title="Webhookvégpontok" icon="plug" href="/hu/webhooks/endpoints">
    Több URL kezelése, titkok rotálása.
  </Card>
  <Card title="Funkcióeszközök" icon="screwdriver-wrench" href="/hu/tools/overview">
    A két eszközhívási útvonal és a kéréseik formátuma.
  </Card>
  <Card title="Toolintegrációk" icon="wrench" href="/hu/guides/build-tool-integration">
    Készítsen teljes, toolokra épülő integrációt elejétől végéig.
  </Card>
</CardGroup>
