Verifikasi Signature Webhook
Untuk memastikan bahwa webhook yang diterima oleh server Anda benar-benar berasal dari Gerbang Pay (dan bukan dari pihak jahat yang memalsukan data transaksi), kami mengamankan setiap pengiriman webhook menggunakan algoritma HMAC-SHA256.
Semua payload HTTP Request Body ditandatangani menggunakan webhook_secret unik milik Anda, dan hasilnya dilampirkan pada header X-Gerbang-Signature.
Konsep Dasar Verifikasi
- Ambil raw string/bytes dari request body (sebelum di-parse menjadi JSON object).
- Ambil nilai header
X-Gerbang-Signature. - Buang prefix
sha256=dari header tersebut, sisanya adalah hash aslinya (berbentuk hexadecimal). - Di sisi server Anda, hitung HMAC-SHA256 dari raw body menggunakan kunci
webhook_secretmilik Anda. - Bandingkan hasil perhitungan server Anda dengan hash dari header. Jika sama, webhook valid. Jika berbeda, tolak webhook tersebut (HTTP 401/403).
⚠️ Sangat Penting: Anda harus menggunakan body request dalam bentuk mentah (raw buffer/string bytes). Jika framework Anda (misalnya Express.js) mengubah body menjadi JSON object terlebih dahulu, lalu di-stringify kembali, spasinya bisa berubah sehingga hasil HMAC tidak akan pernah cocok (selalu invalid).
Contoh Implementasi di Berbagai Bahasa
1. Node.js (Express)
Bagi pengguna Express.js, Anda perlu mem-bypass express.json() khusus untuk route webhook agar Anda bisa mengakses raw body-nya.
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = 'secret_dari_dashboard_anda'; // Simpan di .env
// Simpan rawBody khusus untuk verifikasi
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
app.post('/webhook/gerbangpay', (req, res) => {
const signatureHeader = req.headers['x-gerbang-signature'];
if (!signatureHeader || !signatureHeader.startsWith('sha256=')) {
return res.status(401).send('Missing or invalid signature header');
}
// Buang tulisan 'sha256='
const providedSignature = signatureHeader.replace('sha256=', '');
// Generate ulang HMAC
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
hmac.update(req.rawBody);
const calculatedSignature = hmac.digest('hex');
// Bandingkan menggunakan constant-time compare untuk mencegah timing attack
if (crypto.timingSafeEqual(Buffer.from(providedSignature), Buffer.from(calculatedSignature))) {
// Signature valid!
const event = req.body;
console.log(`Menerima event: ${event.event_type} untuk transaksi ${event.data.provider_order_id}`);
// TODO: Update status di database Anda
res.status(200).send('OK');
} else {
// Signature invalid, tolak!
console.error("Webhook signature mismatch!");
res.status(401).send('Invalid signature');
}
});
app.listen(3000, () => console.log('Server berjalan'));2. Python (Flask)
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
WEBHOOK_SECRET = 'secret_dari_dashboard_anda'.encode('utf-8')
@app.route('/webhook/gerbangpay', methods=['POST'])
def webhook_handler():
signature_header = request.headers.get('X-Gerbang-Signature', '')
if not signature_header.startswith('sha256='):
return jsonify({"error": "Invalid signature format"}), 401
provided_signature = signature_header.replace('sha256=', '')
# Ambil body raw
raw_body = request.get_data()
# Generate HMAC
calculated_signature = hmac.new(
WEBHOOK_SECRET,
msg=raw_body,
digestmod=hashlib.sha256
).hexdigest()
# Compare secara aman
if hmac.compare_digest(provided_signature, calculated_signature):
event = request.json
print(f"Event valid: {event.get('event_type')}")
return "OK", 200
else:
return "Invalid signature", 401
if __name__ == '__main__':
app.run(port=5000)3. PHP
<?php
$webhookSecret = 'secret_dari_dashboard_anda';
$signatureHeader = $_SERVER['HTTP_X_GERBANG_SIGNATURE'] ?? '';
if (strpos($signatureHeader, 'sha256=') !== 0) {
http_response_code(401);
die('Invalid signature header');
}
$providedSignature = substr($signatureHeader, 7); // Potong 'sha256='
// Ambil raw HTTP body
$rawBody = file_get_contents('php://input');
// Hitung HMAC
$calculatedSignature = hash_hmac('sha256', $rawBody, $webhookSecret);
// Gunakan hash_equals untuk mencegah timing attack
if (hash_equals($calculatedSignature, $providedSignature)) {
// Valid
$event = json_decode($rawBody, true);
// Lakukan update database...
http_response_code(200);
echo "OK";
} else {
// Invalid
http_response_code(401);
die('Signature verification failed');
}4. Go (Golang)
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strings"
)
const WebhookSecret = "secret_dari_dashboard_anda"
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signatureHeader := r.Header.Get("X-Gerbang-Signature")
if !strings.HasPrefix(signatureHeader, "sha256=") {
http.Error(w, "Invalid signature header", http.StatusUnauthorized)
return
}
providedSignature := strings.TrimPrefix(signatureHeader, "sha256=")
// Baca raw body
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Cannot read body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
// Hitung HMAC
mac := hmac.New(sha256.New, []byte(WebhookSecret))
mac.Write(rawBody)
calculatedSignature := hex.EncodeToString(mac.Sum(nil))
// Compare securely
if hmac.Equal([]byte(providedSignature), []byte(calculatedSignature)) {
// Valid
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {
// Invalid
http.Error(w, "Invalid signature", http.StatusUnauthorized)
}
}
func main() {
http.HandleFunc("/webhook/gerbangpay", webhookHandler)
http.ListenAndServe(":8080", nil)
}