Integration Implementation Examples
const express = require('express');
const crypto = require('crypto');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 8080;
const SIGNING_SECRET = process.env.SIGNING_SECRET;
const TOLERANCE_PERIOD = 5 * 60 * 1000; // 5 minutes in milliseconds
// Middleware to preserve raw body for signature verification
const rawBodySaver = function (req, res, buf, encoding) {
if (buf && buf.length) {
req.rawBody = buf.toString(encoding || 'utf8');
}
}
// Function to calculate HMAC signature
function calculateHMAC(payloadWithTimestamp) {
const hmac = crypto.createHmac('sha256', SIGNING_SECRET);
hmac.update(payloadWithTimestamp);
return hmac.digest('base64');
}
// Middleware to verify webhook signature and prevent replay attacks
function verifyWebhookSignature(req, res, next) {
const timestamp = req.headers['paynow-timestamp'];
const providedSignature = req.headers['paynow-signature'];
if (!timestamp || !providedSignature) {
return res.status(400).send('Missing required headers');
}
const timestampInt = parseInt(timestamp, 10);
if (isNaN(timestampInt)) {
return res.status(400).send('Invalid timestamp format');
}
const timestampTime = new Date(timestampInt);
const currentTime = new Date();
if (currentTime - timestampTime > TOLERANCE_PERIOD) {
return res.status(401).send('Timestamp out of tolerance');
}
const payloadWithTimestamp = `${timestamp}.${req.rawBody}`;
const expectedSignature = calculateHMAC(payloadWithTimestamp);
const signatureValid = crypto.timingSafeEqual(Buffer.from(providedSignature, 'base64'), Buffer.from(expectedSignature, 'base64'));
if (!signatureValid) {
return res.status(401).send('Invalid signature');
}
next();
}
app.use(express.json({ verify: rawBodySaver }));
app.post('/webhook', verifyWebhookSignature, (req, res) => {
const eventType = req.body.event_type;
switch (eventType) {
case 'ON_DELIVERY_ITEM_ADDED':
// handleOnDeliveryItemAdded(req.body);
break;
case 'ON_DELIVERY_ITEM_ACTIVATED':
// handleOnDeliveryItemActivated(req.body);
break;
case 'ON_DELIVERY_ITEM_USED':
// handleOnDeliveryItemUsed(req.body);
break;
case 'ON_DELIVERY_ITEM_REVOKED':
// handleOnDeliveryItemRevoked(req.body);
break;
default:
console.log(`Received unknown event type: ${eventType}`);
}
res.status(200).send('Webhook processed');
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"time"
)
const tolerancePeriod = 5 * time.Minute
func getSigningSecret() []byte {
return []byte(os.Getenv("SIGNING_SECRET"))
}
func calculateHMAC(payloadWithTimestamp string) string {
h := hmac.New(sha256.New, getSigningSecret())
h.Write([]byte(payloadWithTimestamp))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func verifyWebhookSignature(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
timestamp := r.Header.Get("paynow-timestamp")
providedSignature := r.Header.Get("paynow-signature")
if timestamp == "" || providedSignature == "" {
http.Error(w, "Missing required headers", http.StatusBadRequest)
return
}
timestampInt, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
http.Error(w, "Invalid timestamp format", http.StatusBadRequest)
return
}
timestampTime := time.Unix(timestampInt/1000, (timestampInt%1000)*int64(time.Millisecond))
currentTime := time.Now()
if currentTime.Sub(timestampTime) > tolerancePeriod {
http.Error(w, "Timestamp out of tolerance", http.StatusUnauthorized)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
payloadWithTimestamp := fmt.Sprintf("%d.%s", timestampInt, body)
expectedSignature := calculateHMAC(payloadWithTimestamp)
if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
next(w, r)
}
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
var data struct {
EventType string `json:"event_type"`
}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, "Error decoding JSON", http.StatusBadRequest)
return
}
fmt.Printf("Received event type: %s\n", data.EventType)
// Process event based on data.EventType here
fmt.Fprintln(w, "Webhook processed")
}
func main() {
http.HandleFunc("/webhook", verifyWebhookSignature(handleWebhook))
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Server running at http://localhost:%s/\n", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}Last updated
Was this helpful?
