stripe 2.7.0 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2f1ca52eecc695b6655a4317d0b5c8c893431aab
4
- data.tar.gz: 1a5e5f17ba9e3e73574fb3af3f2014b3b39b06ed
3
+ metadata.gz: 747667dd050969e755253ea2c5328142ebff0dd2
4
+ data.tar.gz: f60cfc0497a8b0044811283e893a667755f3f6cd
5
5
  SHA512:
6
- metadata.gz: 9bf4f072ef017d7247c014606532275d976a46aa451fbb9be12d25eaa792b4ae843795d329cf94359d41c83a1a3bf836b090ba7beb20ebcad6b4151497469cff
7
- data.tar.gz: 58db5a2043f13901d93e9024a1dcfffde774cbcfc4f614cce4b43354069efbc115252b78981f33baddc599a090b2775dd6ee23b9bc2aa4d5c43007d9f062c57d
6
+ metadata.gz: b405457d2c0615be9e9fdf1ea20229b95365fb0a615d2f1b508fe0b4466c8842e7a7469317208e477280df0eb51d280e1e82928d5465de54fdca7e786df44faa
7
+ data.tar.gz: 8718ad60ec44fd3af67bfb5b8567c56a35141ce14937f6a88ed7d455084ed491cb93a3d2fb45c31d2297d0324667ba8d43517aea65b87c72aea7c28240ed9863
data/History.txt CHANGED
@@ -1,3 +1,7 @@
1
+ === 2.8.0 2017-04-28
2
+
3
+ * Support for checking webhook signatures
4
+
1
5
  === 2.7.0 2017-04-26
2
6
 
3
7
  * Add model `InvoiceLineItem`
data/README.md CHANGED
@@ -122,6 +122,19 @@ an intermittent network problem:
122
122
  [Idempotency keys][idempotency-keys] are added to requests to guarantee that
123
123
  retries are safe.
124
124
 
125
+ ### Configuring Timeouts
126
+
127
+ Open and read timeouts are configurable:
128
+
129
+ ```java
130
+ Stripe.open_timeout = 30 // in seconds
131
+ Stripe.read_timeout = 80
132
+ ```
133
+
134
+ Please take care to set conservative read timeouts. Some API requests can take
135
+ some time, and a short timeout increases the likelihood of a problem within our
136
+ servers.
137
+
125
138
  ### Writing a Plugin
126
139
 
127
140
  If you're writing a plugin that uses the library, we'd appreciate it if you
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.7.0
1
+ 2.8.0
data/lib/stripe.rb CHANGED
@@ -28,6 +28,7 @@ require 'stripe/stripe_response'
28
28
  require 'stripe/list_object'
29
29
  require 'stripe/api_resource'
30
30
  require 'stripe/singleton_api_resource'
31
+ require 'stripe/webhook'
31
32
 
32
33
  # Named API resources
33
34
  require 'stripe/account'
data/lib/stripe/errors.rb CHANGED
@@ -89,4 +89,15 @@ module Stripe
89
89
  # back off on request rate.
90
90
  class RateLimitError < StripeError
91
91
  end
92
+
93
+ # SignatureVerificationError is raised when the signature verification for a
94
+ # webhook fails
95
+ class SignatureVerificationError < StripeError
96
+ attr_accessor :sig_header
97
+
98
+ def initialize(message, sig_header, http_body: nil)
99
+ super(message, http_body: http_body)
100
+ @sig_header = sig_header
101
+ end
102
+ end
92
103
  end
data/lib/stripe/util.rb CHANGED
@@ -256,5 +256,17 @@ module Stripe
256
256
  end
257
257
  end
258
258
  end
259
+
260
+ # Constant time string comparison to prevent timing attacks
261
+ # Code borrowed from ActiveSupport
262
+ def self.secure_compare(a, b)
263
+ return false unless a.bytesize == b.bytesize
264
+
265
+ l = a.unpack "C#{a.bytesize}"
266
+
267
+ res = 0
268
+ b.each_byte { |byte| res |= byte ^ l.shift }
269
+ res == 0
270
+ end
259
271
  end
260
272
  end
@@ -1,3 +1,3 @@
1
1
  module Stripe
2
- VERSION = '2.7.0'
2
+ VERSION = '2.8.0'
3
3
  end
