Verifikasi signature webhook
Signature membuktikan bahwa pengirim memiliki secret endpoint webhook Anda dan bahwa body tidak berubah. Verifikasi ini harus berhasil sebelum body dipakai untuk memperbarui pesanan.
Data yang dibutuhkan
| Input | Sumber |
|---|---|
| Raw body | Bytes request HTTP asli, sebelum JSON parsing atau normalisasi. |
| Timestamp | Header x-valcentra-timestamp, berupa Unix timestamp dalam detik. |
| Signature | Header x-valcentra-signature, format v1= diikuti 64 digit hex huruf kecil. |
| Secret | Secret endpoint yang disimpan di backend, bukan API key. |
Jangan menghitung signature dari JSON.stringify(request.body). Spasi, urutan properti, dan encoding yang berubah menghasilkan bytes berbeda meskipun JSON-nya bermakna sama.
Cara signature dihitung
UTF-8(timestamp + ".") + raw_body_bytesValcentra menghitung HMAC-SHA256 atas pesan tersebut dengan secret endpoint. Hasil digest diubah ke lowercase hex, lalu diberi awalan v1=.
Receiver membandingkan digest secara constant-time, bukan dengan perbandingan string biasa. Receiver juga menolak timestamp yang berbeda lebih dari 300 detik (5 menit) dari waktu saat ini, baik terlalu lama maupun terlalu jauh di masa depan.
Timestamp header adalah waktu penandatanganan delivery, bukan event.created_at. Event lama yang dikirim ulang dapat memiliki timestamp pengiriman baru dan tetap valid.
Contoh TypeScript untuk backend Node.js/Bun
Fungsi berikut hanya memverifikasi signature. Integrasikan dengan mekanisme raw body framework backend Anda; atur batas ukuran request sebelum membaca body. Contoh ini tidak menggantikan validasi schema, deduplikasi, atau transaksi pesanan.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook({
rawBody,
timestampHeader,
signatureHeader,
secret,
nowSeconds = Math.floor(Date.now() / 1000),
}: {
rawBody: Uint8Array;
timestampHeader: string | null;
signatureHeader: string | null;
secret: string;
nowSeconds?: number;
}): boolean {
if (!secret || !timestampHeader || !signatureHeader) return false;
if (!/^[0-9]+$/.test(timestampHeader)) return false;
const timestamp = Number(timestampHeader);
if (!Number.isSafeInteger(timestamp) || !Number.isSafeInteger(nowSeconds)) {
return false;
}
if (Math.abs(nowSeconds - timestamp) > 300) return false;
if (!/^v1=[0-9a-f]{64}$/.test(signatureHeader)) return false;
const expected = createHmac("sha256", secret)
.update(timestampHeader + ".", "utf8")
.update(rawBody)
.digest();
const received = Buffer.from(signatureHeader.slice(3), "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
}Pada receiver Anda:
- Baca kedua header dan raw body dengan batas ukuran yang sesuai.
- Panggil
verifyWebhookmenggunakan secret endpoint dari secret manager. - Jika hasilnya
false, tolak request tanpa perubahan pesanan. - Jika hasilnya
true, parse JSON dan validasi field sesuai schema event. - Simpan event secara persisten dan deduplikasi sebelum mengakui sukses, seperti pada panduan webhook.
Signature valid belum berarti event sudah aman diproses
Anda masih harus memeriksa schema, livemode, payment ID, jumlah, mata uang, referensi pesanan, dan apakah event sudah diproses.
Signature tidak mencegah efek bisnis ganda tanpa deduplikasi.
Jika verifikasi gagal
| Penyebab yang umum | Pemeriksaan |
|---|---|
| Secret salah | Cocokkan endpoint, project, dan environment tanpa mencetak secret. |
| Body berubah | Nonaktifkan JSON parsing sebelum verifikasi pada route receiver; jangan stringify ulang body. |
| Timestamp di luar toleransi | Sinkronkan jam server dan periksa bahwa timestamp diperlakukan sebagai detik, bukan milidetik. |
| Header hilang atau berubah | Periksa proxy dan middleware yang meneruskan request. |
| Secret baru belum dipasang | Perbarui konfigurasi receiver setelah pergantian secret. |
Jangan menonaktifkan verifikasi untuk membuat delivery terlihat sukses. Catat kategori kegagalan saja, tanpa signature lengkap, secret, raw body, atau credential.
Saat mengganti secret
Jika pergantian secret melibatkan masa transisi, receiver dapat memverifikasi dengan secret aktif dan secret sebelumnya yang secara eksplisit diizinkan. Setiap kandidat tetap harus melewati pemeriksaan timestamp dan constant-time comparison. Hapus secret lama setelah transisi selesai.
Ini adalah strategi receiver Anda, bukan jaminan bahwa Valcentra mengirim beberapa signature atau menyediakan masa berlaku secret lama tertentu. Untuk secret yang bocor, jangan mempertahankan secret lama sebagai fallback.
Lanjutkan ke: webhook untuk pemrosesan event, atau retry webhook untuk diagnosis delivery gagal.
