1
1
/*
2
2
Package signature implements signature verification for MessageBird webhooks.
3
3
4
- To use define a new validator using your MessageBird Signing key. You can use the
5
- ValidRequest method, just pass the request as a parameter :
4
+ To use define a new validator using your MessageBird Signing key. You can use the
5
+ ValidRequest method, just pass the request and base url as parameters :
6
6
7
- validator := signature.NewValidator("your signing key")
8
- if err := validator.ValidRequest(r); err != nil {
7
+ validator := signature.NewValidator([]byte("your signing key"))
8
+ baseUrl := "https://messagebird.io"
9
+ if err := validator.ValidRequest(r, baseUrl); err != nil {
9
10
// handle error
10
11
}
11
12
12
13
Or use the handler as a middleware for your server:
13
14
14
- http.Handle("/path", validator.Validate(YourHandler))
15
+ http.Handle("/path", validator.Validate(YourHandler, baseUrl ))
15
16
16
17
It will reject the requests that contain invalid signatures.
17
- The validator uses a 5ms seconds window to accept requests as valid, to change
18
- this value, set the ValidityWindow to the disired duration.
19
- Take into account that the validity window works around the current time:
20
- [now - ValidityWindow/2, now + ValidityWindow/2]
21
18
*/
22
19
package signature
23
20
24
21
import (
25
22
"bytes"
26
- "crypto/hmac"
27
23
"crypto/sha256"
28
- "encoding/base64 "
24
+ "encoding/hex "
29
25
"fmt"
30
26
"io/ioutil"
31
27
"net/http"
32
28
"net/url"
33
- "strconv"
34
29
"time"
35
- )
36
30
37
- const (
38
- tsHeader = "MessageBird-Request-Timestamp"
39
- sHeader = "MessageBird-Signature"
31
+ "github.com/dgrijalva/jwt-go"
40
32
)
41
33
42
- // ValidityWindow defines the time window in which to validate a request.
43
- var ValidityWindow = 5 * time .Second
34
+ const signatureHeader = "MessageBird-Signature-JWT"
44
35
45
- // StringToTime converts from Unicode Epoch encoded timestamps to the time.Time type.
46
- func stringToTime (s string ) (time.Time , error ) {
47
- sec , err := strconv .ParseInt (s , 10 , 64 )
48
- if err != nil {
49
- return time.Time {}, err
50
- }
51
- return time .Unix (sec , 0 ), nil
36
+ // TimeFunc provides the current time same as time.Now but can be overridden for testing.
37
+ var TimeFunc = time .Now
38
+
39
+ // allowedMethods lists the signing methods that we accept. We only allow symmetric-key
40
+ // algorithms as our customer signing keys are currently all simple byte strings. HMAC is
41
+ // also the only symkey signature method that is required by the RFC7518 Section 3.1 and
42
+ // thus should be supported by all JWT implementations.
43
+ var allowedMethods = []string {
44
+ jwt .SigningMethodHS256 .Name ,
45
+ jwt .SigningMethodHS384 .Name ,
46
+ jwt .SigningMethodHS512 .Name ,
52
47
}
53
48
54
49
// Validator type represents a MessageBird signature validator.
55
50
type Validator struct {
56
- SigningKey string // Signing Key provided by MessageBird.
51
+ SigningKey [] byte // Signing Key provided by MessageBird.
57
52
}
58
53
59
54
// NewValidator returns a signature validator object.
60
- func NewValidator (signingKey string ) * Validator {
55
+ func NewValidator (signingKey [] byte ) * Validator {
61
56
return & Validator {
62
57
SigningKey : signingKey ,
63
58
}
64
59
}
65
60
66
- // validTimestamp validates if the MessageBird-Request-Timestamp is a valid
67
- // date and if the request is older than the validator Period.
68
- func (v * Validator ) validTimestamp (ts string ) bool {
69
- t , err := stringToTime (ts )
70
- if err != nil {
71
- return false
72
- }
73
- diff := time .Now ().Add (ValidityWindow / 2 ).Sub (t )
74
- return diff < ValidityWindow && diff > 0
75
- }
61
+ // ValidSignature is a method that takes care of the signature validation of
62
+ // incoming requests.
63
+ func (v * Validator ) ValidSignature (signature , url string , payload []byte ) error {
64
+ parser := jwt.Parser {ValidMethods : allowedMethods }
65
+ keyFn := func (* jwt.Token ) (interface {}, error ) { return v .SigningKey , nil }
76
66
77
- // calculateSignature calculates the MessageBird-Signature using HMAC_SHA_256
78
- // encoding and the timestamp, query params and body from the request:
79
- // signature = HMAC_SHA_256(
80
- // TIMESTAMP + \n + QUERY_PARAMS + \n + SHA_256_SUM(BODY),
81
- // signing_key)
82
- func (v * Validator ) calculateSignature (ts , qp string , b []byte ) ([]byte , error ) {
83
- var m bytes.Buffer
84
- bh := sha256 .Sum256 (b )
85
- fmt .Fprintf (& m , "%s\n %s\n %s" , ts , qp , bh [:])
86
- mac := hmac .New (sha256 .New , []byte (v .SigningKey ))
87
- if _ , err := mac .Write (m .Bytes ()); err != nil {
88
- return nil , err
67
+ claims := Claims {
68
+ receivedTime : TimeFunc (),
69
+ correctURLHash : sha256Hash ([]byte (url )),
89
70
}
90
- return mac .Sum (nil ), nil
91
- }
92
-
93
- // validSignature takes the timestamp, query params and body from the request,
94
- // calculates the expected signature and compares it to the one sent by MessageBird.
95
- func (v * Validator ) validSignature (ts , rqp string , b []byte , rs string ) bool {
96
- uqp , err := url .Parse ("?" + rqp )
97
- if err != nil {
98
- return false
71
+ if payload != nil && len (payload ) != 0 {
72
+ claims .correctPayloadHash = sha256Hash (payload )
99
73
}
100
- es , err := v .calculateSignature (ts , uqp .Query ().Encode (), b )
101
- if err != nil {
102
- return false
103
- }
104
- drs , err := base64 .StdEncoding .DecodeString (rs )
105
- if err != nil {
106
- return false
74
+
75
+ if _ , err := parser .ParseWithClaims (signature , & claims , keyFn ); err != nil {
76
+ return fmt .Errorf ("invalid jwt: %w" , err )
107
77
}
108
- return hmac .Equal (drs , es )
78
+
79
+ return nil
109
80
}
110
81
111
82
// ValidRequest is a method that takes care of the signature validation of
112
83
// incoming requests.
113
- func (v * Validator ) ValidRequest (r * http.Request ) error {
114
- ts := r .Header .Get (tsHeader )
115
- rs := r .Header .Get (sHeader )
116
- if ts == "" || rs == "" {
117
- return fmt .Errorf ("Unknown host: %s" , r .Host )
84
+ func (v * Validator ) ValidRequest (r * http.Request , baseUrl string ) error {
85
+ base , err := url .Parse (baseUrl )
86
+ if err != nil {
87
+ return fmt .Errorf ("error parsing base url: %v" , err )
88
+ }
89
+ signature := r .Header .Get (signatureHeader )
90
+ if signature == "" {
91
+ return fmt .Errorf ("signature not found" )
118
92
}
119
93
b , _ := ioutil .ReadAll (r .Body )
120
- if ! v . validTimestamp ( ts ) || ! v . validSignature ( ts , r .URL . RawQuery , b , rs ) {
121
- return fmt .Errorf ("Unknown host : %s" , r . Host )
94
+ if err := v . ValidSignature ( signature , base . ResolveReference ( r .URL ). String () , b ); err != nil {
95
+ return fmt .Errorf ("invalid signature : %s" , err . Error () )
122
96
}
123
97
r .Body = ioutil .NopCloser (bytes .NewBuffer (b ))
124
98
return nil
@@ -127,12 +101,21 @@ func (v *Validator) ValidRequest(r *http.Request) error {
127
101
// Validate is a handler wrapper that takes care of the signature validation of
128
102
// incoming requests and rejects them if invalid or pass them on to your handler
129
103
// otherwise.
130
- func (v * Validator ) Validate (h http.Handler ) http.Handler {
104
+ func (v * Validator ) Validate (h http.Handler , baseUrl string ) http.Handler {
131
105
return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
132
- if err := v .ValidRequest (r ); err != nil {
106
+ if err := v .ValidRequest (r , baseUrl ); err != nil {
133
107
http .Error (w , "" , http .StatusUnauthorized )
134
108
return
135
109
}
136
110
h .ServeHTTP (w , r )
137
111
})
138
112
}
113
+
114
+ func sha256Hash (data []byte ) string {
115
+ if data == nil {
116
+ return ""
117
+ }
118
+
119
+ h := sha256 .Sum256 (data )
120
+ return hex .EncodeToString (h [:])
121
+ }
0 commit comments