omniauth-shopify-oauth2 2.0.0 → 2.2.3

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
- SHA1:
3
- metadata.gz: 3c2fdff942824597bca290d63c4d5f2e8ad3fea3
4
- data.tar.gz: 175291a1282389126084a7d32c2fd7c4965faa9c
2
+ SHA256:
3
+ metadata.gz: 69f2b366f0b3fb5baddea99a0eb2122c2f778c160cb92c9673aa9ce0a53659f0
4
+ data.tar.gz: 893e4e0b105eed07b80c5f2074ee83109b9403d2c24dca0613bd7d6f8dfeaad5
5
5
  SHA512:
6
- metadata.gz: f905cd84cc0df6a8b1ef9d61ae432e0aaeb2d88547b77e4ddf5ca09c991ef181d131690a5ec6022553ff31b1246875cc76fd7420911cb463d021f459a72b8b4b
7
- data.tar.gz: e909527c818473c35ae5e9caf4f3afaffe38e9fdf835fc7bff13fe41c1be8ab2e81f71f1fca02011013d78303d1baccb222fb9e2c6cd7be9704fd9645c668e31
6
+ metadata.gz: 97a15729f1a70b5ca5e64e685da47094f9f8a9ec63e53493464ddb72d48c615ea598bda9ade2cd95ee9d9d85cfa70013a61f5cc3aa54396e34c81ee64d0d548b
7
+ data.tar.gz: b3f98a62584b48ea29f1e61d751e516235ab2029061b4762fe441ff56833fc42a3e9fb8cbcbeea15ab720dda50861e694180d5d04f7580b8e974f8f54e339f7c
data/Gemfile CHANGED
@@ -5,3 +5,7 @@ gemspec
5
5
  if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.2")
6
6
  gem 'rack', '~> 1.6'
7
7
  end
8
+
9
+ group :development, :test do
10
+ gem 'fakeweb', git: 'https://github.com/chrisk/fakeweb.git'
11
+ end
data/README.md CHANGED
@@ -36,6 +36,35 @@ Authenticate the user by having them visit /auth/shopify with a `shop` query par
36
36
  </form>
