From fdec610681f23406a7ef6fc586b45baaa21e9511 Mon Sep 17 00:00:00 2001 From: Derek Garnett Date: Thu, 28 Nov 2024 11:08:06 -0600 Subject: [PATCH] Fix webhook signature verification to use the webhook signing secret (#351) 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! --- README.md | 1 + domains_test.go | 5 +-- examples/examples.go | 3 +- mailgun.go | 84 +++++++++++++++++++++++++++----------------- webhooks.go | 4 +-- webhooks_test.go | 9 +++-- 6 files changed, 66 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 2afda084..cb985f03 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/domains_test.go b/domains_test.go index 03bfba46..9cfb0993 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/examples/examples.go b/examples/examples.go index c3f04f8a..e1ac95fd 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 c77616b7..e5c81461 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 -// Derrick J. Wippler +// Original Author: Michael Banzon +// Contributions: Samuel A. Falvo II +// Derrick J. Wippler // -// 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 175df421..56cda65b 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 { diff --git a/webhooks_test.go b/webhooks_test.go index 0dcfcaf2..add8f8df 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)