Skip to content

webhooks #241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/String-xyz/string-api/api/handler"
"github.com/String-xyz/string-api/api/middleware"
"github.com/String-xyz/string-api/config"

"github.com/jmoiron/sqlx"
"github.com/labstack/echo/v4"
Expand Down Expand Up @@ -117,5 +118,5 @@ func cardRoute(services service.Services, e *echo.Echo) {

func webhookRoute(services service.Services, e *echo.Echo) {
handler := handler.NewWebhook(e, services.Webhook)
handler.RegisterRoutes(e.Group("/webhooks"), middleware.VerifyWebhookPayload())
handler.RegisterRoutes(e.Group("/webhooks"), middleware.VerifyWebhookPayload(config.Var.PERSONA_WEBHOOK_SECRET_KEY, config.Var.CHECKOUT_WEBHOOK_SECRET_KEY))
}
27 changes: 22 additions & 5 deletions api/handler/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import (
)

type Webhook interface {
Handle(c echo.Context) error
HandleCheckout(c echo.Context) error
HandlePersona(c echo.Context) error
RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc)
}

Expand All @@ -24,22 +25,38 @@ func NewWebhook(route *echo.Echo, service service.Webhook) Webhook {
return &webhook{service, nil}
}

func (w webhook) Handle(c echo.Context) error {
func (w webhook) HandleCheckout(c echo.Context) error {
cxt := c.Request().Context()
body, err := io.ReadAll(c.Request().Body)
if err != nil {
return httperror.BadRequest400(c, "Failed to read body")
}

err = w.service.Handle(cxt, body)
err = w.service.Handle(cxt, body, service.WebhookTypeCheckout)
if err != nil {
return httperror.Internal500(c, "Failed to handle webhook")
return httperror.Internal500(c, "Failed to handle checkout webhook")
}

return nil
}

func (w webhook) HandlePersona(c echo.Context) error {
cxt := c.Request().Context()
body, err := io.ReadAll(c.Request().Body)
if err != nil {
return httperror.BadRequest400(c, "Failed to read body")
}

err = w.service.Handle(cxt, body, service.WebhookTypePersona)
if err != nil {
return httperror.Internal500(c, "Failed to handle persona webhook")
}

return nil
}

func (w webhook) RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) {
g.POST("/checkout", w.Handle, ms...)
w.group = g
g.POST("/checkout", w.HandleCheckout, ms...)
g.POST("/persona", w.HandlePersona, ms...)
}
73 changes: 58 additions & 15 deletions api/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/sha256"
"encoding/hex"
"io"
"strings"

libcommon "github.com/String-xyz/go-lib/v2/common"
"github.com/String-xyz/go-lib/v2/httperror"
Expand Down Expand Up @@ -73,7 +74,6 @@ func APIKeySecretAuth(service service.Auth) echo.MiddlewareFunc {

// TODO: Validate platformId
c.Set("platformId", platformId)

return true, nil
},
}
Expand Down Expand Up @@ -102,34 +102,77 @@ func Georestrict(service service.Geofencing) echo.MiddlewareFunc {
}
}

func VerifyWebhookPayload() echo.MiddlewareFunc {
secretKey := config.Var.WEBHOOK_SECRET_KEY

func VerifyWebhookPayload(pskey string, ckoskey string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
signatureHeader := c.Request().Header.Get("Cko-Signature")
var signatureHeaderName string
var secretKey string
var validateFunc func([]byte, string, string) bool

switch c.Path() {
case "/webhooks/checkout":
signatureHeaderName = "Cko-Signature"
secretKey = ckoskey
validateFunc = validateSignatureCheckout
case "/webhooks/persona":
signatureHeaderName = "Persona-Signature"
secretKey = pskey
validateFunc = validateSignaturePersona
default:
return httperror.BadRequest400(c, "Invalid path")
}

signatureHeader := c.Request().Header.Get(signatureHeaderName)
body, err := io.ReadAll(c.Request().Body)
if err != nil {
return httperror.BadRequest400(c, "Failed to read body")
}

c.Request().Body = io.NopCloser(bytes.NewBuffer(body))

mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write(body)
expectedMAC := mac.Sum(nil)

receivedMAC, err := hex.DecodeString(signatureHeader)
if err != nil {
return httperror.BadRequest400(c, "Failed to decode signature")
}

if !hmac.Equal(receivedMAC, expectedMAC) {
if !validateFunc(body, signatureHeader, secretKey) {
return httperror.Unauthorized401(c, "Failed to verify payload")
}

return next(c)
}
}
}

func validateSignatureCheckout(body []byte, signature string, secretKey string) bool {
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write(body)
expectedMAC := mac.Sum(nil)

receivedMAC, err := hex.DecodeString(signature)
if err != nil {
return false
}

return hmac.Equal(receivedMAC, expectedMAC)
}