37
37
  ```
38
38
 
39
+ Or without form `/auth/shopify?shop=your-shop-url.myshopify.com`
40
+ Alternatively you can put shop parameter to session as [Shopify App](https://github.com/Shopify/shopify_app) do
41
+
42
+ ```ruby
43
+ session['shopify.omniauth_params'] = { shop: params[:shop] }
44
+ ```
45
+
46
+ And finally it's possible to use your own query parameter by overriding default setup method. For example, like below:
47
+
48
+ ```ruby
49
+ Rails.application.config.middleware.use OmniAuth::Builder do
50
+ provider :shopify,
51
+ ENV['SHOPIFY_API_KEY'],
52
+ ENV['SHOPIFY_SHARED_SECRET'],
53
+ option :setup, proc { |env|
54
+ strategy = env['omniauth.strategy']
55
+
56
+
57
+
58
+ site = if strategy.request.params['site']
59
+ "https://#{strategy.request.params['site']}"
60
+ else
61
+ ''
62
+ end
63
+
64
+ env['omniauth.strategy'].options[:client_options][:site] = site
65
+ }
66
+ ```
67
+
39
68
  ## Configuring
40
69
 
41
70
  ### Scope
@@ -0,0 +1,59 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ ### New features
6
+
7
+ New features will only be added to the master branch and will not be made available in point releases.
8
+
9
+ ### Bug fixes
10
+
11
+ Only the latest release series will receive bug fixes. When enough bugs are fixed and its deemed worthy to release a new gem, this is the branch it happens from.
12
+
13
+ ### Security issues
14
+
15
+ Only the latest release series will receive patches and new versions in case of a security issue.
16
+
17
+ ### Severe security issues
18
+
19
+ For severe security issues we will provide new versions as above, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team.
20
+
21
+ ### Unsupported Release Series
22
+
23
+ When a release series is no longer supported, it's your own responsibility to deal with bugs and security issues. If you are not comfortable maintaining your own versions, you should upgrade to a supported version.
24
+
25
+ ## Reporting a bug
26
+
27
+ All security bugs in shopify repositories should be reported to [our hackerone program](https://hackerone.com/shopify)
28
+ Shopify's whitehat program is our way to reward security researchers for finding serious security vulnerabilities in the In Scope properties listed at the bottom of this page, including our core application (all functionality associated with a Shopify store, particularly your-store.myshopify.com/admin) and certain ancillary applications.
29
+
30
+ ## Disclosure Policy
31
+
32
+ We look forward to working with all security researchers and strive to be respectful, always assume the best and treat others as peers. We expect the same in return from all participants. To achieve this, our team strives to:
33
+
34
+ - Reply to all reports within one business day and triage within two business days (if applicable)
35
+ - Be as transparent as possible, answering all inquires about our report decisions and adding hackers to duplicate HackerOne reports
36
+ - Award bounties within a week of resolution (excluding extenuating circumstances)
37
+ - Only close reports as N/A when the issue reported is included in Known Issues, Ineligible Vulnerabilities Types or lacks evidence of a vulnerability
38
+
39
+ **The following rules must be followed in order for any rewards to be paid:**
40
+
41
+ - You may only test against shops you have created which include your HackerOne YOURHANDLE @ wearehackerone.com registered email address.
42
+ - You must not attempt to gain access to, or interact with, any shops other than those created by you.
43
+ - The use of commercial scanners is prohibited (e.g., Nessus).
44
+ - Rules for reporting must be followed.
45
+ - Do not disclose any issues publicly before they have been resolved.
46
+ - Shopify reserves the right to modify the rules for this program or deem any submissions invalid at any time. Shopify may cancel the whitehat program without notice at any time.
47
+ - Contacting Shopify Support over chat, email or phone about your HackerOne report is not allowed. We may disqualify you from receiving a reward, or from participating in the program altogether.
48
+ - You are not an employee of Shopify; employees should report bugs to the internal bug bounty program.
49
+ - You hereby represent, warrant and covenant that any content you submit to Shopify is an original work of authorship and that you are legally entitled to grant the rights and privileges conveyed by these terms. You further represent, warrant and covenant that the consent of no other person or entity is or will be necessary for Shopify to use the submitted content.
50
+ - By submitting content to Shopify, you irrevocably waive all moral rights which you may have in the content.
51
+ - All content submitted by you to Shopify under this program is licensed under the MIT License.
52
+ - You must report any discovered vulnerability to Shopify as soon as you have validated the vulnerability.
53
+ - Failure to follow any of the foregoing rules will disqualify you from participating in this program.
54
+
55
+ ** Please see our [Hackerone Profile](https://hackerone.com/shopify) for full details
56
+
57
+ ## Receiving Security Updates
58
+
59
+ To recieve all general updates to vulnerabilities, please subscribe to our hackerone [Hacktivity](https://hackerone.com/shopify/hacktivity)
@@ -1,5 +1,6 @@
1
1
  require 'bundler/setup'
2
2
  require 'sinatra/base'
3
+ require 'active_support/core_ext/hash'
3
4
  require 'omniauth-shopify-oauth2'
4
5
 
5
6
  SCOPE = 'read_products,read_orders,read_customers,write_shipping'
@@ -1,5 +1,5 @@
1
1
  module OmniAuth
2
2
  module Shopify
3
- VERSION = "2.0.0"
3
+ VERSION = "2.2.3"
4
4
  end
5
5
  end
@@ -17,6 +17,7 @@ module OmniAuth
17
17
 
18
18
  option :callback_url
19
19
  option :myshopify_domain, 'myshopify.com'
20
+ option :old_client_secret
20
21
 
21
22
  # When `true`, the user's permission level will apply (in addition to
22
23
  # the requested access scope) when making API requests to Shopify.
@@ -25,7 +26,11 @@ module OmniAuth
25
26
  option :setup, proc { |env|
26
27
  strategy = env['omniauth.strategy']
27
28
 
28
- shopify_auth_params = strategy.session['shopify.omniauth_params'] && strategy.session['shopify.omniauth_params'].with_indifferent_access
29
+ shopify_auth_params = strategy.session['shopify.omniauth_params'] ||
30
+ strategy.session['omniauth.params'] ||
31
+ strategy.request.params
32
+
33
+ shopify_auth_params = shopify_auth_params && shopify_auth_params.with_indifferent_access
29
34
  shop = if shopify_auth_params && shopify_auth_params['shop']
30
35
  "https://#{shopify_auth_params['shop']}"
31
36
  else
@@ -43,6 +48,7 @@ module OmniAuth
43
48
  'associated_user' => access_token['associated_user'],
44
49
  'associated_user_scope' => access_token['associated_user_scope'],
45
50
  'scope' => access_token['scope'],
51
+ 'session' => access_token['session']
46
52
  }
47
53
  end
48
54
  end
@@ -61,8 +67,10 @@ module OmniAuth
61
67
 
62
68
  return false unless timestamp.to_i > Time.now.to_i - CODE_EXPIRES_AFTER
63
69
 
64
- calculated_signature = self.class.hmac_sign(self.class.encoded_params_for_signature(params), options.client_secret)
65
- Rack::Utils.secure_compare(calculated_signature, signature)
70
+ new_secret = options.client_secret
71
+ old_secret = options.old_client_secret
72
+
73
+ validate_signature(new_secret) || (old_secret && validate_signature(old_secret))
66
74
  end
67
75
 
68
76
  def valid_scope?(token)
@@ -74,7 +82,7 @@ module OmniAuth
74
82
 
75
83
  def normalized_scopes(scopes)
76
84
  scope_list = scopes.to_s.split(SCOPE_DELIMITER).map(&:strip).reject(&:empty?).uniq
77
- ignore_scopes = scope_list.map { |scope| scope =~ /\Awrite_(.*)\z/ && "read_#{$1}" }.compact
85
+ ignore_scopes = scope_list.map { |scope| scope =~ /\A(unauthenticated_)?write_(.*)\z/ && "#{$1}read_#{$2}" }.compact
78
86
  scope_list - ignore_scopes
79
87
  end
80
88
 
@@ -82,7 +90,7 @@ module OmniAuth
82
90
  params = params.dup
83
91
  params.delete('hmac')
84
92
  params.delete('signature') # deprecated signature
85
- params.map{|k,v| "#{URI.escape(k.to_s, '&=%')}=#{URI.escape(v.to_s, '&%')}"}.sort.join('&')
93
+ Rack::Utils.build_query(params.sort)
86
94
  end
87
95
 
88
96
  def self.hmac_sign(encoded_params, secret)
@@ -90,7 +98,12 @@ module OmniAuth
90
98
  end
91
99
 
92
100
  def valid_permissions?(token)
93
- token && (options[:per_user_permissions] == !token['associated_user'].nil?)
101
+ return false unless token
102
+
103
+ return true if options[:per_user_permissions] && token['associated_user']
104
+ return true if !options[:per_user_permissions] && !token['associated_user']
105
+
106
+ false
94
107
  end
95
108
 
96
109
  def fix_https
@@ -123,6 +136,8 @@ module OmniAuth
123
136
  end
124
137
 
125
138
  super
139
+ rescue ::OAuth2::Error => e
140
+ fail!(:invalid_credentials, e)
126
141
  end
127
142
 
128
143
  def build_access_token
@@ -139,6 +154,14 @@ module OmniAuth
139
154
  def callback_url
140
155
  options[:callback_url] || full_host + script_name + callback_path
141
156
  end
157
+
158
+ private
159
+
160
+ def validate_signature(secret)
161
+ params = request.GET
162
+ calculated_signature = self.class.hmac_sign(self.class.encoded_params_for_signature(params), secret)
163
+ Rack::Utils.secure_compare(calculated_signature, params['hmac'])
164
+ end
142
165
  end
143
166
  end
144
167
  end
@@ -11,6 +11,8 @@ Gem::Specification.new do |s|
11
11
  s.homepage = 'https://github.com/Shopify/omniauth-shopify-oauth2'
12
12
  s.license = 'MIT'
13
13
 
14
+ s.metadata['allowed_push_host'] = 'https://rubygems.org'
15
+
14
16
  s.files = `git ls-files`.split("\n")
15
17
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
18
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
@@ -21,6 +23,7 @@ Gem::Specification.new do |s|
21
23
  s.add_runtime_dependency 'activesupport'
22
24
 
23
25
  s.add_development_dependency 'minitest', '~> 5.6'
26
+ s.add_development_dependency 'rspec', '~> 3.9.0'
24
27
  s.add_development_dependency 'fakeweb', '~> 1.3'
25
28
  s.add_development_dependency 'rake'
26
29
  end
@@ -1,4 +1,3 @@
1
- require 'spec_helper'
2
1
  require 'omniauth-shopify-oauth2'
3
2
  require 'base64'
4
3
 
@@ -141,4 +140,80 @@ describe OmniAuth::Strategies::Shopify do
141
140
  subject.valid_site?.should eq(true)
142
141
  end
143
142
  end
143
+
144
+ describe '#valid_permissions?' do
145
+ let(:associated_user) do
146
+ {}
147
+ end
148
+
149
+ let(:token) do
150
+ {
151
+ 'associated_user' => associated_user,
152
+ }
153
+ end
154
+
155
+ it 'returns false if there is no token' do
156
+ expect(subject.valid_permissions?(nil)).to be_falsey
157
+ end
158
+
159
+ context 'with per_user_permissions is present' do
160
+ before do
161
+ @options = @options.merge(per_user_permissions: true)
162
+ end
163
+
164
+ context 'when token does not have associated user' do
165
+ let(:associated_user) { nil }
166
+
167
+ it 'return false' do
168
+ expect(subject.valid_permissions?(token)).to be_falsey
169
+ end
170
+ end
171
+
172
+ context 'when token has associated user' do
173
+ it 'return true' do
174
+ expect(subject.valid_permissions?(token)).to be_truthy
175
+ end
176
+ end
177
+ end
178
+
179
+ context 'with per_user_permissions is false' do
180
+ before do
181
+ @options = @options.merge(per_user_permissions: false)
182
+ end
183
+
184
+ context 'when token does not have associated user' do
185
+ let(:associated_user) { nil }
186
+
187
+ it 'return true' do
188
+ expect(subject.valid_permissions?(token)).to be_truthy
189
+ end
190
+ end
191
+
192
+ context 'when token has associated user' do
193
+ it 'return false' do
194
+ expect(subject.valid_permissions?(token)).to be_falsey
195
+ end
196
+ end
197
+ end
198
+
199
+ context 'with per_user_permissions is nil' do
200
+ before do
201
+ @options = @options.merge(per_user_permissions: nil)
202
+ end
203
+
204
+ context 'when token does not have associated user' do
205
+ let(:associated_user) { nil }
206
+
207
+ it 'return true' do
208
+ expect(subject.valid_permissions?(token)).to be_truthy
209
+ end
210
+ end
211
+
212
+ context 'when token has associated user' do
213
+ it 'return false' do
214
+ expect(subject.valid_permissions?(token)).to be_falsey
215
+ end
216
+ end
217
+ end
218
+ end
144
219
  end
@@ -52,7 +52,7 @@ class IntegrationTest < Minitest::Test
52
52
  response = authorize(shop)
53
53
  assert_auth_failure(response, 'invalid_site')
54
54
 
55
- response = callback(sign_params(shop: shop, code: code))
55
+ response = callback(sign_with_new_secret(shop: shop, code: code))
56
56
  assert_auth_failure(response, 'invalid_site')
57
57
  end
58
58
  end
@@ -62,7 +62,7 @@ class IntegrationTest < Minitest::Test
62
62
  code = SecureRandom.hex(16)
63
63
  expect_access_token_request(access_token, OmniAuth::Strategies::Shopify::DEFAULT_SCOPE)
64
64
 
65
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
65
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
66
66
 
67
67
  assert_callback_success(response, access_token, code)
68
68
  end
@@ -73,7 +73,7 @@ class IntegrationTest < Minitest::Test
73
73
  code = SecureRandom.hex(16)
74
74
  expect_access_token_request(access_token, OmniAuth::Strategies::Shopify::DEFAULT_SCOPE)
75
75
 
76
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]).merge(signature: 'ignored'))
76
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]).merge(signature: 'ignored'))
77
77
 
78
78
  assert_callback_success(response, access_token, code)
79
79
  end
@@ -86,7 +86,7 @@ class IntegrationTest < Minitest::Test
86
86
 
87
87
  now = Time.now.to_i
88
88
  params = { shop: 'snowdevil.myshopify.com', code: code, timestamp: now, next: '/products?page=2&q=red%20shirt', state: opts["rack.session"]["omniauth.state"] }
89
- encoded_params = "code=#{code}&next=/products?page=2%26q=red%2520shirt&shop=snowdevil.myshopify.com&state=#{opts["rack.session"]["omniauth.state"]}&timestamp=#{now}"
89
+ encoded_params = "code=#{code}&next=%2Fproducts%3Fpage%3D2%26q%3Dred%2520shirt&shop=snowdevil.myshopify.com&state=#{opts["rack.session"]["omniauth.state"]}&timestamp=#{now}"
90
90
  params[:hmac] = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @secret, encoded_params)
91
91
 
92
92
  response = callback(params)
@@ -100,21 +100,21 @@ class IntegrationTest < Minitest::Test
100
100
  code = SecureRandom.hex(16)
101
101
  expect_access_token_request(access_token, 'read_orders,write_products')
102
102
 
103
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
103
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
104
104
 
105
105
  assert_callback_success(response, access_token, code)
106
106
  end
107
107
 
108
108
  def test_callback_rejects_invalid_hmac
109
109
  @secret = 'wrong_secret'
110
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: SecureRandom.hex(16)))
110
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: SecureRandom.hex(16)))
111
111
 
112
112
  assert_auth_failure(response, 'invalid_signature')
113
113
  end
114
114
 
115
115
  def test_callback_rejects_old_timestamps
116
116
  expired_timestamp = Time.now.to_i - OmniAuth::Strategies::Shopify::CODE_EXPIRES_AFTER - 1
117
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: SecureRandom.hex(16), timestamp: expired_timestamp))
117
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: SecureRandom.hex(16), timestamp: expired_timestamp))
118
118
 
119
119
  assert_auth_failure(response, 'invalid_signature')
120
120
  end
@@ -129,7 +129,7 @@ class IntegrationTest < Minitest::Test
129
129
 
130
130
  def test_callback_rejects_body_params
131
131
  code = SecureRandom.hex(16)
132
- params = sign_params(shop: 'snowdevil.myshopify.com', code: code)
132
+ params = sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code)
133
133
  body = Rack::Utils.build_nested_query(unsigned: 'value')
134
134
 
135
135
  response = request.get("https://app.example.com/auth/shopify/callback?#{Rack::Utils.build_query(params)}",
@@ -169,8 +169,19 @@ class IntegrationTest < Minitest::Test
169
169
  assert_equal 'https://app.example.com/auth/shopify/callback', redirect_params['redirect_uri']
170
170
  end
171
171
 
172
+ def test_default_setup_reads_shop_from_params
173
+ build_app
174
+
175
+ response = request.get('https://app.example.com/auth/shopify?shop=snowdevil.myshopify.com', opts)
176
+
177
+ assert_equal 302, response.status
178
+ assert_match %r{\A#{Regexp.quote("https://snowdevil.myshopify.com/admin/oauth/authorize?")}}, response.location
179
+ redirect_params = Rack::Utils.parse_query(URI(response.location).query)
180
+ assert_equal 'https://app.example.com/auth/shopify/callback', redirect_params['redirect_uri']
181
+ end
182
+
172
183
  def test_unnecessary_read_scopes_are_removed
173
- build_app scope: 'read_content,read_products,write_products',
184
+ build_app scope: 'read_content,read_products,write_products,unauthenticated_read_checkouts,unauthenticated_write_checkouts',
174
185
  callback_path: '/admin/auth/legacy/callback',
175
186
  myshopify_domain: 'myshopify.dev:3000',
176
187
  setup: lambda { |env|
@@ -181,7 +192,7 @@ class IntegrationTest < Minitest::Test
181
192
  response = request.get("https://app.example.com/auth/shopify?shop=snowdevil.myshopify.dev:3000")
182
193
  assert_equal 302, response.status
183
194
  redirect_params = Rack::Utils.parse_query(URI(response.location).query)
184
- assert_equal 'read_content,write_products', redirect_params['scope']
195
+ assert_equal 'read_content,write_products,unauthenticated_write_checkouts', redirect_params['scope']
185
196
  end
186
197
 
187
198
  def test_callback_with_invalid_state_fails
@@ -189,7 +200,7 @@ class IntegrationTest < Minitest::Test
189
200
  code = SecureRandom.hex(16)
190
201
  expect_access_token_request(access_token, OmniAuth::Strategies::Shopify::DEFAULT_SCOPE)
191
202
 
192
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: 'invalid'))
203
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: 'invalid'))
193
204
 
194
205
  assert_equal 302, response.status
195
206
  assert_equal '/auth/failure?message=csrf_detected&strategy=shopify', response.location
@@ -200,7 +211,7 @@ class IntegrationTest < Minitest::Test
200
211
  code = SecureRandom.hex(16)
201
212
  expect_access_token_request(access_token, 'some_invalid_scope', nil)
202
213
 
203
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
214
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
204
215
 
205
216
  assert_equal 302, response.status
206
217
  assert_equal '/auth/failure?message=invalid_scope&strategy=shopify', response.location
@@ -211,7 +222,7 @@ class IntegrationTest < Minitest::Test
211
222
  code = SecureRandom.hex(16)
212
223
  expect_access_token_request(access_token, nil)
213
224
 
214
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
225
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
215
226
 
216
227
  assert_equal 302, response.status
217
228
  assert_equal '/auth/failure?message=invalid_scope&strategy=shopify', response.location
@@ -224,7 +235,7 @@ class IntegrationTest < Minitest::Test
224
235
  code = SecureRandom.hex(16)
225
236
  expect_access_token_request(access_token, 'first_scope')
226
237
 
227
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
238
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
228
239
 
229
240
  assert_equal 302, response.status
230
241
  assert_equal '/auth/failure?message=invalid_scope&strategy=shopify', response.location
@@ -237,7 +248,7 @@ class IntegrationTest < Minitest::Test
237
248
  code = SecureRandom.hex(16)
238
249
  expect_access_token_request(access_token, 'second_scope,first_scope,third_scope')
239
250
 
240
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
251
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
241
252
 
242
253
  assert_equal 302, response.status
243
254
  assert_equal '/auth/failure?message=invalid_scope&strategy=shopify', response.location
@@ -250,7 +261,19 @@ class IntegrationTest < Minitest::Test
250
261
  code = SecureRandom.hex(16)
251
262
  expect_access_token_request(access_token, 'second_scope,first_scope')
252
263
 
253
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
264
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
265
+
266
+ assert_callback_success(response, access_token, code)
267
+ end
268
+
269
+ def test_callback_with_duplicate_read_scopes_works
270
+ build_app scope: 'read_products,write_products,unauthenticated_read_products,unauthenticated_write_products'
271
+
272
+ access_token = SecureRandom.hex(16)
273
+ code = SecureRandom.hex(16)
274
+ expect_access_token_request(access_token, 'write_products,unauthenticated_write_products')
275
+
276
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
254
277
 
255
278
  assert_callback_success(response, access_token, code)
256
279
  end
@@ -262,7 +285,7 @@ class IntegrationTest < Minitest::Test
262
285
  code = SecureRandom.hex(16)
263
286
  expect_access_token_request(access_token, 'read_content,write_products')
264
287
 
265
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
288
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
266
289
 
267
290
  assert_callback_success(response, access_token, code)
268
291
  end
@@ -274,12 +297,24 @@ class IntegrationTest < Minitest::Test
274
297
  code = SecureRandom.hex(16)
275
298
  expect_access_token_request(access_token, 'scope', { id: 1, email: 'bob@bobsen.com'})
276
299
 
277
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
300
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
278
301
 
279
302
  assert_equal 302, response.status
280
303
  assert_equal '/auth/failure?message=invalid_permissions&strategy=shopify', response.location
281
304
  end
282
305
 
306
+ def test_callback_when_per_user_permissions_are_not_present_and_options_is_nil
307
+ build_app(scope: 'scope', per_user_permissions: nil)
308
+
309
+ access_token = SecureRandom.hex(16)
310
+ code = SecureRandom.hex(16)
311
+ expect_access_token_request(access_token, 'scope', nil)
312
+
313
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
314
+
315
+ assert_callback_success(response, access_token, code)
316
+ end
317
+
283
318
  def test_callback_when_per_user_permissions_are_not_present_but_requested
284
319
  build_app(scope: 'scope', per_user_permissions: true)
285
320
 
@@ -287,7 +322,7 @@ class IntegrationTest < Minitest::Test
287
322
  code = SecureRandom.hex(16)
288
323
  expect_access_token_request(access_token, 'scope', nil)
289
324
 
290
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
325
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
291
326
 
292
327
  assert_equal 302, response.status
293
328
  assert_equal '/auth/failure?message=invalid_permissions&strategy=shopify', response.location
@@ -300,26 +335,93 @@ class IntegrationTest < Minitest::Test
300
335
  code = SecureRandom.hex(16)
301
336
  expect_access_token_request(access_token, 'scope', { id: 1, email: 'bob@bobsen.com'})
302
337
 
303
- response = callback(sign_params(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
338
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
339
+
340
+ assert_equal 200, response.status
341
+ end
342
+
343
+ def test_callback_when_a_session_is_present
344
+ build_app(scope: 'scope', per_user_permissions: true)
345
+
346
+ access_token = SecureRandom.hex(16)
347
+ code = SecureRandom.hex(16)
348
+ session = SecureRandom.hex
349
+ expect_access_token_request(access_token, 'scope', { id: 1, email: 'bob@bobsen.com'}, session)
350
+
351
+ response = callback(sign_with_new_secret(shop: 'snowdevil.myshopify.com', code: code, state: opts["rack.session"]["omniauth.state"]))
304
352
 
305
353
  assert_equal 200, response.status
306
354
  end
307
355
 
356
+ def test_callback_works_with_old_secret
357
+ build_app scope: OmniAuth::Strategies::Shopify::DEFAULT_SCOPE
358
+ access_token = SecureRandom.hex(16)
359
+ code = SecureRandom.hex(16)
360
+ expect_access_token_request(access_token, OmniAuth::Strategies::Shopify::DEFAULT_SCOPE)
361
+
362
+ signed_params = sign_with_old_secret(
363
+ shop: 'snowdevil.myshopify.com',
364
+ code: code,
365
+ state: opts["rack.session"]["omniauth.state"]
366
+ )
367
+
368
+ response = callback(signed_params)
369
+
370
+ assert_callback_success(response, access_token, code)
371
+ end
372
+
373
+ def test_callback_when_creds_are_invalid
374
+ build_app scope: OmniAuth::Strategies::Shopify::DEFAULT_SCOPE
375
+
376
+ FakeWeb.register_uri(
377
+ :post,
378
+ "https://snowdevil.myshopify.com/admin/oauth/access_token",
379
+ status: [ "401", "Invalid token" ],
380
+ body: "Token is invalid or has already been requested"
381
+ )
382
+
383
+ signed_params = sign_with_new_secret(
384
+ shop: 'snowdevil.myshopify.com',
385
+ code: SecureRandom.hex(16),
386
+ state: opts["rack.session"]["omniauth.state"]
387
+ )
388
+
389
+ response = callback(signed_params)
390
+
391
+ assert_equal 302, response.status
392
+ assert_equal '/auth/failure?message=invalid_credentials&strategy=shopify', response.location
393
+ end
394
+
308
395
  private
309
396
 
310
- def sign_params(params)
311
- params = params.dup
397
+ def sign_with_old_secret(params)
398
+ params = add_time(params)
399
+ encoded_params = OmniAuth::Strategies::Shopify.encoded_params_for_signature(params)
400
+ params['hmac'] = OmniAuth::Strategies::Shopify.hmac_sign(encoded_params, @old_secret)
401
+ params
402
+ end
312
403
 
404
+ def add_time(params)
405
+ params = params.dup
313
406
  params[:timestamp] ||= Time.now.to_i
407
+ params
408
+ end
314
409
 
410
+ def sign_with_new_secret(params)
411
+ params = add_time(params)
315
412
  encoded_params = OmniAuth::Strategies::Shopify.encoded_params_for_signature(params)
316
413
  params['hmac'] = OmniAuth::Strategies::Shopify.hmac_sign(encoded_params, @secret)
317
414
  params
318
415
  end
319
416
 
320
- def expect_access_token_request(access_token, scope, associated_user=nil)
417
+ def expect_access_token_request(access_token, scope, associated_user=nil, session=nil)
321
418
  FakeWeb.register_uri(:post, "https://snowdevil.myshopify.com/admin/oauth/access_token",
322
- body: JSON.dump(access_token: access_token, scope: scope, associated_user: associated_user),
419
+ body: JSON.dump(
420
+ access_token: access_token,
421
+ scope: scope,
422
+ associated_user: associated_user,
423
+ session: session,
424
+ ),
323
425
  content_type: 'application/json')
324
426
  end
325
427
 
@@ -344,6 +446,9 @@ class IntegrationTest < Minitest::Test
344
446
  end
345
447
 
346
448
  def build_app(options={})
449
+ @old_secret = '12d34s1'
450
+ @secret = '53cr3tz'
451
+ options.merge!(old_client_secret: @old_secret)
347
452
  app = proc { |env|
348
453
  @omniauth_result = env['omniauth.auth']
349
454
  [200, {Rack::CONTENT_TYPE => "text/plain"}, "OK"]
@@ -351,9 +456,8 @@ class IntegrationTest < Minitest::Test
351
456
 
352
457
  opts["rack.session"]["omniauth.state"] = SecureRandom.hex(32)
353
458
  app = OmniAuth::Builder.new(app) do
354
- provider :shopify, '123', '53cr3tz', options
459
+ provider :shopify, '123', '53cr3tz' , options
355
460
  end
356
- @secret = '53cr3tz'
357
461
  @app = Rack::Session::Cookie.new(app, secret: SecureRandom.hex(64))
358
462
  end
359
463
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-shopify-oauth2
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Odorcic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-14 00:00:00.000000000 Z
11
+ date: 2020-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: omniauth-oauth2
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.9.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.9.0
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: fakeweb
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +107,7 @@ files:
93
107
  - Gemfile
94
108
  - README.md
95
109
  - Rakefile
110
+ - SECURITY.md
96
111
  - example/Gemfile
97
112
  - example/config.ru
98
113
  - lib/omniauth-shopify-oauth2.rb
@@ -107,7 +122,8 @@ files:
107
122
  homepage: https://github.com/Shopify/omniauth-shopify-oauth2
108
123
  licenses:
109
124
  - MIT
110
- metadata: {}
125
+ metadata:
126
+ allowed_push_host: https://rubygems.org
111
127
  post_install_message:
112
128
  rdoc_options: []
113
129
  require_paths:
@@ -123,8 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
139
  - !ruby/object:Gem::Version
124
140
  version: '0'
125
141
  requirements: []
126
- rubyforge_project:
127
- rubygems_version: 2.6.14
142
+ rubygems_version: 3.0.3
128
143
  signing_key:
129
144
  specification_version: 4
130
145
  summary: Shopify strategy for OmniAuth