forked from orbcorp/orb-go
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebhook.go
More file actions
138 lines (119 loc) · 4.34 KB
/
webhook.go
File metadata and controls
138 lines (119 loc) · 4.34 KB
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// File generated from our OpenAPI spec by Stainless.
package orb
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/orbcorp/orb-go/internal/requestconfig"
"github.com/orbcorp/orb-go/option"
)
// WebhookHeaderTimestampFormat is the format of the header X-Orb-Timestamp for webhook requests sent by Orb.
const WebhookHeaderTimestampFormat = "2006-01-02T15:04:05.999999999"
// WebhookService contains methods and other services that help with interacting
// with the Orb API. Note, unlike clients, this service does not read variables
// from the environment automatically. You should not instantiate this service
// directly, and instead use the [NewWebhookService] method instead.
type WebhookService struct {
Options []option.RequestOption
// webhookSecret is the secret defined at the client level
webhookSecret string
}
// NewWebhookService generates a new service that applies the given options to each
// request. These options are applied after the parent client's options (if there
// is one), and before any request-specific options.
func NewWebhookService(opts ...option.RequestOption) (r *WebhookService) {
r = &WebhookService{}
r.Options = opts
// This is a dummy response object. We need to build a request config to be able to check the webhook secret defined
// at the client level.
_res := struct{}{}
cfg, err := requestconfig.NewRequestConfig(context.TODO(), http.MethodPost, "/webhooks", nil, &_res, opts...)
if err != nil {
panic(err)
}
if cfg.WebhookSecret != "" {
r.webhookSecret = cfg.WebhookSecret
}
return
}
// Validates whether or not the webhook payload was sent by Orb. Pass an empty string to use the secret defined at the
// client level.
//
// An error will be raised if the webhook payload was not sent by Orb.
func (r *WebhookService) VerifySignature(payload []byte, headers http.Header, secret string, now time.Time) (err error) {
return r.verifySignatureImpl(payload, headers, secret, now, 5*time.Minute)
}
// Identical to VerifySignature, but allows you to pass in a WebhookVerifySignatureParams struct to specify extra
// parameters such as the tolerance for X-Orb-Timestamp.
func (r *WebhookService) VerifySignatureWithParams(params WebhookVerifySignatureParams) (err error) {
if params.Now.IsZero() {
params.Now = time.Now()
}
if params.Tolerance == 0 {
params.Tolerance = 5 * time.Minute
}
return r.verifySignatureImpl(params.Payload, params.Headers, params.Secret, params.Now, params.Tolerance)
}
func (r *WebhookService) verifySignatureImpl(payload []byte, headers http.Header, secret string, now time.Time, tolerance time.Duration) (err error) {
if secret == "" {
secret = r.webhookSecret
}
if secret == "" {
return errors.New("The webhook secret must either be set using the env var, ORB_WEBHOOK_SECRET, on the client class, orb.NewClient(option.WithWebhookSecret(\"123\")}), or passed to this function")
}
msgSignature := headers.Values("X-Orb-Signature")
if len(msgSignature) == 0 {
return errors.New("could not find X-Orb-Signature header")
}
msgTimestamp := headers.Get("X-Orb-Timestamp")
if len(msgTimestamp) == 0 {
return errors.New("could not find X-Orb-Timestamp header")
}
timestamp, err := time.Parse(WebhookHeaderTimestampFormat, msgTimestamp)
if err != nil {
return fmt.Errorf("invalid timestamp headers: %s", err)
}
if timestamp.Before(now.Add(-tolerance)) {
return fmt.Errorf("value from X-Orb-Timestamp header too old, tolerance=-%s", tolerance.String())
}
if timestamp.After(now.Add(tolerance)) {
return fmt.Errorf("value from X-Orb-Timestamp header too new, tolerance=%s", tolerance.String())
}
secretBytes := []byte(secret)
mac := hmac.New(sha256.New, secretBytes)
mac.Write([]byte("v1:"))
mac.Write([]byte(msgTimestamp))
mac.Write([]byte(":"))
mac.Write(payload)
expected := mac.Sum(nil)
for _, part := range msgSignature {
parts := strings.Split(part, "=")
if len(parts) != 2 {
continue
}
if parts[0] != "v1" {
continue
}
signature, err := hex.DecodeString(parts[1])
if err != nil {
continue
}
if hmac.Equal(signature, expected) {
return nil
}
}
return errors.New("None of the given webhook signatures match the expected signature")
}
type WebhookVerifySignatureParams struct {
Payload []byte
Headers http.Header
Secret string
Now time.Time
Tolerance time.Duration
}