github.com/stripe/stripe-go/v76@v76.25.0/webhook/client.go (about) 1 package webhook 2 3 import ( 4 "crypto/hmac" 5 "crypto/sha256" 6 "encoding/hex" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/stripe/stripe-go/v76" 15 ) 16 17 // 18 // Public constants 19 // 20 21 const ( 22 // DefaultTolerance indicates that signatures older than this will be rejected by ConstructEvent. 23 DefaultTolerance time.Duration = 300 * time.Second 24 // signingVersion represents the version of the signature we currently use. 25 signingVersion string = "v1" 26 ) 27 28 // 29 // Public variables 30 // 31 32 // This block represents the list of errors that could be raised when using the webhook package. 33 var ( 34 ErrInvalidHeader = errors.New("webhook has invalid Stripe-Signature header") 35 ErrNoValidSignature = errors.New("webhook had no valid signature") 36 ErrNotSigned = errors.New("webhook has no Stripe-Signature header") 37 ErrTooOld = errors.New("timestamp wasn't within tolerance") 38 ) 39 40 // 41 // Public functions 42 // 43 44 // ComputeSignature computes a webhook signature using Stripe's v1 signing 45 // method. 46 // 47 // See https://stripe.com/docs/webhooks#signatures for more information. 48 func ComputeSignature(t time.Time, payload []byte, secret string) []byte { 49 mac := hmac.New(sha256.New, []byte(secret)) 50 mac.Write([]byte(fmt.Sprintf("%d", t.Unix()))) 51 mac.Write([]byte(".")) 52 mac.Write(payload) 53 return mac.Sum(nil) 54 } 55 56 // ConstructEvent initializes an Event object from a JSON webhook payload, validating 57 // the Stripe-Signature header using the specified signing secret. Returns an error 58 // if the body or Stripe-Signature header provided are unreadable, if the 59 // signature doesn't match, or if the timestamp for the signature is older than 60 // DefaultTolerance. 61 // 62 // NOTE: Stripe will only send Webhook signing headers after you have retrieved 63 // your signing secret from the Stripe dashboard: 64 // https://dashboard.stripe.com/webhooks 65 // 66 // This will return an error if the event API version does not match the 67 // stripe.APIVersion constant. 68 func ConstructEvent(payload []byte, header string, secret string) (stripe.Event, error) { 69 return ConstructEventWithTolerance(payload, header, secret, DefaultTolerance) 70 } 71 72 // ConstructEventIgnoringTolerance initializes an Event object from a JSON webhook 73 // payload, validating the Stripe-Signature header using the specified signing secret. 74 // Returns an error if the body or Stripe-Signature header provided are unreadable or 75 // if the signature doesn't match. Does not check the signature's timestamp. 76 // 77 // NOTE: Stripe will only send Webhook signing headers after you have retrieved 78 // your signing secret from the Stripe dashboard: 79 // https://dashboard.stripe.com/webhooks 80 // 81 // This will return an error if the event API version does not match the 82 // stripe.APIVersion constant. 83 func ConstructEventIgnoringTolerance(payload []byte, header string, secret string) (stripe.Event, error) { 84 return constructEvent(payload, header, secret, ConstructEventOptions{IgnoreTolerance: true}) 85 } 86 87 // ConstructEventWithTolerance initializes an Event object from a JSON webhook payload, 88 // validating the signature in the Stripe-Signature header using the specified signing 89 // secret and tolerance window. Returns an error if the body or Stripe-Signature header 90 // provided are unreadable, if the signature doesn't match, or if the timestamp 91 // for the signature is older than the specified tolerance. 92 // 93 // NOTE: Stripe will only send Webhook signing headers after you have retrieved 94 // your signing secret from the Stripe dashboard: 95 // https://dashboard.stripe.com/webhooks 96 // 97 // This will return an error if the event API version does not match the 98 // stripe.APIVersion constant. 99 func ConstructEventWithTolerance(payload []byte, header string, secret string, tolerance time.Duration) (stripe.Event, error) { 100 return constructEvent(payload, header, secret, ConstructEventOptions{Tolerance: tolerance}) 101 } 102 103 // ConstructEventWithOptions initializes an Event object from a JSON webhook payload, 104 // validating the signature in the Stripe-Signature header using the specified signing 105 // secret and tolerance window provided by the options, if applicable. 106 // 107 // See `ConstructEventOptions` for more details on each of the options. 108 // 109 // Returns an error if the signature doesn't match, or: 110 // - if `IgnoreTolerance` is false and the timestamp embedded in the event 111 // header is not within the tolerance window (similar to `ConstructEventWithTolerance`) 112 // - if `IgnoreAPIVersionMismatch` is false and the webhook event API version 113 // does not match the API version of the stripe-go library, as defined in 114 // `stripe.APIVersion`. 115 // 116 // NOTE: Stripe will only send Webhook signing headers after you have retrieved 117 // your signing secret from the Stripe dashboard: 118 // https://dashboard.stripe.com/webhooks 119 func ConstructEventWithOptions(payload []byte, header string, secret string, options ConstructEventOptions) (stripe.Event, error) { 120 return constructEvent(payload, header, secret, options) 121 } 122 123 // ValidatePayload validates the payload against the Stripe-Signature header 124 // using the specified signing secret. Returns an error if the body or 125 // Stripe-Signature header provided are unreadable, if the signature doesn't 126 // match, or if the timestamp for the signature is older than DefaultTolerance. 127 // 128 // NOTE: Stripe will only send Webhook signing headers after you have retrieved 129 // your signing secret from the Stripe dashboard: 130 // https://dashboard.stripe.com/webhooks 131 func ValidatePayload(payload []byte, header string, secret string) error { 132 return ValidatePayloadWithTolerance(payload, header, secret, DefaultTolerance) 133 } 134 135 // ValidatePayloadIgnoringTolerance validates the payload against the Stripe-Signature header 136 // using the specified signing secret. Returns an error if the body or 137 // Stripe-Signature header provided are unreadable or if the signature doesn't match. 138 // Does not check the signature's timestamp. 139 // 140 // NOTE: Stripe will only send Webhook signing headers after you have retrieved 141 // your signing secret from the Stripe dashboard: 142 // https://dashboard.stripe.com/webhooks 143 func ValidatePayloadIgnoringTolerance(payload []byte, header string, secret string) error { 144 return validatePayload(payload, header, secret, 0*time.Second, false) 145 } 146 147 // ValidatePayloadWithTolerance validates the payload against the Stripe-Signature header 148 // using the specified signing secret and tolerance window. Returns an error if the body 149 // or Stripe-Signature header provided are unreadable, if the signature doesn't match, or 150 // if the timestamp for the signature is older than the specified tolerance. 151 // 152 // NOTE: Stripe will only send Webhook signing headers after you have retrieved 153 // your signing secret from the Stripe dashboard: 154 // https://dashboard.stripe.com/webhooks 155 func ValidatePayloadWithTolerance(payload []byte, header string, secret string, tolerance time.Duration) error { 156 return validatePayload(payload, header, secret, tolerance, true) 157 } 158 159 type ConstructEventOptions struct { 160 // Validates event timestamps using a custom Tolerance window. If this is 161 // not set and `IgnoreTolerance` is false, will default to 162 // `DefaultTolerance`. 163 Tolerance time.Duration 164 165 // If set to true, will ignore the `tolerance` option entirely and will not 166 // check the event signature's timestamp. Defaults to false. When false, 167 // constructing an event will fail with an error if the timestamp is not 168 // within the `Tolerance` window. 169 IgnoreTolerance bool 170 171 // If set to true, will ignore validating whether an event's API version 172 // matches the stripe-go API version. Defaults to false, returning an error 173 // when there is a mismatch. 174 IgnoreAPIVersionMismatch bool 175 } 176 177 // 178 // Private types 179 // 180 181 type signedHeader struct { 182 timestamp time.Time 183 signatures [][]byte 184 } 185 186 // 187 // Private functions 188 // 189 190 func constructEvent(payload []byte, sigHeader string, secret string, options ConstructEventOptions) (stripe.Event, error) { 191 e := stripe.Event{} 192 193 tolerance := options.Tolerance 194 if options.Tolerance == 0 && !options.IgnoreTolerance { 195 tolerance = DefaultTolerance 196 } 197 198 if err := validatePayload(payload, sigHeader, secret, tolerance, !options.IgnoreTolerance); err != nil { 199 return e, err 200 } 201 202 if err := json.Unmarshal(payload, &e); err != nil { 203 return e, fmt.Errorf("Failed to parse webhook body json: %s", err.Error()) 204 } 205 206 if !options.IgnoreAPIVersionMismatch && e.APIVersion != stripe.APIVersion { 207 return e, fmt.Errorf("Received event with API version %s, but stripe-go %s expects API version %s. We recommend that you create a WebhookEndpoint with this API version. Otherwise, you can disable this error by using `ConstructEventWithOptions(..., ConstructEventOptions{..., ignoreAPIVersionMismatch: true})` but be wary that objects may be incorrectly deserialized.", e.APIVersion, stripe.ClientVersion, stripe.APIVersion) 208 } 209 210 return e, nil 211 212 } 213 214 func parseSignatureHeader(header string) (*signedHeader, error) { 215 sh := &signedHeader{} 216 217 if header == "" { 218 return sh, ErrNotSigned 219 } 220 221 // Signed header looks like "t=1495999758,v1=ABC,v1=DEF,v0=GHI" 222 pairs := strings.Split(header, ",") 223 for _, pair := range pairs { 224 parts := strings.Split(pair, "=") 225 if len(parts) != 2 { 226 return sh, ErrInvalidHeader 227 } 228 229 switch parts[0] { 230 case "t": 231 timestamp, err := strconv.ParseInt(parts[1], 10, 64) 232 if err != nil { 233 return sh, ErrInvalidHeader 234 } 235 sh.timestamp = time.Unix(timestamp, 0) 236 237 case signingVersion: 238 sig, err := hex.DecodeString(parts[1]) 239 if err != nil { 240 continue // Ignore invalid signatures 241 } 242 243 sh.signatures = append(sh.signatures, sig) 244 245 default: 246 continue // Ignore unknown parts of the header 247 } 248 } 249 250 if len(sh.signatures) == 0 { 251 return sh, ErrNoValidSignature 252 } 253 254 return sh, nil 255 } 256 257 func validatePayload(payload []byte, sigHeader string, secret string, tolerance time.Duration, enforceTolerance bool) error { 258 259 header, err := parseSignatureHeader(sigHeader) 260 if err != nil { 261 return err 262 } 263 264 expectedSignature := ComputeSignature(header.timestamp, payload, secret) 265 expiredTimestamp := time.Since(header.timestamp) > tolerance 266 if enforceTolerance && expiredTimestamp { 267 return ErrTooOld 268 } 269 270 // Check all given v1 signatures, multiple signatures will be sent temporarily in the case of a rolled signature secret 271 for _, sig := range header.signatures { 272 if hmac.Equal(expectedSignature, sig) { 273 return nil 274 } 275 } 276 277 return ErrNoValidSignature 278 } 279 280 // For mocking webhook events 281 type UnsignedPayload struct { 282 Payload []byte 283 Secret string 284 Timestamp time.Time 285 Scheme string 286 } 287 288 type SignedPayload struct { 289 UnsignedPayload 290 291 Signature []byte 292 Header string 293 } 294 295 func GenerateTestSignedPayload(options *UnsignedPayload) *SignedPayload { 296 signedPayload := &SignedPayload{UnsignedPayload: *options} 297 298 if signedPayload.Timestamp == (time.Time{}) { 299 signedPayload.Timestamp = time.Now() 300 } 301 302 if signedPayload.Scheme == "" { 303 signedPayload.Scheme = "v1" 304 } 305 306 signedPayload.Signature = ComputeSignature(signedPayload.Timestamp, signedPayload.Payload, signedPayload.Secret) 307 signedPayload.Header = generateHeader(*signedPayload) 308 309 return signedPayload 310 } 311 312 func generateHeader(p SignedPayload) string { 313 return fmt.Sprintf("t=%d,%s=%s", p.Timestamp.Unix(), p.Scheme, hex.EncodeToString(p.Signature)) 314 }