SHA HMAC signature verification for Paystack Webhook in Go lang
Introduction
This article shows a complete setup on how you can verify a signature sent by paystack when they make a POST
request to your server. I'm using the bun http router as my go lang framework. I really do not have strength for long talks, so i'll show you the code.
Code Section
package service
import (
"crypto/hmac"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/uptrace/bunrouter"
"github.com/uptrace/bunrouter/extra/reqlog"
"github.com/SAMBA-Research/microservice-shared/paystack"
"github.com/SAMBA-Research/microservice-shared/utils"
)
type WebhookResponse struct {
Event string `json:"event"`
Data struct {
ID int `json:"id"`
Domain string `json:"domain"`
Status string `json:"status"`
Reference string `json:"reference"`
Amount int `json:"amount"`
// Message interface{} `json:"message"`
GatewayResponse string `json:"gateway_response"`
PaidAt time.Time `json:"paid_at"`
CreatedAt time.Time `json:"created_at"`
// Channel string `json:"channel"`
// Currency string `json:"currency"`
// IPAddress string `json:"ip_address"`
// Metadata interface{} `json:"metadata"`
// Log Log `json:"log"`
// Fees interface{} `json:"fees"`
// Customer Customer `json:"customer"`
// Authorization Authorization `json:"authorization"`
// Plan interface{} `json:"plan"`
} `json:"data"`
}
func (srv *Microservice) paystackPaymentWebHook(w http.ResponseWriter, r bunrouter.Request) (err error) {
body, err := io.ReadAll(r.Body)
if err != nil {
fmt.Println("Error reading request body:", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
var paystackRequestBody WebhookResponse
defer r.Body.Close()
err = json.Unmarshal(body, &paystackRequestBody)
if err != nil {
fmt.Println("Error decoding JSON:", err)
return
}
signature := r.Header.Get("x-paystack-signature")
if isValidSignature(body, signature, srv.cfg.PaystackSecret) {
// Unmarshal the JSON payload
fmt.Println("valid signature")
if paystackRequestBody.Event == "charge.success" {
// extract tenant and payment_id from reference
ref := paystackRequestBody.Data.Reference
// make request to your service or update your database here.
paystackWebhookRequest, err := utils.POSTJSON[types.CommonResponse](srv.getPaymentService(fmt.Sprintf("/v1/payment/webhook/paystack",)), paystackRequestBody)
if err != nil {
return utils.ReturnError(w, err, http.StatusInternalServerError)
}
if paystackWebhookRequest.ErrorCode != 0 {
return utils.ReturnError(w, fmt.Errorf("service error: %s", paystackWebhookRequest.Message), http.StatusInternalServerError)
}
}
return utils.ReturnJSON(w, nil, http.StatusOK)
} else {
return utils.ReturnError(w, fmt.Errorf("invalid signature %s", signature), http.StatusBadRequest)
}
}
func isValidSignature(payload []byte, signature, secretKey string) bool {
mac := hmac.New(sha512.New, []byte(secretKey))
mac.Write(payload)
expectedMAC := hex.EncodeToString(mac.Sum(nil))
return expectedMAC == signature
}
Things to note
Don't bother trying to install the package below, it's not for public use. they are just libraries that help me perform certain tasks like making http request and retrieving shared types.
"github.com/SAMBA-Research/microservice-shared/paystack"
"github.com/SAMBA-Research/microservice-shared/utils"
Conclusion
So that's how you would typically handle webhook requests in go lang. Hopefully in the feature paystack adds this implementation to their documenation. Thanks Guys, thanks for reading.