Skip to content

Commit

Permalink
Fix webhook signature verification to use the webhook signing secret (#…
Browse files Browse the repository at this point in the history
…351)

Since [the other PR](#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!
  • Loading branch information
drkgrntt authored Nov 28, 2024
1 parent f7886ae commit fdec610
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 40 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down
5 changes: 3 additions & 2 deletions domains_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion examples/examples.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
84 changes: 52 additions & 32 deletions mailgun.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@
// 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
// corresponding test. Of course, contributions to the documentation are always
// 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`
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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 == "" {
Expand All @@ -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
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
9 changes: 6 additions & 3 deletions webhooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit fdec610

Please sign in to comment.