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.
Butuh implementasi untuk kebutuhan bisnis?
OVLA Media membantu pengembangan dan optimasi aplikasi web, integrasi sistem, serta solusi Laravel sesuai kebutuhan perusahaan Anda.
Artikel Terkait
Cara Mencegah Transaksi Ganda dari Webhook Payment Gateway
Webhook payment gateway dapat terkirim lebih dari satu kali karena retry atau gangguan jaringan. Jika tidak ditangani dengan benar, satu pembayaran dapat tercatat sebagai transaksi ganda. Laravel dapat mencegah masalah ini dengan idempotency, unique transaction ID, database transaction, dan validasi status pembayaran.
Cara Membuat Login Menggunakan Email atau Username di Laravel
Laravel dapat dibuat agar pengguna login menggunakan email atau username dalam satu form. Dengan validasi yang tepat, sistem dapat mencari akun berdasarkan salah satu identitas tersebut tanpa membuat halaman login terpisah.
Cara Membatasi Percobaan Login Menggunakan Rate Limiting Laravel
Rate limiting pada Laravel dapat digunakan untuk membatasi jumlah percobaan login dalam periode tertentu. Fitur ini membantu mengurangi risiko brute-force dengan menolak sementara permintaan login yang terlalu banyak dari pengguna atau alamat IP yang sama.