SHA HMAC signature verification for Paystack Webhook in Go lang

ยท

2 min read

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.