@@ -0,0 +1,79 @@
1
+ module Stripe
2
+ module Webhook
3
+ DEFAULT_TOLERANCE = 300
4
+
5
+ # Initializes an Event object from a JSON payload.
6
+ #
7
+ # This may raise JSON::ParserError if the payload is not valid JSON, or
8
+ # SignatureVerificationError if the signature verification fails.
9
+ def self.construct_event(payload, sig_header, secret, tolerance: DEFAULT_TOLERANCE)
10
+ data = JSON.parse(payload, symbolize_names: true)
11
+ event = Event.construct_from(data)
12
+
13
+ Signature.verify_header(payload, sig_header, secret, tolerance: tolerance)
14
+
15
+ event
16
+ end
17
+
18
+ module Signature
19
+ EXPECTED_SCHEME = 'v1'
20
+
21
+ def self.compute_signature(payload, secret)
22
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, payload)
23
+ end
24
+ private_class_method :compute_signature
25
+
26
+ # Extracts the timestamp and the signature(s) with the desired scheme
27
+ # from the header
28
+ def self.get_timestamp_and_signatures(header, scheme)
29
+ list_items = header.split(/,\s*/).map { |i| i.split('=', 2) }
30
+ timestamp = Integer(list_items.select { |i| i[0] == 't' }[0][1])
31
+ signatures = list_items.select { |i| i[0] == scheme }.map { |i| i[1] }
32
+ [timestamp, signatures]
33
+ end
34
+ private_class_method :get_timestamp_and_signatures
35
+
36
+ # Verifies the signature header for a given payload.
37
+ #
38
+ # Raises a SignatureVerificationError in the following cases:
39
+ # - the header does not match the expected format
40
+ # - no signatures found with the expected scheme
41
+ # - no signatures matching the expected signature
42
+ # - a tolerance is provided and the timestamp is not within the
43
+ # tolerance
44
+ #
45
+ # Returns true otherwise
46
+ def self.verify_header(payload, header, secret, tolerance: nil)
47
+ begin
48
+ timestamp, signatures = get_timestamp_and_signatures(header, EXPECTED_SCHEME)
49
+ rescue
50
+ raise SignatureVerificationError.new(
51
+ "Unable to extract timestamp and signatures from header",
52
+ header, http_body: payload)
53
+ end
54
+
55
+ if signatures.empty?
56
+ raise SignatureVerificationError.new(
57
+ "No signatures found with expected scheme #{EXPECTED_SCHEME}",
58
+ header, http_body: payload)
59
+ end
60
+
61
+ signed_payload = "#{timestamp}.#{payload}"
62
+ expected_sig = compute_signature(signed_payload, secret)
63
+ unless signatures.any? {|s| Util.secure_compare(expected_sig, s)}
64
+ raise SignatureVerificationError.new(
65
+ "No signatures found matching the expected signature for payload",
66
+ header, http_body: payload)
67
+ end
68
+
69
+ if tolerance && timestamp < Time.now.to_f - tolerance
70
+ raise SignatureVerificationError.new(
71
+ "Timestamp outside the tolerance zone (#{Time.at(timestamp)})",
72
+ header, http_body: payload)
73
+ end
74
+
75
+ true
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,92 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ module Stripe
4
+ class WebhookTest < Test::Unit::TestCase
5
+ EVENT_PAYLOAD = '''{
6
+ "id": "evt_test_webhook",
7
+ "object": "event"
8
+ }'''
9
+ SECRET = 'whsec_test_secret'
10
+
11
+ def generate_header(opts={})
12
+ opts[:timestamp] ||= Time.now.to_i
13
+ opts[:payload] ||= EVENT_PAYLOAD
14
+ opts[:secret] ||= SECRET
15
+ opts[:scheme] ||= Stripe::Webhook::Signature::EXPECTED_SCHEME
16
+ opts[:signature] ||= Stripe::Webhook::Signature.send(:compute_signature, "#{opts[:timestamp]}.#{opts[:payload]}", opts[:secret])
17
+ "t=#{opts[:timestamp]},#{opts[:scheme]}=#{opts[:signature]}"
18
+ end
19
+
20
+ context ".construct_event" do
21
+ should "return an Event instance from a valid JSON payload and valid signature header" do
22
+ header = generate_header
23
+ event = Stripe::Webhook.construct_event(EVENT_PAYLOAD, header, SECRET)
24
+ assert event.kind_of?(Stripe::Event)
25
+ end
26
+
27
+ should "raise a JSON::ParserError from an invalid JSON payload" do
28
+ assert_raises JSON::ParserError do
29
+ payload = 'this is not valid JSON'
30
+ header = generate_header(payload: payload)
31
+ Stripe::Webhook.construct_event(payload, header, SECRET)
32
+ end
33
+ end
34
+
35
+ should "raise a SignatureVerificationError from a valid JSON payload and an invalid signature header" do
36
+ header = 'bad_header'
37
+ assert_raises Stripe::SignatureVerificationError do
38
+ Stripe::Webhook.construct_event(EVENT_PAYLOAD, header, SECRET)
39
+ end
40
+ end
41
+ end
42
+
43
+ context ".verify_signature_header" do
44
+ should "raise a SignatureVerificationError when the header does not have the expected format" do
45
+ header = 'i\'m not even a real signature header'
46
+ e = assert_raises(Stripe::SignatureVerificationError) do
47
+ Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, 'secret')
48
+ end
49
+ assert_match("Unable to extract timestamp and signatures from header", e.message)
50
+ end
51
+
52
+ should "raise a SignatureVerificationError when there are no signatures with the expected scheme" do
53
+ header = generate_header(scheme: 'v0')
54
+ e = assert_raises(Stripe::SignatureVerificationError) do
55
+ Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, 'secret')
56
+ end
57
+ assert_match("No signatures found with expected scheme", e.message)
58
+ end
59
+
60
+ should "raise a SignatureVerificationError when there are no valid signatures for the payload" do
61
+ header = generate_header(signature: 'bad_signature')
62
+ e = assert_raises(Stripe::SignatureVerificationError) do
63
+ Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, 'secret')
64
+ end
65
+ assert_match("No signatures found matching the expected signature for payload", e.message)
66
+ end
67
+
68
+ should "raise a SignatureVerificationError when the timestamp is not within the tolerance" do
69
+ header = generate_header(timestamp: Time.now.to_i - 15)
70
+ e = assert_raises(Stripe::SignatureVerificationError) do
71
+ Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, SECRET, tolerance: 10)
72
+ end
73
+ assert_match("Timestamp outside the tolerance zone", e.message)
74
+ end
75
+
76
+ should "return true when the header contains a valid signature and the timestamp is within the tolerance" do
77
+ header = generate_header
78
+ assert(Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, SECRET, tolerance: 10))
79
+ end
80
+
81
+ should "return true when the header contains at least one valid signature" do
82
+ header = generate_header + ",v1=bad_signature"
83
+ assert(Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, SECRET, tolerance: 10))
84
+ end
85
+
86
+ should "return true when the header contains a valid signature and the timestamp is off but no tolerance is provided" do
87
+ header = generate_header(timestamp: 12345)
88
+ assert(Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, SECRET))
89
+ end
90
+ end
91
+ end
92
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stripe
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.0
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stripe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-26 00:00:00.000000000 Z
11
+ date: 2017-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -97,6 +97,7 @@ files:
97
97
  - lib/stripe/transfer.rb
98
98
  - lib/stripe/util.rb
99
99
  - lib/stripe/version.rb
100
+ - lib/stripe/webhook.rb
100
101
  - openapi/fixtures.json
101
102
  - openapi/fixtures.yaml
102
103
  - openapi/spec2.json
@@ -146,6 +147,7 @@ files:
146
147
  - test/stripe/three_d_secure_test.rb
147
148
  - test/stripe/transfer_test.rb
148
149
  - test/stripe/util_test.rb
150
+ - test/stripe/webhook_test.rb
149
151
  - test/stripe_test.rb
150
152
  - test/test_data.rb
151
153
  - test/test_helper.rb
@@ -218,6 +220,7 @@ test_files:
218
220
  - test/stripe/three_d_secure_test.rb
219
221
  - test/stripe/transfer_test.rb
220
222
  - test/stripe/util_test.rb
223
+ - test/stripe/webhook_test.rb
221
224
  - test/stripe_test.rb
222
225
  - test/test_data.rb
223
226
  - test/test_helper.rb