Signature verification
PanDoktorek podpisuje każdy webhook sekretem subskrypcji (HMAC-SHA256).
Twój system MUSI zweryfikować podpis i timestamp przed przetworzeniem —
inaczej ktoś może wysłać fałszywy booking.cancelled albo powtórzyć stary.
Nagłówki
| Nagłówek | Przykład | Znaczenie |
|---|---|---|
X-Pandoktorek-Event | booking.created | typ zdarzenia |
X-Pandoktorek-Event-Id | 9f0c… | identyfikator zdarzenia — deduplikuj po nim |
X-Pandoktorek-Timestamp | 1762336800 | czas podpisu (Unix, sekundy) |
X-Pandoktorek-Signature | v1=5d41… | podpis |
Algorytm (v1)
signed_payload = timestamp + "." + raw_body
signature = "v1=" + hex(HMAC_SHA256(secret, signed_payload))
- Odrzuć żądanie, gdy
|now - timestamp| > 300sekund (ochrona przed replay). - Policz podpis z surowych bajtów body — nie z
json.loads+json.dumps(zmiana whitespace = inny podpis). - Porównaj w czasie stałym (
hmac.compare_digest,crypto.timingSafeEqual). - Zdeduplikuj po
X-Pandoktorek-Event-Id(dostarczenie at-least-once).
Python (FastAPI)
import hashlib, hmac, os, time
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
SECRET = os.environ["PANDOKTOREK_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/pandoktorek")
async def pandoktorek_webhook(request: Request) -> dict[str, bool]:
body = await request.body()
timestamp = request.headers.get("X-Pandoktorek-Timestamp", "")
signature = request.headers.get("X-Pandoktorek-Signature", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
raise HTTPException(status_code=400, detail="stale timestamp")
expected = "v1=" + hmac.new(
SECRET, f"{timestamp}.".encode() + body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
raise HTTPException(status_code=401, detail="invalid signature")
# ... deduplikacja po X-Pandoktorek-Event-Id, potem przetwarzanie
return {"ok": True}
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.PANDOKTOREK_WEBHOOK_SECRET;
app.post("/webhooks/pandoktorek", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("X-Pandoktorek-Timestamp") ?? "";
const signature = req.get("X-Pandoktorek-Signature") ?? "";
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(400).send("stale timestamp");
}
const expected =
"v1=" +
crypto
.createHmac("sha256", SECRET)
.update(Buffer.concat([Buffer.from(`${timestamp}.`), req.body]))
.digest("hex");
const ok =
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!ok) return res.status(401).send("invalid signature");
// ... deduplikacja po X-Pandoktorek-Event-Id, potem przetwarzanie
res.json({ ok: true });
});
Częste błędy
- Parsowanie JSON przed weryfikacją (framework zmienia bajty body).
- Pominięcie timestampu w podpisie — stary format
sha256=…nie jest już używany. - Zwykłe
==zamiast porównania w czasie stałym. - Brak deduplikacji po
Event-Id— ten sam event może przyjść więcej niż raz.