func validateSignaturePersona(body []byte, signatureHeader string, secretKey string) bool {
parts := strings.Split(signatureHeader, ",")
var timestamp, signature string
for _, part := range parts {
if strings.HasPrefix(part, "t=") {
timestamp = strings.TrimPrefix(part, "t=")
} else if strings.HasPrefix(part, "v1=") {
signature = strings.TrimPrefix(part, "v1=")
}
}

macData := timestamp + "." + string(body)

mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(macData))
expectedMAC := mac.Sum(nil)

receivedMAC, err := hex.DecodeString(signature)
if err != nil {
return false
}

return hmac.Equal(expectedMAC, receivedMAC)
}
106 changes: 52 additions & 54 deletions api/middleware/middleware_test.go
Original file line number Diff line number Diff line change
@@ -1,90 +1,88 @@
package middleware

import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

env "github.com/String-xyz/go-lib/v2/config"
"github.com/String-xyz/string-api/config"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)

func init() {
env.LoadEnv(&config.Var, "../../.env")
}

func TestVerifyWebhookPayload(t *testing.T) {
secretKey := config.Var.WEBHOOK_SECRET_KEY
checkoutSecretKey := "checkout_secret_key"
personaSecretKey := "persona_secret_key"

// We'll test with two cases
tests := []struct {
name string
giveBody []byte
giveMAC string
wantHTTPCode int
name string
path string
signatureKey string
signatureName string
secretKey string
expectCode int
}{
{
// This test case provides a valid body and MAC
name: "Valid MAC",
giveBody: []byte("Hello, World!"),
giveMAC: ComputeMAC([]byte("Hello, World!"), secretKey),
wantHTTPCode: http.StatusOK,
name: "Test unauthorized access due to invalid signature for Checkout",
path: "/webhooks/checkout",
signatureKey: "invalid_signature",
signatureName: "Cko-Signature",
secretKey: checkoutSecretKey,
expectCode: http.StatusUnauthorized,
},
{
name: "Test successful access for Checkout",
path: "/webhooks/checkout",
signatureKey: computeHmacSha256("hello", checkoutSecretKey),
signatureName: "Cko-Signature",
secretKey: checkoutSecretKey,
expectCode: http.StatusOK,
},
{
name: "Test unauthorized access due to invalid signature for Persona",
path: "/webhooks/persona",
signatureKey: "t=1629478952,v1=invalid_signature",
signatureName: "Persona-Signature",
secretKey: personaSecretKey,
expectCode: http.StatusUnauthorized,
},
{
// This test case provides an invalid MAC
name: "Invalid MAC",
giveBody: []byte("Hello, World!"),
giveMAC: ComputeMAC([]byte("Bye, World!"), secretKey),
wantHTTPCode: http.StatusUnauthorized,
name: "Test successful access for Persona",
path: "/webhooks/persona",
signatureKey: fmt.Sprintf("t=1629478952,v1=%s", computeHmacSha256("1629478952.hello", personaSecretKey)),
signatureName: "Persona-Signature",
secretKey: personaSecretKey,
expectCode: http.StatusOK,
},
}

// Let's iterate over our test cases
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Instantiate Echo
e := echo.New()

// Our middleware under test
middleware := VerifyWebhookPayload()

// Mock a request
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(tt.giveBody))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set("Cko-Signature", tt.giveMAC)

// Mock a response recorder
req := httptest.NewRequest(echo.POST, "/", strings.NewReader("hello"))
req.Header.Set(tt.signatureName, tt.signatureKey)
rec := httptest.NewRecorder()

// Create a context for our request
c := e.NewContext(req, rec)
c.SetPath(tt.path)

// Mock a next function
next := func(c echo.Context) error {
return c.String(http.StatusOK, "OK")
}

// Call our middleware
err := middleware(next)(c)

// There should be no error returned
assert.NoError(t, err)
middleware := VerifyWebhookPayload(personaSecretKey, checkoutSecretKey)
middleware(func(c echo.Context) error {
return c.String(http.StatusOK, "Test")
})(c)

// Check if the status code is what we expect
assert.Equal(t, tt.wantHTTPCode, rec.Code)
assert.Equal(t, tt.expectCode, rec.Code)
})
}
}

// Helper function to compute the MAC of a given body and secret
func ComputeMAC(body []byte, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
return hex.EncodeToString(mac.Sum(nil))
// Utility function to compute HMAC for testing
func computeHmacSha256(message string, secret string) string {
key := []byte(secret)
h := hmac.New(sha256.New, key)
h.Write([]byte(message))
return hex.EncodeToString(h.Sum(nil))
}
Loading