Webhook Security
HMAC-SHA256 signature verification, raw body handling ve replay protection.
Pientegra webhook'ları per-site webhook secret ile HMAC-SHA256 kullanarak imzalar.
Signature doğrulanmadan event işlenmemelidir.
Signature header
Pientegra-Signature: t=1730131200000,v1=4f3c2a8d9b1e0c7a6f5d4b3a2e1c0d9b8a7f6e5d4c3b2a190e1d2c3b4a5f6e7d| Parça | Açıklama |
|---|---|
t | Unix timestamp, millisecond. |
v1 | HMAC-SHA256 hex digest. |
Pientegra signature input'u şu formatta hesaplar:
HMAC-SHA256(webhook_secret, "<timestamp>.<raw-body>")raw-body tam olarak HTTP request body byte'larıdır. JSON parse edip tekrar
serialize etmek whitespace veya key order değiştirebileceği için signature'ı bozar.
Verification steps
Pientegra-Signatureheader'ını parse edin.ttimestamp'ini alın ve makul tolerance içinde olduğunu kontrol edin."<timestamp>.<raw-body>"string'i üzerinden HMAC-SHA256 hesaplayın.- Provided digest ile expected digest'i constant-time compare edin.
- Doğrulama geçerse JSON parse edip event'i işleyin.
Node.js example
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_MS = 5 * 60 * 1000;
export function verifyPientegraSignature(
secret: string,
rawBody: Buffer | string,
signatureHeader: string,
): boolean {
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => {
const [key, value] = part.split('=', 2);
return [key?.trim(), value?.trim()];
}),
);
const timestamp = Number(parts.t);
const provided = parts.v1;
if (!timestamp || !provided) return false;
if (Math.abs(Date.now() - timestamp) > TOLERANCE_MS) {
return false;
}
const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody, 'utf8');
const expected = createHmac('sha256', secret)
.update(Buffer.concat([Buffer.from(`${timestamp}.`, 'utf8'), body]))
.digest('hex');
const expectedBuffer = Buffer.from(expected, 'hex');
const providedBuffer = Buffer.from(provided, 'hex');
if (expectedBuffer.length !== providedBuffer.length) return false;
return timingSafeEqual(expectedBuffer, providedBuffer);
}Express raw body example
app.post('/pientegra/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.header('Pientegra-Signature');
if (!signature) return res.status(401).send('missing signature');
if (!verifyPientegraSignature(process.env.PIENTEGRA_WEBHOOK_SECRET!, req.body, signature)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
await handlePientegraEvent(event);
return res.status(200).send('ok');
});Express'te global express.json() middleware'i raw body'yi tüketebilir. Webhook
route'u için express.raw({ type: 'application/json' }) kullandığınızdan emin olun.
PHP example
<?php
function verify_pientegra_signature(
string $secret,
string $rawBody,
string $signatureHeader,
int $toleranceMs = 300000
): bool {
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
[$key, $value] = explode('=', $part, 2);
$parts[trim($key)] = trim($value);
}
$timestamp = intval($parts['t'] ?? 0);
$provided = $parts['v1'] ?? '';
if ($timestamp === 0 || $provided === '') return false;
$nowMs = intval(microtime(true) * 1000);
if (abs($nowMs - $timestamp) > $toleranceMs) return false;
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $provided);
}Replay protection
Timestamp check tek başına yeterli değildir. Saldırgan kısa tolerance penceresi içinde aynı signed request'i tekrar gönderebilir. Bu yüzden:
eventIddeğerini saklayın.eventIdretry'lar arasında değişmez, bir business event tek bireventIdtaşır — yani aynıeventId'i ikinci kez gördüğünüzde duplicate'in kendisidir.- Balance mutation'ları ayrıca business key ile idempotent yapın.
- Duplicate event gördüğünüzde side effect çalıştırmadan
2xxdönün.
Önerilen unique key örnekleri:
| Flow | Idempotency key |
|---|---|
| Deposit credit | intent.approved:<intent.id> |
| Withdrawal debit | withdrawal.sent:<withdrawal.id> |
| Withdrawal refund | withdrawal.rejected:<withdrawal.id> |
Secret rotation
Webhook secret leak şüphesinde yeni secret üretin ve deployment sırasında kısa bir overlap penceresi kullanın. Handler, rotation süresince önce yeni secret ile, başarısız olursa eski secret ile verify edebilir.
function verifyWithRotation(rawBody: Buffer, signature: string): boolean {
return (
verifyPientegraSignature(process.env.PIENTEGRA_WEBHOOK_SECRET_NEW!, rawBody, signature) ||
verifyPientegraSignature(process.env.PIENTEGRA_WEBHOOK_SECRET_OLD!, rawBody, signature)
);
}