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
  • Loading branch information
drkgrntt committed Nov 27, 2024
1 parent f7886ae commit f10029a
Show file tree
Hide file tree
Showing 6 changed files with 72 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
80 changes: 48 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,16 @@ 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 {
return mg.webhookSigningKey
}

// 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 +365,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
14 changes: 12 additions & 2 deletions webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,12 @@ 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()))
key := mg.WebhookSigningKey()
// For backwards compatibility, fall back to the api key
if key == "" {
key = mg.APIKey()
}
h := hmac.New(sha256.New, []byte(key))

_, err = io.WriteString(h, sig.TimeStamp)
if err != nil {
Expand All @@ -149,7 +154,12 @@ 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()))
key := mg.WebhookSigningKey()
// For backwards compatibility, fall back to the api key
if key == "" {
key = mg.APIKey()
}
h := hmac.New(sha256.New, []byte(key))

_, 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 f10029a

Please sign in to comment.