Skip to content

Commit 9e46bb9

Browse files
author
marcel corso gonzalez
authored
Merge pull request #109 from nqkdev/master
Support new webhook signature method
2 parents cb04299 + dcc6ce7 commit 9e46bb9

File tree

7 files changed

+803
-2
lines changed

7 files changed

+803
-2
lines changed

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ module github.com/messagebird/go-rest-api/v7
22

33
go 1.14
44

5-
require github.com/stretchr/testify v1.7.0
5+
require (
6+
github.com/golang-jwt/jwt v3.2.1+incompatible
7+
github.com/stretchr/testify v1.7.0
8+
)

go.sum

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
22
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
4+
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
35
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
46
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5-
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
67
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
78
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
89
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

signature/signature.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/*
22
Package signature implements signature verification for MessageBird webhooks.
33
4+
Deprecated: package signature is no longer supported. Use package
5+
signature_jwt instead.
6+
47
To use define a new validator using your MessageBird Signing key. You can use the
58
ValidRequest method, just pass the request as a parameter:
69
@@ -57,6 +60,7 @@ type Validator struct {
5760
}
5861

5962
// NewValidator returns a signature validator object.
63+
// Deprecated: Use signature_jwt.NewValidator(string) instead.
6064
func NewValidator(signingKey string) *Validator {
6165
return &Validator{
6266
SigningKey: signingKey,
@@ -110,6 +114,7 @@ func (v *Validator) validSignature(ts, rqp string, b []byte, rs string) bool {
110114

111115
// ValidRequest is a method that takes care of the signature validation of
112116
// incoming requests.
117+
// Deprecated: Use signature_jwt.Validator.ValidateSignature(*http.Request, string) instead.
113118
func (v *Validator) ValidRequest(r *http.Request) error {
114119
ts := r.Header.Get(tsHeader)
115120
rs := r.Header.Get(sHeader)
@@ -127,6 +132,7 @@ func (v *Validator) ValidRequest(r *http.Request) error {
127132
// Validate is a handler wrapper that takes care of the signature validation of
128133
// incoming requests and rejects them if invalid or pass them on to your handler
129134
// otherwise.
135+
// Deprecated: Use signature_jwt.Validator.Validate(http.Handler) instead.
130136
func (v *Validator) Validate(h http.Handler) http.Handler {
131137
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
132138
if err := v.ValidRequest(r); err != nil {

signature_jwt/claims.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package signature_jwt
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
8+
"github.com/golang-jwt/jwt"
9+
)
10+
11+
// maxSkew is the maximum time skew that we accept. Sometimes the Internet is *so* fast
12+
// that messages are received before they are sent, or the clocks of two servers are not
13+
// in-sync, whichever cause seems more likely to you.
14+
const maxSkew = 1 * time.Second
15+
16+
// Claims replaces jwt.StandardClaims as it checks all aspects of the the JWT token that
17+
// have been specified by the MessageBird RFC.
18+
type Claims struct {
19+
// The following 3 fields are added to Claims before JWT is parsed so that the
20+
// immediately following call to Valid() by jwt-go has *all* necessary information to
21+
// determine whether JWT is valid. These fields should not be overwritten by JSON
22+
// unmarshal.
23+
receivedTime time.Time
24+
correctPayloadHash string
25+
correctURLHash string
26+
skipURLValidation bool
27+
28+
Issuer string `json:"iss"`
29+
NotBefore int64 `json:"nbf"`
30+
ExpirationTime int64 `json:"exp"`
31+
JWTID string `json:"jti"`
32+
URLHash string `json:"url_hash"`
33+
PayloadHash string `json:"payload_hash,omitempty"`
34+
}
35+
36+
// Valid is called by jwt-go after the Claims struct has been filled. If an error is
37+
// returned, it means that the JWT should not be trusted.
38+
func (c Claims) Valid() error {
39+
var errs []string
40+
41+
if c.Issuer != "MessageBird" {
42+
errs = append(errs, "claim iss has wrong value")
43+
}
44+
45+
if iat := time.Unix(c.NotBefore, int64(c.receivedTime.Nanosecond())).Add(-maxSkew); c.receivedTime.Before(iat) {
46+
errs = append(errs, "claim nbf is in the future")
47+
}
48+
49+
if exp := time.Unix(c.ExpirationTime, int64(c.receivedTime.Nanosecond())).Add(maxSkew); c.receivedTime.After(exp) {
50+
errs = append(errs, "claim exp is in the past")
51+
}
52+
53+
if c.JWTID == "" {
54+
errs = append(errs, "claim jti is empty or missing")
55+
}
56+
57+
if !c.skipURLValidation && c.correctURLHash != c.URLHash {
58+
errs = append(errs, "claim url_hash is invalid")
59+
}
60+
61+
switch {
62+
case c.correctPayloadHash == "" && c.PayloadHash != "":
63+
errs = append(errs, "claim payload_hash is set but actual payload is missing")
64+
case c.correctPayloadHash != "" && c.PayloadHash == "":
65+
errs = append(errs, "claim payload_hash is not set but payload is present")
66+
case c.correctPayloadHash != c.PayloadHash:
67+
errs = append(errs, "claim payload_hash is invalid")
68+
}
69+
70+
if len(errs) == 0 {
71+
return nil
72+
}
73+
return fmt.Errorf("%s", strings.Join(errs, "; "))
74+
}
75+
76+
// Claims satisfies jwt.Claims.
77+
var _ jwt.Claims = Claims{}

signature_jwt/signature.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
Package signature_jwt implements signature verification for MessageBird webhooks.
3+
4+
To use define a new validator using your MessageBird Signing key. Can be
5+
retrieved from https://dashboard.messagebird.com/developers/settings.
6+
This is NOT your API key.
7+
8+
You can use the ValidateRequest method, just pass the request and base url as parameters:
9+
10+
validator := signature_jwt.NewValidator([]byte("your signing key"))
11+
baseUrl := "https://yourdomain.com"
12+
if err := validator.ValidateRequest(r, baseUrl); err != nil {
13+
// handle error
14+
}
15+
16+
Or use the handler as a middleware for your server:
17+
18+
http.Handle("/path", validator.Validate(YourHandler, baseUrl))
19+
20+
It will reject the requests that contain invalid signatures.
21+
22+
For more information, see https://developers.messagebird.com/docs/verify-http-requests
23+
*/
24+
package signature_jwt
25+
26+
import (
27+
"bytes"
28+
"crypto/sha256"
29+
"encoding/hex"
30+
"fmt"
31+
"io/ioutil"
32+
"net/http"
33+
"net/url"
34+
"time"
35+
36+
"github.com/golang-jwt/jwt"
37+
)
38+
39+
const signatureHeader = "MessageBird-Signature-JWT"
40+
41+
// TimeFunc provides the current time same as time.Now but can be overridden for testing.
42+
var TimeFunc = time.Now
43+
44+
// allowedMethods lists the signing methods that we accept. We only allow symmetric-key
45+
// algorithms as our customer signing keys are currently all simple byte strings. HMAC is
46+
// also the only symkey signature method that is required by the RFC7518 Section 3.1 and
47+
// thus should be supported by all JWT implementations.
48+
var allowedMethods = []string{
49+
jwt.SigningMethodHS256.Name,
50+
jwt.SigningMethodHS384.Name,
51+
jwt.SigningMethodHS512.Name,
52+
}
53+
54+
// Validator type represents a MessageBird signature validator.
55+
type Validator struct {
56+
parser jwt.Parser
57+
keyFn jwt.Keyfunc
58+
59+
skipURLValidation bool
60+
}
61+
62+
type ValidatorOption func(*Validator)
63+
64+
// SkipURLValidation instructs Validator to not validate url_hash claim.
65+
// It is recommended to not skip URL validation to ensure high security.
66+
// but the ability to skip URL validation is necessary in some cases, e.g.
67+
// your service is behind proxy or when you want to validate it yourself.
68+
// Note that if enabled, no query parameters should be trusted.
69+
func SkipURLValidation() ValidatorOption {
70+
return func(c *Validator) {
71+
c.skipURLValidation = true
72+
}
73+
}
74+
75+
// NewValidator returns a signature validator object.
76+
// Signing key can be retrieved from
77+
// https://dashboard.messagebird.com/developers/settings.
78+
// Note that this is NOT your API key.
79+
func NewValidator(signingKey string, opts ...ValidatorOption) *Validator {
80+
validator := &Validator{
81+
parser: jwt.Parser{
82+
ValidMethods: allowedMethods,
83+
},
84+
keyFn: func(*jwt.Token) (interface{}, error) { return []byte(signingKey), nil },
85+
}
86+
87+
for _, opt := range opts {
88+
opt(validator)
89+
}
90+
91+
return validator
92+
}
93+
94+
// ValidateSignature returns the signature token claims when the signature
95+
// is validated successfully. Otherwise, an error is returned.
96+
// The provided url is the raw url including the protocol, hostname and
97+
// query string, e.g. https://example.com/?example=42.
98+
func (v *Validator) ValidateSignature(signature, url string, payload []byte) (jwt.Claims, error) {
99+
claims := Claims{
100+
receivedTime: TimeFunc(),
101+
skipURLValidation: v.skipURLValidation,
102+
}
103+
104+
if !v.skipURLValidation && url != "" {
105+
claims.correctURLHash = sha256Hash([]byte(url))
106+
}
107+
if payload != nil && len(payload) != 0 {
108+
claims.correctPayloadHash = sha256Hash(payload)
109+
}
110+
111+
if token, err := v.parser.ParseWithClaims(signature, &claims, v.keyFn); err != nil {
112+
return nil, fmt.Errorf("invalid jwt: %w", err)
113+
} else {
114+
return token.Claims, nil
115+
}
116+
}
117+
118+
// ValidateRequest is a method that takes care of the signature validation of
119+
// incoming requests.
120+
func (v *Validator) ValidateRequest(r *http.Request, baseURL string) error {
121+
signature := r.Header.Get(signatureHeader)
122+
if signature == "" {
123+
return fmt.Errorf("signature not found")
124+
}
125+
126+
var fullURL string
127+
if !v.skipURLValidation && baseURL != "" {
128+
base, err := url.Parse(baseURL)
129+
if err != nil {
130+
return fmt.Errorf("error parsing base url: %v", err)
131+
}
132+
fullURL = base.ResolveReference(r.URL).String()
133+
}
134+
135+
b, _ := ioutil.ReadAll(r.Body)
136+
if _, err := v.ValidateSignature(signature, fullURL, b); err != nil {
137+
return fmt.Errorf("invalid signature: %s", err.Error())
138+
}
139+
r.Body = ioutil.NopCloser(bytes.NewBuffer(b))
140+
return nil
141+
}
142+
143+
// Validate is a handler wrapper that takes care of the signature validation of
144+
// incoming requests and rejects them if invalid or pass them on to your handler
145+
// otherwise.
146+
func (v *Validator) Validate(h http.Handler, baseURL string) http.Handler {
147+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
148+
if err := v.ValidateRequest(r, baseURL); err != nil {
149+
http.Error(w, "", http.StatusUnauthorized)
150+
return
151+
}
152+
h.ServeHTTP(w, r)
153+
})
154+
}
155+
156+
func sha256Hash(data []byte) string {
157+
h := sha256.Sum256(data)
158+
return hex.EncodeToString(h[:])
159+
}

0 commit comments

Comments
 (0)