Cara Membuat Webhook Laravel yang Aman dengan Signature Verification
Apa Itu Webhook?
Webhook adalah mekanisme ketika sebuah layanan mengirimkan HTTP request ke aplikasi setelah terjadi suatu event.
Contohnya:
Pembayaran berhasil
↓
Payment Gateway
↓
Webhook Laravel
↓
Update Status TransaksiWebhook banyak digunakan untuk:
Payment gateway.
WhatsApp API.
Marketplace.
Sistem ERP.
Sistem CRM.
Aplikasi pihak ketiga.
Mengapa Webhook Harus Diamankan?
Endpoint webhook biasanya dapat diakses melalui internet.
Misalnya:
POST /api/webhook/paymentJika tidak memiliki validasi, siapa pun dapat mencoba mengirim request:
{
"status": "paid"
}Aplikasi bisa saja menganggap pembayaran benar-benar berhasil.
Karena itu, webhook perlu melakukan verifikasi terhadap sumber request.
Apa Itu Signature Verification?
Signature verification menggunakan secret key yang hanya diketahui oleh aplikasi dan penyedia webhook.
Konsep sederhananya:
Provider
↓
Data + Secret Key
↓
Generate Signature
↓
Laravel menerima request
↓
Generate Signature kembali
↓
Bandingkan
↓
Valid / DitolakJika signature tidak cocok, request ditolak.
1. Membuat Endpoint Webhook
Tambahkan route:
Route::post(
'/webhook/payment',
[WebhookController::class, 'payment']
);Endpoint dapat diakses oleh provider:
https://example.com/api/webhook/payment2. Simpan Secret di .env
Jangan menyimpan secret langsung di source code.
Gunakan:
WEBHOOK_SECRET=your-secret-keyKemudian pada Laravel:
$secret = config('services.webhook.secret');Tambahkan pada:
config/services.php'webhook' => [
'secret' => env('WEBHOOK_SECRET'),
],Dengan cara ini secret tidak perlu ditulis langsung di controller.
3. Membuat Signature
Misalnya provider menggunakan HMAC SHA-256.
Data yang ditandatangani:
request bodyKemudian:
hash_hmac(
'sha256',
$payload,
$secret
);Contoh:
$signature = hash_hmac(
'sha256',
$payload,
$secret
);Hasilnya berupa string hash yang digunakan sebagai signature.
4. Mengambil Raw Request Body
Untuk signature verification, gunakan body asli request.
Di Laravel:
$payload = $request->getContent();Jangan langsung menggunakan:
$request->all();untuk proses signature jika provider menghitung signature berdasarkan raw body.
Perbedaan format JSON dapat menghasilkan signature yang berbeda.
5. Verifikasi Signature
Misalnya provider mengirim signature melalui header:
X-Webhook-SignatureController:
public function payment(Request $request)
{
$payload = $request->getContent();
$signature = $request->header(
'X-Webhook-Signature'
);
$expected = hash_hmac(
'sha256',
$payload,
config('services.webhook.secret')
);
if (!$signature ||
!hash_equals($expected, $signature)
) {
abort(401, 'Invalid signature');
}
// Proses webhook
return response()->json([
'message' => 'Webhook received',
]);
}6. Mengapa Menggunakan hash_equals()?
Jangan membandingkan signature menggunakan:
if ($expected == $signature)Gunakan:
hash_equals(
$expected,
$signature
);hash_equals() dirancang untuk perbandingan string secara timing-safe sehingga lebih tepat digunakan ketika membandingkan signature autentikasi.
7. Tambahkan Timestamp
Signature saja belum selalu cukup.
Salah satu masalah yang perlu diperhatikan adalah replay attack.
Contohnya penyerang mendapatkan request webhook yang valid:
Payment: Rp500.000
Signature: validKemudian request tersebut dikirim kembali berkali-kali.
Untuk mengurangi risiko ini, provider dapat menyertakan timestamp:
X-Webhook-TimestampSignature kemudian dihitung dari:
timestamp + "." + payloadContoh:
$timestamp = $request->header(
'X-Webhook-Timestamp'
);
$payload = $request->getContent();
$signedPayload =
$timestamp . '.' . $payload;
$expected = hash_hmac(
'sha256',
$signedPayload,
config('services.webhook.secret')
);8. Periksa Timestamp
Jangan menerima timestamp yang terlalu lama.
Contohnya:
if (
abs(now()->timestamp - (int) $timestamp)
> 300
) {
abort(
401,
'Webhook expired'
);
}Artinya request hanya diterima jika timestamp masih berada dalam rentang sekitar 5 menit.
Batas waktunya dapat disesuaikan dengan sistem provider.
9. Gunakan HTTPS
Webhook production harus menggunakan HTTPS:
https://example.com/api/webhook/paymentHindari:
http://example.com/api/webhook/paymentHTTPS membantu melindungi data yang dikirim antara provider dan server aplikasi.
10. Jangan Menaruh Secret di URL
Hindari:
https://example.com/webhook?secret=123456Secret pada URL dapat masuk ke log server, proxy, browser history, atau sistem monitoring.
Lebih baik menggunakan:
X-Webhook-Signatureatau header autentikasi yang disediakan provider.
11. Gunakan Idempotency
Webhook dapat dikirim lebih dari satu kali.
Contohnya:
Payment Success
↓
Webhook #1
Webhook #2
Webhook #3Aplikasi harus dapat menangani request yang sama tanpa menggandakan transaksi.
Simpan ID event:
event_idpada database.
Contohnya:
if (
WebhookEvent::where(
'event_id',
$eventId
)->exists()
) {
return response()->json([
'message' => 'Already processed',
]);
}Dengan demikian event yang sama tidak diproses berulang kali.
12. Gunakan Database Transaction
Jika webhook mengubah beberapa data sekaligus, gunakan transaction.
Contohnya:
DB::transaction(function () use ($data) {
$payment = Payment::create([
'transaction_id' => $data['transaction_id'],
'status' => 'paid',
]);
Transaction::where(
'id',
$data['transaction_id']
)->update([
'status' => 'paid',
]);
});Jika terjadi error, perubahan dapat dibatalkan secara otomatis.
13. Jangan Langsung Memproses Data Berat
Webhook sebaiknya memberikan response dengan cepat.
Jika proses membutuhkan waktu lama:
Webhook
↓
Validasi Signature
↓
Simpan Event
↓
Queue
↓
Response 200Kemudian proses berat dilakukan oleh Laravel Queue.
Contohnya:
ProcessPaymentWebhook::dispatch(
$data
);Pendekatan ini membantu mencegah timeout dari provider.
14. Validasi Payload
Signature yang valid tidak berarti isi data otomatis benar.
Tetap lakukan validasi:
$data = $request->validate([
'event_id' => ['required', 'string'],
'transaction_id' => ['required', 'string'],
'status' => [
'required',
'string',
],
]);Kemudian proses data yang sudah divalidasi.
15. Jangan Percaya Status dari Request Saja
Misalnya webhook mengirim:
{
"transaction_id": "TRX-001",
"status": "paid"
}Jangan langsung menganggap pembayaran valid hanya karena status tersebut diterima.
Untuk transaksi penting, sistem dapat melakukan verifikasi tambahan sesuai kemampuan provider.
Contohnya:
Webhook
↓
Signature Valid
↓
Event Valid
↓
Transaction Valid
↓
Update Status16. Rate Limiting
Webhook juga dapat diberikan rate limiting sebagai lapisan perlindungan tambahan.
Namun, jangan menerapkan limit terlalu ketat karena provider dapat mengirim retry ketika webhook tidak mendapatkan response yang sesuai.
Gunakan batas yang sesuai dengan dokumentasi provider.
17. Logging Webhook
Simpan log yang cukup untuk troubleshooting:
Log::info(
'Webhook payment received',
[
'event_id' => $eventId,
'transaction_id' => $transactionId,
]
);Hindari mencatat:
Password
Secret Key
Authorization Token
Data kartu
Data sensitif pelanggan18. Contoh Controller yang Lebih Aman
Contoh sederhana:
public function payment(Request $request)
{
$payload = $request->getContent();
$signature = $request->header(
'X-Webhook-Signature'
);
$timestamp = $request->header(
'X-Webhook-Timestamp'
);
if (!$signature || !$timestamp) {
return response()->json([
'message' => 'Invalid webhook',
], 401);
}
if (
abs(now()->timestamp - (int) $timestamp)
> 300
) {
return response()->json([
'message' => 'Webhook expired',
], 401);
}
$signedPayload =
$timestamp . '.' . $payload;
$expected = hash_hmac(
'sha256',
$signedPayload,
config('services.webhook.secret')
);
if (!hash_equals(
$expected,
$signature
)) {
return response()->json([
'message' => 'Invalid signature',
], 401);
}
$data = json_decode(
$payload,
true
);
if (!$data) {
return response()->json([
'message' => 'Invalid payload',
], 400);
}
ProcessPaymentWebhook::dispatch($data);
return response()->json([
'message' => 'Webhook received',
]);
}Implementasi sebenarnya harus mengikuti format signature dari provider yang digunakan.
Contoh Alur Webhook yang Aman
Provider
↓
HTTPS
↓
Laravel Webhook
↓
Validasi Timestamp
↓
Verifikasi Signature
↓
Validasi Payload
↓
Cek Event ID
↓
Simpan Event
↓
Queue
↓
Update TransaksiDengan alur tersebut, webhook memiliki beberapa lapisan perlindungan.
Kesalahan yang Perlu Dihindari
Hanya Memeriksa IP
IP address dapat berubah dan tidak selalu menjadi mekanisme autentikasi yang cukup.
Tidak Memverifikasi Signature
Siapa pun dapat mencoba memanggil endpoint webhook.
Menggunakan Request Data yang Sudah Diparse
Signature sering dihitung berdasarkan raw body. Gunakan:
$request->getContent();sesuai aturan provider.
Tidak Menggunakan hash_equals()
Gunakan perbandingan timing-safe untuk signature.
Tidak Menangani Duplicate Event
Webhook dapat dikirim ulang sehingga idempotency penting.
Memproses Data Terlalu Lama
Gunakan queue untuk proses berat.
Menyimpan Secret di Source Code
Gunakan .env dan konfigurasi Laravel.
Webhook Laravel Bersama Ovla Media
Ovla Media dapat membantu membangun integrasi webhook yang aman untuk:
Payment gateway.
WhatsApp API.
Marketplace.
Sistem ERP.
CRM.
Sistem POS.
Sistem klinik.
Payment notification.
Sinkronisasi data.
Aplikasi bisnis custom.
Webhook juga dapat dilengkapi dengan signature verification, idempotency, queue, logging, retry handling, dan monitoring.
Kesimpulan
Webhook merupakan bagian penting dari integrasi antar sistem, tetapi endpoint webhook harus dilindungi agar tidak mudah dipalsukan.
Lapisan keamanan yang dapat digunakan:
HTTPS
↓
Signature Verification
↓
Timestamp
↓
Payload Validation
↓
Idempotency
↓
Queue
↓
LoggingUntuk signature berbasis HMAC, Laravel dapat menggunakan:
$expected = hash_hmac(
'sha256',
$payload,
$secret
);
hash_equals(
$expected,
$signature
);Namun, format signature harus mengikuti dokumentasi layanan yang mengirim webhook.
Dengan implementasi yang tepat, Laravel dapat menerima webhook dari berbagai layanan eksternal dengan lebih aman, terkontrol, dan mudah dimonitor.
Artikel Terkait
Cara Membuat Export Excel dengan Filter Tanggal di Laravel
Export Excel dengan filter tanggal memungkinkan pengguna mengunduh data berdasarkan periode tertentu. Fitur ini cocok untuk laporan transaksi, penjualan, pasien, pembayaran, absensi, dan data operasional lainnya di aplikasi Laravel.
Cara Membuat QR Code untuk Verifikasi Invoice Laravel
QR Code pada invoice dapat digunakan untuk membantu pelanggan memverifikasi keaslian dan status dokumen secara online. Dengan Laravel, QR Code dapat diarahkan ke halaman verifikasi yang menampilkan nomor invoice, tanggal, nominal, dan status pembayaran tanpa perlu membuka database secara langsung.
Cara Deploy Laravel ke VPS Ubuntu Menggunakan Nginx dan PHP-FPM
Deploy Laravel ke VPS Ubuntu membutuhkan beberapa komponen seperti Nginx, PHP-FPM, database, Composer, dan konfigurasi permission. Dengan konfigurasi yang tepat, aplikasi Laravel dapat berjalan lebih stabil dan aman di server production.