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 +5 -5
- data/Gemfile +4 -0
- data/README.md +29 -0
- data/SECURITY.md +59 -0
- data/example/config.ru +1 -0
- data/lib/omniauth/shopify/version.rb +1 -1
- data/lib/omniauth/strategies/shopify.rb +29 -6
- data/omniauth-shopify-oauth2.gemspec +3 -0
- data/spec/omniauth/strategies/shopify_spec.rb +76 -1
- data/test/integration_test.rb +130 -26
- metadata +20 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 69f2b366f0b3fb5baddea99a0eb2122c2f778c160cb92c9673aa9ce0a53659f0
|
4
|
+
data.tar.gz: 893e4e0b105eed07b80c5f2074ee83109b9403d2c24dca0613bd7d6f8dfeaad5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97a15729f1a70b5ca5e64e685da47094f9f8a9ec63e53493464ddb72d48c615ea598bda9ade2cd95ee9d9d85cfa70013a61f5cc3aa54396e34c81ee64d0d548b
|
7
|
+
data.tar.gz: b3f98a62584b48ea29f1e61d751e516235ab2029061b4762fe441ff56833fc42a3e9fb8cbcbeea15ab720dda50861e694180d5d04f7580b8e974f8f54e339f7c
|
data/Gemfile
CHANGED
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
|
data/SECURITY.md
ADDED
@@ -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)
|
data/example/config.ru
CHANGED
@@ -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']
|
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
|
-
|
65
|
-
|
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 =~ /\
|
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
|
-
|
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
|
-
|
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
|
data/test/integration_test.rb
CHANGED
@@ -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(
|
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(
|
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(
|
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
|
89
|
+
encoded_params = "code=#{code}&next=%2Fproducts%3Fpage%3D2%26q%3Dred%2520shirt&shop=snowdevil.myshopify.com&state=#{opts["rack.session"]["omniauth.state"]}×tamp=#{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(
|
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(
|
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(
|
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 =
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|
311
|
-
params = params
|
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(
|
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.
|
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:
|
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
|
-
|
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
|