Skip to content

Commit 4ae9045

Browse files
authored
Merge pull request #60 from Juanadelacuesta/ENG-10
Eng 10 As a Go developer I want the Messagebird API requests to be signed
2 parents 29552d9 + f45487a commit 4ae9045

File tree

2 files changed

+400
-0
lines changed

2 files changed

+400
-0
lines changed

signature/signature.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
Package signature implements signature verification for MessageBird webhooks.
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:
6+
7+
validator := signature.NewValidator("your signing key")
8+
if err := validator.ValidRequest(r); err != nil {
9+
// handle error
10+
}
11+
12+
Or use the handler as a middleware for your server:
13+
14+
http.Handle("/path", validator.Validate(YourHandler))
15+
16+
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+
*/
22+
package signature
23+
24+
import (
25+
"bytes"
26+
"crypto/hmac"
27+
"crypto/sha256"
28+
"encoding/base64"
29+
"fmt"
30+
"io/ioutil"
31+
"net/http"
32+
"net/url"
33+
"strconv"
34+
"time"
35+
)
36+
37+
const (
38+
tsHeader = "MessageBird-Request-Timestamp"
39+
sHeader = "MessageBird-Signature"
40+
)
41+
42+
// ValidityWindow defines the time window in which to validate a request.
43+
var ValidityWindow = 5 * time.Second
44+
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
52+
}
53+
54+
// Validator type represents a MessageBird signature validator.
55+
type Validator struct {
56+
SigningKey string // Signing Key provided by MessageBird.
57+
}
58+
59+
// NewValidator returns a signature validator object.
60+
func NewValidator(signingKey string) *Validator {
61+
return &Validator{
62+
SigningKey: signingKey,
63+
}
64+
}
65+
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+
}
76+
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
89+
}
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
99+
}
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
107+
}
108+
return hmac.Equal(drs, es)
109+
}
110+
111+
// ValidRequest is a method that takes care of the signature validation of
112+
// 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)
118+
}
119+
b, _ := ioutil.ReadAll(r.Body)
120+
if v.validTimestamp(ts) == false || v.validSignature(ts, r.URL.RawQuery, b, rs) == false {
121+
return fmt.Errorf("Unknown host: %s", r.Host)
122+
}
123+
r.Body = ioutil.NopCloser(bytes.NewBuffer(b))
124+
return nil
125+
}
126+
127+
// Validate is a handler wrapper that takes care of the signature validation of
128+
// incoming requests and rejects them if invalid or pass them on to your handler
129+
// otherwise.
130+
func (v *Validator) Validate(h http.Handler) http.Handler {
131+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
132+
if err := v.ValidRequest(r); err != nil {
133+
http.Error(w, "", http.StatusUnauthorized)
134+
return
135+
}
136+
h.ServeHTTP(w, r)
137+
})
138+
}

0 commit comments

Comments
 (0)