-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathwebhook_verifier.go
100 lines (80 loc) · 2.53 KB
/
webhook_verifier.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package paddle
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"net/http"
"regexp"
)
var (
// ErrMissingSignature is returned when the signature is missing.
ErrMissingSignature = errors.New("missing signature")
// ErrInvalidSignatureFormat is returned when the signature format is invalid.
ErrInvalidSignatureFormat = errors.New("invalid signature format")
)
// signatureRegexp matches the Paddle-Signature header format, e.g.:
//
// ts=1671552777;h1=eb4d0dc8853be92b7f063b9f3ba5233eb920a09459b6e6b2c26705b4364db151
//
// More information can be found here: https://developer.paddle.com/webhooks/signature-verification.
var signatureRegexp = regexp.MustCompile(`^ts=(\d+);h1=(\w+)$`)
// WebhookVerifier is used to verify webhook requests from Paddle.
type WebhookVerifier struct {
secretKey []byte
}
// NewWebhookVerifier creates a new WebhookVerifier with the given secret key.
func NewWebhookVerifier(secretKey string) *WebhookVerifier {
return &WebhookVerifier{secretKey: []byte(secretKey)}
}
// Verify verifies the signature of a webhook request.
func (wv *WebhookVerifier) Verify(req *http.Request) (bool, error) {
sig := req.Header.Get("Paddle-Signature")
if sig == "" {
return false, ErrMissingSignature
}
matches := signatureRegexp.FindAllStringSubmatch(sig, -1)
if len(matches) != 1 || len(matches[0]) != 3 {
return false, ErrInvalidSignatureFormat
}
ts := matches[0][1]
h1 := matches[0][2]
const maxBodySize = 2 << 20 // 2 MB
req.Body = http.MaxBytesReader(nil, req.Body, maxBodySize)
body, err := io.ReadAll(req.Body)
if err != nil {
return false, err
}
req.Body = io.NopCloser(bytes.NewBuffer(body))
mac := hmac.New(sha256.New, wv.secretKey)
mac.Write([]byte(ts))
mac.Write([]byte(":"))
mac.Write(body)
generatedMAC := mac.Sum(nil)
dst, err := hex.DecodeString(h1)
if err != nil {
return false, err
}
return hmac.Equal(dst, generatedMAC), nil
}
// Middleware returns a middleware that verifies the signature of a webhook
// request.
func (wv *WebhookVerifier) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ok, err := wv.Verify(r)
if err != nil && (errors.Is(err, ErrMissingSignature) || errors.Is(err, ErrInvalidSignatureFormat)) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !ok {
http.Error(w, "signature mismatch", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}