Created
November 28, 2019 16:28
-
-
Save amlwwalker/3213e01424364425748446e43542d5b6 to your computer and use it in GitHub Desktop.
Validating Nexmo Signatures on incoming webhooks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package nexmo | |
// Config holds any values required for dispatching SMS messages to Nexmo | |
type Config struct { | |
Creds *Credentials | |
NexmoWorkers int `env:"NEXMO_WORKERS" envDefault:"5"` | |
NexmoURL string `env:"NEXMO_URL" envDefault:"https://rest.nexmo.com/sms/json"` | |
NexmoSecretFile string `env:"NEXMO_CONFIG_FILE" envDefault:"/secrets/nexmo/nexmo.json"` | |
} | |
// Credentials holds the values required for interacting with Nexmo | |
type Credentials struct { | |
Secret string `json:"api_secret"` | |
Key string `json:"api_key"` | |
Origin string `json:"origin"` | |
SignatureSecret string `json:"signature_secret"` | |
} | |
type NexmoDeliveryReceipt struct { | |
ErrorCode string `json:"err-code"` | |
APIKey string `json:"api-key"` | |
TimeStamp string `json:"timestamp"` | |
MessageTimestamp string `json:"message-timestamp"` | |
MessageID string `json:"messageId"` | |
MSISDN string `json:"msisdn"` | |
NetworkCode string `json:"network-code"` | |
Signature string `json:"sig"` | |
Nonce string `json:"nonce"` | |
Price string `json:"price"` | |
Scts string `json:"scts"` | |
Status string `json:"status"` | |
To string `json:"to"` | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package nexmo | |
import ( | |
"crypto/hmac" | |
"crypto/sha256" | |
"encoding/hex" | |
"fmt" | |
"sort" | |
"strings" | |
"github.com/fatih/structs" | |
) | |
// This function checks that the body's signature matches the signature in the body. | |
// hash method in nexmo is set to SHA256 HMAC | |
func GenerateIncomingSignature(config *Config, parameters NexmoDeliveryReceipt) string { | |
m := structs.Map(parameters) // make a map of the keys of the object so we can sort by key | |
s := structs.New(parameters) // create an indexed struct from the object | |
var nexmoString string //store the final string | |
results := make(map[string]string) | |
for i, _ := range m { | |
name := s.Field(i) //get the field from the object | |
value := name.Value().(string) | |
tagValue := name.Tag("json") | |
tagValue = strings.Replace(tagValue, "&", "_", -1) //retrieve the name according to the json tags | |
tagValue = strings.Replace(tagValue, "=", "_", -1) //could be done with a regex but meh. | |
value = strings.Replace(value, "&", "_", -1) | |
value = strings.Replace(value, "=", "_", -1) //could be done with a regex but meh. | |
if tagValue != "-" && tagValue != "sig" { | |
results[tagValue] = value | |
} | |
} | |
//now convert the map to a new array for sorting | |
var notSorted []string | |
for k := range results { | |
notSorted = append(notSorted, k) | |
} | |
//finally, sort | |
sort.Strings(notSorted) | |
//and generate the string | |
for _, k := range notSorted { | |
nexmoString += "&" + fmt.Sprintf("%s=%v", k, results[k]) | |
} | |
// Create a new HMAC by defining the hash type and the key (as byte array) | |
h := hmac.New(sha256.New, []byte(config.Creds.SignatureSecret)) | |
// Write Data to it | |
h.Write([]byte(nexmoString)) | |
// Get result and encode as hexadecimal string | |
sha := hex.EncodeToString(h.Sum(nil)) | |
//sha can now be used as the signature if we want to verify our requests to nexmo | |
return strings.ToUpper(sha) //the result comes back upper case | |
} | |
func ValidateRequest(config *Config, receipt NexmoDeliveryReceipt) bool { | |
validSignature := GenerateIncomingSignature(config, receipt) | |
fmt.Println(validSignature, receipt.Signature) | |
return validSignature == receipt.Signature | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package nexmo | |
import ( | |
"testing" | |
) | |
func TestDeliveryReceiptAuthenticateIncomingRequest(t *testing.T) { | |
testConfig := &Config{ | |
Creds: &Credentials{ | |
Secret: "NexmoSecret", | |
Key: "NexmoKey", | |
Origin: "TestSender", | |
SignatureSecret: "my_secret_key_for_testing", | |
}, | |
NexmoWorkers: 5, | |
NexmoURL: "http://nexmo.example.local/sms", | |
NexmoSecretFile: "", | |
} | |
t.Run("Should generate a SHA256 HASH that matches the expected value. Tests the function directly", func(t *testing.T) { | |
deliveryReceipt := NexmoDeliveryReceipt{ | |
MessageTimestamp: "2019-11-28 12:30:07", | |
ErrorCode: "0", | |
APIKey: "ff4df137", | |
TimeStamp: "1574944796", | |
Nonce: "c8b92f47-8f68-445b-a299-fce7035d0b92", | |
MessageID: "1500000088E2D6F9", | |
MSISDN: "447725698433", | |
NetworkCode: "23431", | |
Price: "0.03330000", | |
Scts: "1911281330", | |
Status: "delivered", | |
To: "447520632433", | |
} | |
SignatureToTest := "EFCCB9935B8A55DFC61EDDBAA278C012C743D8DFFE93EAD8F887F870228F7161" | |
deliveryReceipt.Signature = SignatureToTest | |
hmacSHA256Hash := GenerateIncomingSignature(testConfig, deliveryReceipt) | |
if hmacSHA256Hash != deliveryReceipt.Signature { | |
t.FailNow() | |
} | |
}) | |
t.Run("Should generate a SHA256 HASH that matches the expected value. Testing replacing & and = symbols. Tests using the helper function", func(t *testing.T) { | |
deliveryReceipt := NexmoDeliveryReceipt{ | |
MessageTimestamp: "2019-11-28 12:30:07", | |
ErrorCode: "0", | |
APIKey: "ff4df137", | |
TimeStamp: "1574944796", | |
Nonce: "c8b&&92f47-8f68-445b-a299-fce7035d&&0b92", | |
MessageID: "1500=00&00=88E2D6F9", | |
MSISDN: "447725698433", | |
NetworkCode: "23431", | |
Price: "0.03330000", | |
Scts: "1911281330", | |
Status: "deli&&vered", | |
To: "447520632433", | |
} | |
SignatureToTest := "D860AC7C025BD3D72B0537CBB5D6E9C1D76E04563F4FBA5F9EE8461F6631C110" | |
deliveryReceipt.Signature = SignatureToTest | |
if !ValidateRequest(testConfig, deliveryReceipt) { | |
t.FailNow() | |
} | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment