stripe 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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