Podpis HMAC
Każdy webhook wysyłany przez DPay Web EID jest podpisany cyfrowo algorytmem HMAC-SHA256. Weryfikacja podpisu pozwala potwierdzić, że webhook rzeczywiście pochodzi od DPay i nie został zmodyfikowany w drodze.
Bezpieczeństwo
Zawsze weryfikuj podpis przed przetworzeniem webhooka. Bez weryfikacji każdy może wysłać fałszywy webhook na Twój endpoint i podszyć się pod DPay.
Algorytm
signature = HMAC-SHA256(secret, "{timestamp}.{raw_body}")
Gdzie:
secret- klucz HMAC (whsec_xxx) otrzymany przy rejestracji webhookatimestamp- wartość nagłówkaX-Timestamp(Unix timestamp)raw_body- dokładnie surowe ciało zapytania (przed JSON parse)
Kroki weryfikacji
- Pobierz
X-TimestampiX-Signaturez nagłówków HTTP - Odczytaj surowe ciało zapytania (raw body, nie sparsowany JSON)
- Wylicz:
HMAC-SHA256(secret, "{timestamp}.{raw_body}") - Porównaj wynik z wartością
X-Signaturepo usunięciu prefiksusha256= - Użyj funkcji odpornej na timing attacks (
hash_equals,hmac.compare_digest)
Przykład PHP
<?php
function verifyWebhookSignature(
string $payload,
string $signature,
string $timestamp,
string $secret
): bool {
$expected = hash_hmac('sha256', "{$timestamp}.{$payload}", $secret);
$actual = str_replace('sha256=', '', $signature);
return hash_equals($expected, $actual);
}
// Użycie w kontrolerze:
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$secret = getenv('DPAY_EID_WEBHOOK_SECRET'); // whsec_xxx
if (!verifyWebhookSignature($payload, $signature, $timestamp, $secret)) {
http_response_code(401);
exit('Invalid signature');
}
// Podpis OK - przetwarzaj zdarzenie
$event = json_decode($payload, true);
$eventType = $event['type'];
$sessionId = $event['data']['verification_session_id'];
switch ($eventType) {
case 'verification.completed':
$resultStatus = $event['data']['result_status'];
// Pobierz pełny wynik z /verifications/{id}/result
break;
case 'verification.failed':
// Obsługa odrzucenia
break;
}
http_response_code(200);
echo 'OK';
Przykład Python (Flask)
import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['DPAY_EID_WEBHOOK_SECRET']
def verify_signature(payload: bytes, signature: str, timestamp: str) -> bool:
"""Weryfikuje podpis HMAC-SHA256 webhooka."""
message = f"{timestamp}.{payload.decode('utf-8')}"
expected = hmac.new(
WEBHOOK_SECRET.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
actual = signature.replace("sha256=", "")
return hmac.compare_digest(expected, actual)
@app.route("/webhooks/dpay", methods=["POST"])
def handle_webhook():
payload = request.get_data() # surowe body, NIE request.json
signature = request.headers.get("X-Signature", "")
timestamp = request.headers.get("X-Timestamp", "")
if not verify_signature(payload, signature, timestamp):
abort(401)
event = request.get_json()
event_type = event["type"]
if event_type == "verification.completed":
session_id = event["data"]["verification_session_id"]
result_status = event["data"].get("result_status")
# Przetwarzaj wynik...
return "", 200
Przykład Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.DPAY_EID_WEBHOOK_SECRET;
// Ważne: zapisz raw body, NIE używaj express.json() przed weryfikacją
app.use('/webhooks/dpay', express.raw({ type: 'application/json' }));
function verifySignature(payload, signature, timestamp) {
const message = `${timestamp}.${payload.toString('utf-8')}`;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(message)
.digest('hex');
const actual = signature.replace('sha256=', '');
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(actual, 'hex')
);
}
app.post('/webhooks/dpay', (req, res) => {
const signature = req.headers['x-signature'] || '';
const timestamp = req.headers['x-timestamp'] || '';
if (!verifySignature(req.body, signature, timestamp)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf-8'));
switch (event.type) {
case 'verification.completed':
// Przetwarzaj wynik
break;
case 'verification.failed':
// Obsługa odrzucenia
break;
}
res.status(200).send('OK');
});
Najczęstsze błędy
| Błąd | Przyczyna | Rozwiązanie |
|---|---|---|
| Sygnatura zawsze niezgodna | Używasz sparsowanego JSON zamiast raw body | Czytaj surowe ciało zapytania (php://input, request.get_data(), req.body z express.raw()) |
| Sygnatura niezgodna sporadycznie | Middleware modyfikuje body (np. dodaje whitespace) | Zarejestruj raw body przed wszystkimi middleware'ami JSON |
hash_equals rzuca błąd | Różne długości stringów | Najpierw sprawdź czy nagłówki są obecne, dopiero potem porównuj |
| 401 na produkcji, OK na sandboxie | Inny secret per środowisko | Każde środowisko ma własny webhook + własny secret |
| Timing attack | Używanie === zamiast hash_equals | Zawsze używaj timing-safe comparison |
Replay attack protection
Dodatkowo możesz weryfikować że X-Timestamp jest świeży (np. nie starszy niż 5 minut):
$timestamp = (int) $_SERVER['HTTP_X_TIMESTAMP'];
$now = time();
if (abs($now - $timestamp) > 300) { // 5 minut
http_response_code(401);
exit('Webhook timestamp too old');
}
To zabezpiecza przed atakami typu replay - odtworzeniem starszego, nagranego wcześniej webhooka.