Repository: mailgun/mailgun-go
Pull request #351 (2024-11-27 23:11:21)

Fix webhook signature verification to use the webhook signing secret
Since [the other PR](https://github.com/mailgun/mailgun-go/pull/321) didn't seem to be being addressed, I'm submitting my own. The issues brought up in it have been resolved. Tests pass and it seems to work as directed when testing it as a `replace` in a different project. Please let me know if anything needs addressed, I'd like to have this available as soon as I can get it. Thanks!


Patch:
diff --git a/README.md b/README.md
index 2afda08..cb985f0 100644
--- a/README.md
+++ b/README.md
@@ -211,6 +211,7 @@ func main() {
 	// You can find the Private API Key in your Account Menu, under "Settings":
 	// (https://app.mailgun.com/app/account/security)
 	mg := mailgun.NewMailgun("your-domain.com", "private-api-key")
+	mg.SetWebhookSigningKey("webhook-signing-key")
 
 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 
diff --git a/examples/examples.go b/examples/examples.go
index c3f04f8..e1ac95f 100644
--- a/examples/examples.go
+++ b/examples/examples.go
@@ -904,8 +904,9 @@ func UpdateWebhook(domain, apiKey string) error {
 	return mg.UpdateWebhook(ctx, "clicked", []string{"https://your_domain.com/clicked"})
 }
 
-func VerifyWebhookSignature(domain, apiKey, timestamp, token, signature string) (bool, error) {
+func VerifyWebhookSignature(domain, apiKey, webhookSigningKey, timestamp, token, signature string) (bool, error) {
 	mg := mailgun.NewMailgun(domain, apiKey)
+	mg.SetWebhookSigningKey(webhookSigningKey)
 
 	return mg.VerifyWebhookSignature(mailgun.Signature{
 		TimeStamp: timestamp,
diff --git a/mailgun.go b/mailgun.go
index c77616b..e5c8146 100644
--- a/mailgun.go
+++ b/mailgun.go
@@ -6,11 +6,11 @@
 // For further information please see the Mailgun documentation at
 // http://documentation.mailgun.com/
 //
-//  Original Author: Michael Banzon
-//  Contributions:   Samuel A. Falvo II <sam.falvo %at% rackspace.com>
-//                   Derrick J. Wippler <thrawn01 %at% gmail.com>
+//	Original Author: Michael Banzon
+//	Contributions:   Samuel A. Falvo II <sam.falvo %at% rackspace.com>
+//	                 Derrick J. Wippler <thrawn01 %at% gmail.com>
 //
-// Examples
+// # Examples
 //
 // All functions and method have a corresponding test, so if you don't find an
 // example for a function you'd like to know more about, please check for a
@@ -18,7 +18,7 @@
 // welcome as well. Feel free to submit a pull request or open a Github issue
 // if you cannot find an example to suit your needs.
 //
-// List iterators
+// # List iterators
 //
 // Most methods that begin with `List` return an iterator which simplfies
 // paging through large result sets returned by the mailgun API. Most `List`
@@ -28,23 +28,22 @@
 //
 // For example, the following iterates over all pages of events 100 items at a time
 //
-//  mg := mailgun.NewMailgun("your-domain.com", "your-api-key")
-//  it := mg.ListEvents(&mailgun.ListEventOptions{Limit: 100})
+//	mg := mailgun.NewMailgun("your-domain.com", "your-api-key")
+//	it := mg.ListEvents(&mailgun.ListEventOptions{Limit: 100})
 //
-//  // The entire operation should not take longer than 30 seconds
-//  ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
-//  defer cancel()
+//	// The entire operation should not take longer than 30 seconds
+//	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+//	defer cancel()
 //
-//  // For each page of 100 events
-//  var page []mailgun.Event
-//  for it.Next(ctx, &page) {
-//    for _, e := range page {
-//      // Do something with 'e'
-//    }
-//  }
+//	// For each page of 100 events
+//	var page []mailgun.Event
+//	for it.Next(ctx, &page) {
+//	  for _, e := range page {
+//	    // Do something with 'e'
+//	  }
+//	}
 //
-//
-// License
+// # License
 //
 // Copyright (c) 2013-2019, Michael Banzon.
 // All rights reserved.
@@ -270,12 +269,13 @@ type Mailgun interface {
 // MailgunImpl bundles data needed by a large number of methods in order to interact with the Mailgun API.
 // Colloquially, we refer to instances of this structure as "clients."
 type MailgunImpl struct {
-	apiBase         string
-	domain          string
-	apiKey          string
-	client          *http.Client
-	baseURL         string
-	overrideHeaders map[string]string
+	apiBase           string
+	domain            string
+	apiKey            string
+	webhookSigningKey string
+	client            *http.Client
+	baseURL           string
+	overrideHeaders   map[string]string
 
 	mu                 sync.RWMutex
 	capturedCurlOutput string
@@ -292,7 +292,7 @@ func NewMailgun(domain, apiKey string) *MailgunImpl {
 }
 
 // NewMailgunFromEnv returns a new Mailgun client using the environment variables
-// MG_API_KEY, MG_DOMAIN, and MG_URL
+// MG_API_KEY, MG_DOMAIN, MG_URL, and MG_WEBHOOK_SIGNING_KEY
 func NewMailgunFromEnv() (*MailgunImpl, error) {
 	apiKey := os.Getenv("MG_API_KEY")
 	if apiKey == "" {
@@ -310,6 +310,11 @@ func NewMailgunFromEnv() (*MailgunImpl, error) {
 		mg.SetAPIBase(url)
 	}
 
+	webhookSigningKey := os.Getenv("MG_WEBHOOK_SIGNING_KEY")
+	if webhookSigningKey != "" {
+		mg.SetWebhookSigningKey(webhookSigningKey)
+	}
+
 	return mg, nil
 }
 
@@ -338,6 +343,20 @@ func (mg *MailgunImpl) SetClient(c *http.Client) {
 	mg.client = c
 }
 
+// WebhookSigningKey returns the webhook signing key configured for this client
+func (mg *MailgunImpl) WebhookSigningKey() string {
+	key := mg.webhookSigningKey
+	if key == "" {
+		return mg.APIKey()
+	}
+	return key
+}
+
+// SetWebhookSigningKey updates the webhook signing key for this client
+func (mg *MailgunImpl) SetWebhookSigningKey(webhookSigningKey string) {
+	mg.webhookSigningKey = webhookSigningKey
+}
+
 // SetOnBehalfOfSubaccount sets X-Mailgun-On-Behalf-Of header to SUBACCOUNT_ACCOUNT_ID in order to perform API request
 // on behalf of subaccount.
 func (mg *MailgunImpl) SetOnBehalfOfSubaccount(subaccountId string) {
@@ -350,14 +369,15 @@ func (mg *MailgunImpl) RemoveOnBehalfOfSubaccount() {
 }
 
 // SetAPIBase updates the API Base URL for this client.
-//  // For EU Customers
-//  mg.SetAPIBase(mailgun.APIBaseEU)
 //
-//  // For US Customers
-//  mg.SetAPIBase(mailgun.APIBaseUS)
+//	// For EU Customers
+//	mg.SetAPIBase(mailgun.APIBaseEU)
+//
+//	// For US Customers
+//	mg.SetAPIBase(mailgun.APIBaseUS)
 //
-//  // Set a custom base API
-//  mg.SetAPIBase("https://localhost/v3")
+//	// Set a custom base API
+//	mg.SetAPIBase("https://localhost/v3")
 func (mg *MailgunImpl) SetAPIBase(address string) {
 	mg.apiBase = address
 }
diff --git a/webhooks.go b/webhooks.go
index 175df42..56cda65 100644
--- a/webhooks.go
+++ b/webhooks.go
@@ -123,7 +123,7 @@ type WebhookPayload struct {
 
 // Use this method to parse the webhook signature given as JSON in the webhook response
 func (mg *MailgunImpl) VerifyWebhookSignature(sig Signature) (verified bool, err error) {
-	h := hmac.New(sha256.New, []byte(mg.APIKey()))
+	h := hmac.New(sha256.New, []byte(mg.WebhookSigningKey()))
 
 	_, err = io.WriteString(h, sig.TimeStamp)
 	if err != nil {
@@ -149,7 +149,7 @@ func (mg *MailgunImpl) VerifyWebhookSignature(sig Signature) (verified bool, err
 // Deprecated: Please use the VerifyWebhookSignature() to parse the latest
 // version of WebHooks from mailgun
 func (mg *MailgunImpl) VerifyWebhookRequest(req *http.Request) (verified bool, err error) {
-	h := hmac.New(sha256.New, []byte(mg.APIKey()))
+	h := hmac.New(sha256.New, []byte(mg.WebhookSigningKey()))
 
 	_, err = io.WriteString(h, req.FormValue("timestamp"))
 	if err != nil {


Test patch:
diff --git a/domains_test.go b/domains_test.go
index 03bfba4..9cfb099 100644
--- a/domains_test.go
+++ b/domains_test.go
@@ -11,8 +11,9 @@ import (
 )
 
 const (
-	testDomain = "mailgun.test"
-	testKey    = "api-fake-key"
+	testDomain            = "mailgun.test"
+	testKey               = "api-fake-key"
+	testWebhookSigningKey = "webhook-signing-key"
 )
 
 func TestListDomains(t *testing.T) {
diff --git a/webhooks_test.go b/webhooks_test.go
index 0dcfcaf..add8f8d 100644
--- a/webhooks_test.go
+++ b/webhooks_test.go
@@ -80,9 +80,10 @@ var signedTests = []bool{
 
 func TestVerifyWebhookSignature(t *testing.T) {
 	mg := mailgun.NewMailgun(testDomain, testKey)
+	mg.SetWebhookSigningKey(testWebhookSigningKey)
 
 	for _, v := range signedTests {
-		fields := getSignatureFields(mg.APIKey(), v)
+		fields := getSignatureFields(mg.WebhookSigningKey(), v)
 		sig := mailgun.Signature{
 			TimeStamp: fields["timestamp"],
 			Token:     fields["token"],
@@ -100,9 +101,10 @@ func TestVerifyWebhookSignature(t *testing.T) {
 
 func TestVerifyWebhookRequest_Form(t *testing.T) {
 	mg := mailgun.NewMailgun(testDomain, testKey)
+	mg.SetWebhookSigningKey(testWebhookSigningKey)
 
 	for _, v := range signedTests {
-		fields := getSignatureFields(mg.APIKey(), v)
+		fields := getSignatureFields(mg.WebhookSigningKey(), v)
 		req := buildFormRequest(fields)
 
 		verified, err := mg.VerifyWebhookRequest(req)
@@ -116,9 +118,10 @@ func TestVerifyWebhookRequest_Form(t *testing.T) {
 
 func TestVerifyWebhookRequest_MultipartForm(t *testing.T) {
 	mg := mailgun.NewMailgun(testDomain, testKey)
+	mg.SetWebhookSigningKey(testWebhookSigningKey)
 
 	for _, v := range signedTests {
-		fields := getSignatureFields(mg.APIKey(), v)
+		fields := getSignatureFields(mg.WebhookSigningKey(), v)
 		req := buildMultipartFormRequest(fields)
 
 		verified, err := mg.VerifyWebhookRequest(req)