omniauth-globus 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,514 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'json'
5
+ require 'omniauth-globus'
6
+ require 'stringio'
7
+
8
+ describe OmniAuth::Strategies::Globus do
9
+ let(:request) { double('Request', params: {}, cookies: {}, env: {}) }
10
+ let(:app) do
11
+ lambda do
12
+ [200, {}, ['Hello.']]
13
+ end
14
+ end
15
+
16
+ subject do
17
+ OmniAuth::Strategies::Globus.new(app, 'appid', 'secret', @options || {}).tap do |strategy|
18
+ allow(strategy).to receive(:request) do
19
+ request
20
+ end
21
+ end
22
+ end
23
+
24
+ before do
25
+ OmniAuth.config.test_mode = true
26
+ end
27
+
28
+ after do
29
+ OmniAuth.config.test_mode = false
30
+ end
31
+
32
+ describe '#client_options' do
33
+ it 'has correct site' do
34
+ expect(subject.client.site).to eq('https://auth.globus.org')
35
+ end
36
+
37
+ it 'has correct authorize_url' do
38
+ expect(subject.client.options[:authorize_url]).to eq('https://auth.globus.org/v2/oauth2/authorize')
39
+ end
40
+
41
+ it 'has correct token_url' do
42
+ expect(subject.client.options[:token_url]).to eq('https://auth.globus.org/v2/oauth2/token')
43
+ end
44
+
45
+ describe 'overrides' do
46
+ context 'as strings' do
47
+ it 'should allow overriding the site' do
48
+ @options = { client_options: { 'site' => 'https://example.com' } }
49
+ expect(subject.client.site).to eq('https://example.com')
50
+ end
51
+
52
+ it 'should allow overriding the authorization_endpoint' do
53
+ @options = { client_options: { 'authorization_endpoint' => 'https://example.com' } }
54
+ expect(subject.client.options[:authorization_endpoint]).to eq('https://example.com')
55
+ end
56
+
57
+ it 'should allow overriding the token_endpoint' do
58
+ @options = { client_options: { 'token_endpoint' => 'https://example.com' } }
59
+ expect(subject.client.options[:token_endpoint]).to eq('https://example.com')
60
+ end
61
+ end
62
+
63
+ context 'as symbols' do
64
+ it 'should allow overriding the site' do
65
+ @options = { client_options: { site: 'https://example.com' } }
66
+ expect(subject.client.site).to eq('https://example.com')
67
+ end
68
+
69
+ it 'should allow overriding the authorization_endpoint' do
70
+ @options = { client_options: { authorization_endpoint: 'https://example.com' } }
71
+ expect(subject.client.options[:authorization_endpoint]).to eq('https://example.com')
72
+ end
73
+
74
+ it 'should allow overriding the token_endpoint' do
75
+ @options = { client_options: { token_endpoint: 'https://example.com' } }
76
+ expect(subject.client.options[:token_endpoint]).to eq('https://example.com')
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ describe '#authorize_options' do
83
+ %i[access_type login_hint prompt scope state device_id device_name].each do |k|
84
+ it "should support #{k}" do
85
+ @options = { k => 'http://someval' }
86
+ expect(subject.authorize_params[k.to_s]).to eq('http://someval')
87
+ end
88
+ end
89
+
90
+ describe 'redirect_uri' do
91
+ it 'should default to nil' do
92
+ @options = {}
93
+ expect(subject.authorize_params['redirect_uri']).to eq(nil)
94
+ end
95
+
96
+ it 'should set the redirect_uri parameter if present' do
97
+ @options = { redirect_uri: 'https://example.com' }
98
+ expect(subject.authorize_params['redirect_uri']).to eq('https://example.com')
99
+ end
100
+ end
101
+
102
+ describe 'access_type' do
103
+ it 'should default to "offline"' do
104
+ @options = {}
105
+ expect(subject.authorize_params['access_type']).to eq('offline')
106
+ end
107
+
108
+ it 'should set the access_type parameter if present' do
109
+ @options = { access_type: 'online' }
110
+ expect(subject.authorize_params['access_type']).to eq('online')
111
+ end
112
+ end
113
+
114
+ describe 'login_hint' do
115
+ it 'should default to nil' do
116
+ expect(subject.authorize_params['login_hint']).to eq(nil)
117
+ end
118
+
119
+ it 'should set the login_hint parameter if present' do
120
+ @options = { login_hint: 'john@example.com' }
121
+ expect(subject.authorize_params['login_hint']).to eq('john@example.com')
122
+ end
123
+ end
124
+
125
+ describe 'prompt' do
126
+ it 'should default to nil' do
127
+ expect(subject.authorize_params['prompt']).to eq(nil)
128
+ end
129
+
130
+ it 'should set the prompt parameter if present' do
131
+ @options = { prompt: 'consent select_account' }
132
+ expect(subject.authorize_params['prompt']).to eq('consent select_account')
133
+ end
134
+ end
135
+
136
+ describe 'request_visible_actions' do
137
+ it 'should default to nil' do
138
+ expect(subject.authorize_params['request_visible_actions']).to eq(nil)
139
+ end
140
+
141
+ it 'should set the request_visible_actions parameter if present' do
142
+ @options = { request_visible_actions: 'something' }
143
+ expect(subject.authorize_params['request_visible_actions']).to eq('something')
144
+ end
145
+ end
146
+
147
+ describe 'include_granted_scopes' do
148
+ it 'should default to nil' do
149
+ expect(subject.authorize_params['include_granted_scopes']).to eq(nil)
150
+ end
151
+
152
+ it 'should set the include_granted_scopes parameter if present' do
153
+ @options = { include_granted_scopes: 'true' }
154
+ expect(subject.authorize_params['include_granted_scopes']).to eq('true')
155
+ end
156
+ end
157
+
158
+ describe 'scope' do
159
+ it 'should leave base scopes as is' do
160
+ @options = { scope: 'profile' }
161
+ expect(subject.authorize_params['scope']).to eq('profile')
162
+ end
163
+
164
+ it 'should join scopes' do
165
+ @options = { scope: 'profile,email' }
166
+ expect(subject.authorize_params['scope']).to eq('profile email')
167
+ end
168
+
169
+ it 'should deal with whitespace when joining scopes' do
170
+ @options = { scope: 'profile, email' }
171
+ expect(subject.authorize_params['scope']).to eq('profile email')
172
+ end
173
+
174
+ it 'should set default scope to openid, profile, email' do
175
+ expect(subject.authorize_params['scope']).to eq("openid profile email")
176
+ end
177
+
178
+ it 'should support space delimited scopes' do
179
+ @options = { scope: 'profile email' }
180
+ expect(subject.authorize_params['scope']).to eq('profile email')
181
+ end
182
+
183
+ it 'should support extremely badly formed scopes' do
184
+ @options = { scope: 'profile email,foo,steve yeah' }
185
+ expect(subject.authorize_params['scope']).to eq('profile email foo steve yeah')
186
+ end
187
+ end
188
+
189
+ describe 'state' do
190
+ it 'should set the state parameter' do
191
+ @options = { state: 'some_state' }
192
+ expect(subject.authorize_params['state']).to eq('some_state')
193
+ expect(subject.authorize_params[:state]).to eq('some_state')
194
+ expect(subject.session['omniauth.state']).to eq('some_state')
195
+ end
196
+
197
+ it 'should set the omniauth.state dynamically' do
198
+ allow(subject).to receive(:request) { double('Request', params: { 'state' => 'some_state' }, env: {}) }
199
+ expect(subject.authorize_params['state']).to eq('some_state')
200
+ expect(subject.authorize_params[:state]).to eq('some_state')
201
+ expect(subject.session['omniauth.state']).to eq('some_state')
202
+ end
203
+ end
204
+
205
+ describe 'overrides' do
206
+ it 'should include top-level options that are marked as :authorize_options' do
207
+ @options = { authorize_options: %i[scope foo request_visible_actions], scope: 'http://bar', foo: 'baz', request_visible_actions: 'something' }
208
+ expect(subject.authorize_params['scope']).to eq('http://bar')
209
+ expect(subject.authorize_params['foo']).to eq('baz')
210
+ expect(subject.authorize_params['request_visible_actions']).to eq('something')
211
+ end
212
+
213
+ describe 'request overrides' do
214
+ %i[access_type login_hint prompt scope state].each do |k|
215
+ context "authorize option #{k}" do
216
+ let(:request) { double('Request', params: { k.to_s => 'http://example.com' }, cookies: {}, env: {}) }
217
+
218
+ it "should set the #{k} authorize option dynamically in the request" do
219
+ @options = { k: '' }
220
+ expect(subject.authorize_params[k.to_s]).to eq('http://example.com')
221
+ end
222
+ end
223
+ end
224
+
225
+ describe 'custom authorize_options' do
226
+ let(:request) { double('Request', params: { 'foo' => 'something' }, cookies: {}, env: {}) }
227
+
228
+ it 'should support request overrides from custom authorize_options' do
229
+ @options = { authorize_options: [:foo], foo: '' }
230
+ expect(subject.authorize_params['foo']).to eq('something')
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ describe '#authorize_params' do
238
+ it 'should include any authorize params passed in the :authorize_params option' do
239
+ @options = { authorize_params: { request_visible_actions: 'something', foo: 'bar', baz: 'zip' }, bad: 'not_included' }
240
+ expect(subject.authorize_params['request_visible_actions']).to eq('something')
241
+ expect(subject.authorize_params['foo']).to eq('bar')
242
+ expect(subject.authorize_params['baz']).to eq('zip')
243
+ expect(subject.authorize_params['bad']).to eq(nil)
244
+ end
245
+ end
246
+
247
+ describe '#token_params' do
248
+ it 'should include any token params passed in the :token_params option' do
249
+ @options = { token_params: { foo: 'bar', baz: 'zip' } }
250
+ expect(subject.token_params['foo']).to eq('bar')
251
+ expect(subject.token_params['baz']).to eq('zip')
252
+ end
253
+ end
254
+
255
+ describe '#token_options' do
256
+ it 'should include top-level options that are marked as :token_options' do
257
+ @options = { token_options: %i[scope foo], scope: 'bar', foo: 'baz', bad: 'not_included' }
258
+ expect(subject.token_params['scope']).to eq('bar')
259
+ expect(subject.token_params['foo']).to eq('baz')
260
+ expect(subject.token_params['bad']).to eq(nil)
261
+ end
262
+ end
263
+
264
+ describe '#callback_path' do
265
+ it 'has the correct default callback path' do
266
+ expect(subject.callback_path).to eq("/auth/globus/callback")
267
+ end
268
+
269
+ it 'should set the callback_path parameter if present' do
270
+ @options = { callback_path: '/auth/foo/callback' }
271
+ expect(subject.callback_path).to eq('/auth/foo/callback')
272
+ end
273
+ end
274
+
275
+ describe '#info' do
276
+ let(:client) do
277
+ OAuth2::Client.new('abc', 'def') do |builder|
278
+ builder.request :url_encoded
279
+ builder.adapter :test do |stub|
280
+ stub.get("/v2/oauth2/userinfo") { [200, { "content-type" => "application/json" }, response_hash.to_json] }
281
+ end
282
+ end
283
+ end
284
+ let(:access_token) { OAuth2::AccessToken.from_hash(client, {}) }
285
+ before { allow(subject).to receive(:access_token).and_return(access_token) }
286
+
287
+ context 'with email' do
288
+ let(:response_hash) do
289
+ { email: 'something@domain.invalid' }
290
+ end
291
+
292
+ it 'should return email' do
293
+ expect(subject.info[:email]).to eq('something@domain.invalid')
294
+ end
295
+ end
296
+ end
297
+
298
+ describe '#extra' do
299
+ let(:client) do
300
+ OAuth2::Client.new('abc', 'def') do |builder|
301
+ builder.request :url_encoded
302
+ builder.adapter :test do |stub|
303
+ stub.get('/v2/oauth2/userinfo') { [200, { 'content-type' => 'application/json' }, '{"sub": "12345"}'] }
304
+ end
305
+ end
306
+ end
307
+ let(:access_token) { OAuth2::AccessToken.from_hash(client, {}) }
308
+
309
+ before { allow(subject).to receive(:access_token).and_return(access_token) }
310
+
311
+ describe 'id_token' do
312
+ shared_examples 'id_token issued by valid issuer' do |issuer| # rubocop:disable Metrics/BlockLength
313
+ context 'when the id_token is passed into the access token' do
314
+ let(:token_info) do
315
+ {
316
+ identity_provider_display_name: "ORCID",
317
+ sub: "abc",
318
+ preferred_username: "0000-0003-1419-2405@orcid.org",
319
+ identity_provider: "def",
320
+ organization: "DataCite",
321
+ email: "mfenner@datacite.org",
322
+ name: "Martin Fenner",
323
+ exp: Time.now.to_i + 3600,
324
+ iss: "https://auth.globus.org"
325
+ }
326
+ end
327
+ let(:id_token) { JWT.encode(token_info, 'secret') }
328
+ let(:access_token) { OAuth2::AccessToken.from_hash(client, 'id_token' => id_token) }
329
+
330
+ it 'should include id_token when set on the access_token' do
331
+ expect(subject.extra).to include(id_token: id_token)
332
+ end
333
+ end
334
+ end
335
+
336
+ it_behaves_like 'id_token issued by valid issuer', 'https://auth.globus.org'
337
+
338
+ context 'when the id_token is issued by an invalid issuer' do
339
+ let(:token_info) do
340
+ {
341
+ identity_provider_display_name: "ORCID",
342
+ sub: "abc",
343
+ preferred_username: "0000-0003-1419-2405@orcid.org",
344
+ identity_provider: "def",
345
+ organization: "DataCite",
346
+ email: "mfenner@datacite.org",
347
+ name: "Martin Fenner",
348
+ exp: Time.now.to_i + 3600,
349
+ iss: "https://fake.globus.org"
350
+ }
351
+ end
352
+ let(:id_token) { JWT.encode(token_info, 'secret') }
353
+ let(:access_token) { OAuth2::AccessToken.from_hash(client, 'id_token' => id_token) }
354
+
355
+ it 'raises JWT::InvalidIssuerError' do
356
+ expect { subject.extra }.to raise_error(JWT::InvalidIssuerError)
357
+ end
358
+ end
359
+
360
+ context 'when the id_token is missing' do
361
+ it 'should not include id_token' do
362
+ expect(subject.extra).not_to have_key(:id_token)
363
+ end
364
+
365
+ it 'should not include id_info' do
366
+ expect(subject.extra).not_to have_key(:id_info)
367
+ end
368
+ end
369
+ end
370
+
371
+ describe 'raw_info' do
372
+ context 'when skip_info is true' do
373
+ before { subject.options[:skip_info] = true }
374
+
375
+ it 'should not include raw_info' do
376
+ expect(subject.extra).not_to have_key(:raw_info)
377
+ end
378
+ end
379
+
380
+ context 'when skip_info is false' do
381
+ before { subject.options[:skip_info] = false }
382
+
383
+ it 'should include raw_info' do
384
+ expect(subject.extra[:raw_info]).to eq('sub' => '12345')
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ describe 'build_access_token' do
391
+ it 'should use a hybrid authorization request_uri if this is an AJAX request with a code parameter' do
392
+ allow(request).to receive(:xhr?).and_return(true)
393
+ allow(request).to receive(:params).and_return('code' => 'valid_code')
394
+
395
+ client = double(:client)
396
+ auth_code = double(:auth_code)
397
+ allow(client).to receive(:auth_code).and_return(auth_code)
398
+ expect(subject).to receive(:client).and_return(client)
399
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'postmessage' }, {})
400
+
401
+ expect(subject).not_to receive(:orig_build_access_token)
402
+ subject.build_access_token
403
+ end
404
+
405
+ it 'should use a hybrid authorization request_uri if this is an AJAX request (mobile) with a code parameter' do
406
+ allow(request).to receive(:xhr?).and_return(true)
407
+ allow(request).to receive(:params).and_return('code' => 'valid_code', 'redirect_uri' => '')
408
+
409
+ client = double(:client)
410
+ auth_code = double(:auth_code)
411
+ allow(client).to receive(:auth_code).and_return(auth_code)
412
+ expect(subject).to receive(:client).and_return(client)
413
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: '' }, {})
414
+
415
+ expect(subject).not_to receive(:orig_build_access_token)
416
+ subject.build_access_token
417
+ end
418
+
419
+ it 'should use the request_uri from params if this not an AJAX request (request from installed app) with a code parameter' do
420
+ allow(request).to receive(:xhr?).and_return(false)
421
+ allow(request).to receive(:params).and_return('code' => 'valid_code', 'redirect_uri' => 'redirect_uri')
422
+
423
+ client = double(:client)
424
+ auth_code = double(:auth_code)
425
+ allow(client).to receive(:auth_code).and_return(auth_code)
426
+ expect(subject).to receive(:client).and_return(client)
427
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'redirect_uri' }, {})
428
+
429
+ expect(subject).not_to receive(:orig_build_access_token)
430
+ subject.build_access_token
431
+ end
432
+
433
+ it 'should read access_token from hash if this is not an AJAX request with a code parameter' do
434
+ allow(request).to receive(:xhr?).and_return(false)
435
+ allow(request).to receive(:params).and_return('access_token' => 'valid_access_token')
436
+ expect(subject).to receive(:verify_token).with('valid_access_token').and_return true
437
+ expect(subject).to receive(:client).and_return(:client)
438
+
439
+ token = subject.build_access_token
440
+ expect(token).to be_instance_of(::OAuth2::AccessToken)
441
+ expect(token.token).to eq('valid_access_token')
442
+ expect(token.client).to eq(:client)
443
+ end
444
+
445
+ it 'reads the code from a json request body' do
446
+ body = StringIO.new(%({"code":"json_access_token"}))
447
+ client = double(:client)
448
+ auth_code = double(:auth_code)
449
+
450
+ allow(request).to receive(:xhr?).and_return(false)
451
+ allow(request).to receive(:content_type).and_return('application/json')
452
+ allow(request).to receive(:body).and_return(body)
453
+ allow(client).to receive(:auth_code).and_return(auth_code)
454
+ expect(subject).to receive(:client).and_return(client)
455
+
456
+ expect(auth_code).to receive(:get_token).with('json_access_token', { redirect_uri: 'postmessage' }, {})
457
+
458
+ subject.build_access_token
459
+ end
460
+
461
+ it 'should use callback_url without query_string if this is not an AJAX request' do
462
+ allow(request).to receive(:xhr?).and_return(false)
463
+ allow(request).to receive(:params).and_return('code' => 'valid_code')
464
+ allow(request).to receive(:content_type).and_return('application/x-www-form-urlencoded')
465
+
466
+ client = double(:client)
467
+ auth_code = double(:auth_code)
468
+ allow(client).to receive(:auth_code).and_return(auth_code)
469
+ allow(subject).to receive(:callback_url).and_return('redirect_uri_without_query_string')
470
+
471
+ expect(subject).to receive(:client).and_return(client)
472
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'redirect_uri_without_query_string' }, {})
473
+ subject.build_access_token
474
+ end
475
+ end
476
+
477
+ describe 'verify_token' do
478
+ before(:each) do
479
+ subject.options.client_options[:connection_build] = proc do |builder|
480
+ builder.request :url_encoded
481
+ builder.adapter :test do |stub|
482
+ stub.get('/v2/oauth2/userinfo?access_token=valid_access_token') do
483
+ [200, { 'Content-Type' => 'application/json; charset=UTF-8' }, JSON.dump(
484
+ {
485
+ identity_provider_display_name: "ORCID",
486
+ sub: "abc",
487
+ preferred_username: "0000-0003-1419-2405@orcid.org",
488
+ identity_provider: "def",
489
+ organization: "DataCite",
490
+ email: "mfenner@datacite.org",
491
+ name: "Martin Fenner",
492
+ exp: 1568009648,
493
+ iss: "https://auth.globus.org"
494
+ }
495
+ )]
496
+ end
497
+ stub.get('/v2/oauth2/userinfo?access_token=invalid_access_token') do
498
+ [400, { 'Content-Type' => 'application/json; charset=UTF-8' }, JSON.dump(error_description: 'Invalid Value')]
499
+ end
500
+ end
501
+ end
502
+ end
503
+
504
+ # it 'should verify token if access_token is valid' do
505
+ # expect(subject.send(:verify_token, 'valid_access_token')).to eq(true)
506
+ # end
507
+
508
+ it 'should raise error if access_token is invalid' do
509
+ expect do
510
+ subject.send(:verify_token, 'invalid_access_token')
511
+ end.to raise_error(OAuth2::Error)
512
+ end
513
+ end
514
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'spec_helper'
4
+
5
+ describe 'Rubocop' do
6
+ it 'should pass with no offenses detected' do
7
+ expect(`rubocop`).to include('no offenses detected')
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ Bundler.setup
5
+
6
+ require "simplecov"
7
+ SimpleCov.start
8
+
9
+ require "rspec"
10
+ require "rack/test"
11
+ require "webmock/rspec"
12
+ require "omniauth-globus"
13
+
14
+ def fixture_path
15
+ File.expand_path("fixtures", __dir__) + "/"
16
+ end
17
+
18
+ RSpec.configure do |config|
19
+ config.include WebMock::API
20
+ config.include Rack::Test::Methods
21
+ config.extend OmniAuth::Test::StrategyMacros, type: :strategy
22
+ config.expect_with :rspec do |c|
23
+ c.syntax = :expect
24
+ end
25
+ end