omniauth-microsoft_graph 0.2.1 → 2.2.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.
@@ -0,0 +1,538 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'json'
5
+ require 'omniauth_microsoft_graph'
6
+ require 'stringio'
7
+
8
+ describe OmniAuth::Strategies::MicrosoftGraph 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::MicrosoftGraph.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://login.microsoftonline.com/')
35
+ end
36
+
37
+ it 'has correct authorize_url' do
38
+ expect(subject.client.options[:authorize_url]).to eq('common/oauth2/v2.0/authorize')
39
+ end
40
+
41
+ it 'has correct token_url' do
42
+ expect(subject.client.options[:token_url]).to eq('common/oauth2/v2.0/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 authorize_url' do
53
+ @options = { client_options: { 'authorize_url' => 'https://example.com' } }
54
+ expect(subject.client.options[:authorize_url]).to eq('https://example.com')
55
+ end
56
+
57
+ it 'should allow overriding the token_url' do
58
+ @options = { client_options: { 'token_url' => 'https://example.com' } }
59
+ expect(subject.client.options[:token_url]).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 authorize_url' do
70
+ @options = { client_options: { authorize_url: 'https://example.com' } }
71
+ expect(subject.client.options[:authorize_url]).to eq('https://example.com')
72
+ end
73
+
74
+ it 'should allow overriding the token_url' do
75
+ @options = { client_options: { token_url: 'https://example.com' } }
76
+ expect(subject.client.options[:token_url]).to eq('https://example.com')
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ describe '#authorize_options' do
83
+ %i[display score auth_type scope prompt login_hint domain_hint response_mode].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 'callback_url' do
91
+ it 'should default to nil' do
92
+ @options = {}
93
+ expect(subject.authorize_params['callback_url']).to eq(nil)
94
+ end
95
+
96
+ it 'should set the callback_url parameter if present' do
97
+ @options = { callback_url: 'https://example.com' }
98
+ expect(subject.authorize_params['callback_url']).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 'scope' do
137
+
138
+ it 'should leave base scopes as is' do
139
+ @options = { scope: 'profile' }
140
+ expect(subject.authorize_params['scope']).to eq('profile')
141
+ end
142
+
143
+ it 'should join scopes' do
144
+ @options = { scope: 'profile,email' }
145
+ expect(subject.authorize_params['scope']).to eq('profile email')
146
+ end
147
+
148
+ it 'should deal with whitespace when joining scopes' do
149
+ @options = { scope: 'profile, email' }
150
+ expect(subject.authorize_params['scope']).to eq('profile email')
151
+ end
152
+
153
+ it 'should set default scope to email,profile' do
154
+ expect(subject.authorize_params['scope']).to eq('offline_access openid email profile https://graph.microsoft.com/User.Read')
155
+ end
156
+
157
+ it 'should support space delimited scopes' do
158
+ @options = { scope: 'profile email' }
159
+ expect(subject.authorize_params['scope']).to eq('profile email')
160
+ end
161
+
162
+ it 'should support extremely badly formed scopes' do
163
+ @options = { scope: 'profile email,foo,steve yeah http://example.com' }
164
+ expect(subject.authorize_params['scope']).to eq('profile email https://graph.microsoft.com/foo https://graph.microsoft.com/steve https://graph.microsoft.com/yeah http://example.com')
165
+ end
166
+ end
167
+
168
+ describe 'state' do
169
+ it 'should set the state parameter' do
170
+ @options = { state: 'some_state' }
171
+ expect(subject.authorize_params['state']).to eq('some_state')
172
+ expect(subject.authorize_params[:state]).to eq('some_state')
173
+ expect(subject.session['omniauth.state']).to eq('some_state')
174
+ end
175
+
176
+ it 'should set the omniauth.state dynamically' do
177
+ allow(subject).to receive(:request) { double('Request', params: { 'state' => 'some_state' }, env: {}) }
178
+ expect(subject.authorize_params['state']).to eq('some_state')
179
+ expect(subject.authorize_params[:state]).to eq('some_state')
180
+ expect(subject.session['omniauth.state']).to eq('some_state')
181
+ end
182
+ end
183
+
184
+ describe 'overrides' do
185
+ it 'should include top-level options that are marked as :authorize_options' do
186
+ @options = { authorize_options: %i[scope foo request_visible_actions], scope: 'http://bar', foo: 'baz', hd: 'wow', request_visible_actions: 'something' }
187
+ expect(subject.authorize_params['scope']).to eq('http://bar')
188
+ expect(subject.authorize_params['foo']).to eq('baz')
189
+ expect(subject.authorize_params['hd']).to eq(nil)
190
+ expect(subject.authorize_params['request_visible_actions']).to eq('something')
191
+ end
192
+
193
+ describe 'request overrides' do
194
+ %i[access_type login_hint prompt scope state].each do |k|
195
+ context "authorize option #{k}" do
196
+ let(:request) { double('Request', params: { k.to_s => 'http://example.com' }, cookies: {}, env: {}) }
197
+
198
+ it "should set the #{k} authorize option dynamically in the request" do
199
+ @options = { k: '' }
200
+ expect(subject.authorize_params[k.to_s]).to eq('http://example.com')
201
+ end
202
+ end
203
+ end
204
+
205
+ describe 'custom authorize_options' do
206
+ let(:request) { double('Request', params: { 'foo' => 'something' }, cookies: {}, env: {}) }
207
+
208
+ it 'should support request overrides from custom authorize_options' do
209
+ @options = { authorize_options: [:foo], foo: '' }
210
+ expect(subject.authorize_params['foo']).to eq('something')
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ describe '#authorize_params' do
218
+ it 'should include any authorize params passed in the :authorize_params option' do
219
+ @options = { authorize_params: { request_visible_actions: 'something', foo: 'bar', baz: 'zip' }, hd: 'wow', bad: 'not_included' }
220
+ expect(subject.authorize_params['request_visible_actions']).to eq('something')
221
+ expect(subject.authorize_params['foo']).to eq('bar')
222
+ expect(subject.authorize_params['baz']).to eq('zip')
223
+ expect(subject.authorize_params['bad']).to eq(nil)
224
+ end
225
+ end
226
+
227
+ describe '#token_params' do
228
+ it 'should include any token params passed in the :token_params option' do
229
+ @options = { token_params: { foo: 'bar', baz: 'zip' } }
230
+ expect(subject.token_params['foo']).to eq('bar')
231
+ expect(subject.token_params['baz']).to eq('zip')
232
+ end
233
+ end
234
+
235
+ describe '#token_options' do
236
+ it 'should include top-level options that are marked as :token_options' do
237
+ @options = { token_options: %i[scope foo], scope: 'bar', foo: 'baz', bad: 'not_included' }
238
+ expect(subject.token_params['scope']).to eq('bar')
239
+ expect(subject.token_params['foo']).to eq('baz')
240
+ expect(subject.token_params['bad']).to eq(nil)
241
+ end
242
+ end
243
+
244
+ describe '#callback_path' do
245
+ it 'has the correct default callback path' do
246
+ allow(subject).to receive(:script_name).and_return('')
247
+ expect(subject.callback_path).to eq('/auth/microsoft_graph/callback')
248
+ end
249
+
250
+ it 'should set the callback_path parameter if present' do
251
+ @options = { callback_path: '/auth/foo/callback' }
252
+ expect(subject.callback_path).to eq('/auth/foo/callback')
253
+ end
254
+
255
+ it 'should set the callback_path with script_name if present' do
256
+ allow(subject).to receive(:script_name).and_return('/api/v1')
257
+ expect(subject.callback_path).to eq('/api/v1/auth/microsoft_graph/callback')
258
+ end
259
+ end
260
+
261
+ describe '#info' do
262
+ let(:client) do
263
+ OAuth2::Client.new('abc', 'def') do |builder|
264
+ builder.request :url_encoded
265
+ builder.adapter :test do |stub|
266
+ stub.get('/v1.0/me') { [200, { 'content-type' => 'application/json' }, response_hash.to_json] }
267
+ end
268
+ end
269
+ end
270
+ let(:access_token) { OAuth2::AccessToken.from_hash(client, { 'access_token' => 'a' }) }
271
+ before { allow(subject).to receive(:access_token).and_return(access_token) }
272
+
273
+ context 'with verified email' do
274
+ let(:response_hash) do
275
+ { mail: 'something@domain.invalid' }
276
+ end
277
+
278
+ it 'should return equal email ' do
279
+ expect(subject.info['email']).to eq('something@domain.invalid')
280
+ end
281
+ end
282
+
283
+ context 'when email verification fails' do
284
+ let(:response_hash) { { mail: 'something@domain.invalid' } }
285
+ let(:error) { OmniAuth::MicrosoftGraph::DomainVerificationError.new }
286
+
287
+ before do
288
+ allow(OmniAuth::MicrosoftGraph::DomainVerifier).to receive(:verify!).and_raise(error)
289
+ end
290
+
291
+ it 'raises an error' do
292
+ expect { subject.auth_hash }.to raise_error error
293
+ end
294
+ end
295
+ end
296
+
297
+ describe '#extra' do
298
+ let(:client) do
299
+ OAuth2::Client.new('abc', 'def') do |builder|
300
+ builder.request :url_encoded
301
+ builder.adapter :test do |stub|
302
+ stub.get('/v1.0/me') { [200, { 'content-type' => 'application/json' }, '{"id": "12345"}'] }
303
+ end
304
+ end
305
+ end
306
+ let(:access_token) { OAuth2::AccessToken.from_hash(client, { 'access_token' => 'a' }) }
307
+
308
+ before { allow(subject).to receive(:access_token).and_return(access_token) }
309
+
310
+ describe 'raw_info' do
311
+ it 'should include raw_info' do
312
+ expect(subject.extra['raw_info']).to eq('id' => '12345')
313
+ end
314
+ end
315
+ end
316
+
317
+ describe 'build_access_token' do
318
+ it 'should use a hybrid authorization request_uri if this is an AJAX request with a code parameter' do
319
+ allow(request).to receive(:scheme).and_return('https')
320
+ allow(request).to receive(:url).and_return('https://example.com')
321
+ allow(request).to receive(:xhr?).and_return(true)
322
+ allow(request).to receive(:params).and_return('code' => 'valid_code')
323
+
324
+ client = double(:client)
325
+ auth_code = double(:auth_code)
326
+ allow(client).to receive(:auth_code).and_return(auth_code)
327
+ expect(subject).to receive(:client).and_return(client)
328
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: '/auth/microsoft_graph/callback' }, {})
329
+
330
+ expect(subject).not_to receive(:orig_build_access_token)
331
+ subject.instance_variable_set("@env", {})
332
+ subject.send(:build_access_token)
333
+ end
334
+
335
+ it 'should use a hybrid authorization request_uri if this is an AJAX request (mobile) with a code parameter' do
336
+ allow(request).to receive(:scheme).and_return('https')
337
+ allow(request).to receive(:url).and_return('https://example.com')
338
+ allow(request).to receive(:xhr?).and_return(true)
339
+ allow(request).to receive(:params).and_return('code' => 'valid_code', 'callback_url' => 'localhost')
340
+
341
+ client = double(:client)
342
+ auth_code = double(:auth_code)
343
+ allow(client).to receive(:auth_code).and_return(auth_code)
344
+ expect(subject).to receive(:client).and_return(client)
345
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'localhost' }, {})
346
+
347
+ expect(subject).not_to receive(:orig_build_access_token)
348
+ subject.instance_variable_set("@env", {})
349
+ subject.send(:build_access_token)
350
+ end
351
+
352
+ it 'should use the request_uri from params if this not an AJAX request (request from installed app) with a code parameter' do
353
+ allow(request).to receive(:scheme).and_return('https')
354
+ allow(request).to receive(:url).and_return('https://example.com')
355
+ allow(request).to receive(:xhr?).and_return(false)
356
+ allow(request).to receive(:params).and_return('code' => 'valid_code', 'callback_url' => 'callback_url')
357
+
358
+ client = double(:client)
359
+ auth_code = double(:auth_code)
360
+ allow(client).to receive(:auth_code).and_return(auth_code)
361
+ expect(subject).to receive(:client).and_return(client)
362
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'callback_url' }, {})
363
+
364
+ expect(subject).not_to receive(:orig_build_access_token)
365
+ subject.send(:build_access_token)
366
+ end
367
+
368
+ it 'should read access_token from hash if this is not an AJAX request with a code parameter' do
369
+ allow(request).to receive(:scheme).and_return('https')
370
+ allow(request).to receive(:url).and_return('https://example.com')
371
+ allow(request).to receive(:xhr?).and_return(false)
372
+ allow(request).to receive(:params).and_return('access_token' => 'valid_access_token')
373
+ expect(subject).to receive(:verify_token).with('valid_access_token').and_return true
374
+ expect(subject).to receive(:client).and_return(:client)
375
+
376
+ token = subject.send(:build_access_token)
377
+ expect(token).to be_instance_of(::OAuth2::AccessToken)
378
+ expect(token.token).to eq('valid_access_token')
379
+ expect(token.client).to eq(:client)
380
+ end
381
+
382
+ it 'reads the code from a json request body' do
383
+ body = StringIO.new(%({"code":"json_access_token"}))
384
+ client = double(:client)
385
+ auth_code = double(:auth_code)
386
+
387
+ allow(request).to receive(:scheme).and_return('https')
388
+ allow(request).to receive(:url).and_return('https://example.com')
389
+ allow(request).to receive(:xhr?).and_return(false)
390
+ allow(request).to receive(:content_type).and_return('application/json')
391
+ allow(request).to receive(:body).and_return(body)
392
+ allow(client).to receive(:auth_code).and_return(auth_code)
393
+ expect(subject).to receive(:client).and_return(client)
394
+
395
+ expect(auth_code).to receive(:get_token).with('json_access_token', { redirect_uri: '/auth/microsoft_graph/callback' }, {})
396
+
397
+ subject.send(:build_access_token)
398
+ end
399
+
400
+ it 'should use callback_url without query_string if this is not an AJAX request' do
401
+ allow(request).to receive(:scheme).and_return('https')
402
+ allow(request).to receive(:url).and_return('https://example.com')
403
+ allow(request).to receive(:xhr?).and_return(false)
404
+ allow(request).to receive(:params).and_return('code' => 'valid_code')
405
+ allow(request).to receive(:content_type).and_return('application/x-www-form-urlencoded')
406
+
407
+ client = double(:client)
408
+ auth_code = double(:auth_code)
409
+ allow(client).to receive(:auth_code).and_return(auth_code)
410
+ allow(subject).to receive(:callback_url).and_return('callback_url_without_query_string')
411
+
412
+ expect(subject).to receive(:client).and_return(client)
413
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'callback_url_without_query_string' }, {})
414
+ subject.send(:build_access_token)
415
+ end
416
+ end
417
+
418
+ describe 'verify_token' do
419
+ before(:each) do
420
+ subject.options.client_options[:connection_build] = proc do |builder|
421
+ builder.request :url_encoded
422
+ builder.adapter :test do |stub|
423
+ stub.get('/v1.0/me?access_token=valid_access_token') do
424
+ [200, { 'Content-Type' => 'application/json; charset=UTF-8' }, JSON.dump(
425
+ aud: '000000000000.apps.googleusercontent.com',
426
+ id: '123456789',
427
+ email: 'example@example.com',
428
+ access_type: 'offline',
429
+ scope: 'profile email',
430
+ expires_in: 436
431
+ )]
432
+ end
433
+ stub.get('/v1.0/me?access_token=invalid_access_token') do
434
+ [400, { 'Content-Type' => 'application/json; charset=UTF-8' }, JSON.dump(error_description: 'Invalid Value')]
435
+ end
436
+ end
437
+ end
438
+ end
439
+
440
+ it 'should verify token if access_token is valid and app_id equals' do
441
+ subject.options.client_id = '000000000000.apps.googleusercontent.com'
442
+ expect(subject.send(:verify_token, 'valid_access_token')).to eq(true)
443
+ end
444
+
445
+ it 'should verify token if access_token is valid and app_id authorized' do
446
+ subject.options.authorized_client_ids = ['000000000000.apps.googleusercontent.com']
447
+ expect(subject.send(:verify_token, 'valid_access_token')).to eq(true)
448
+ end
449
+
450
+ it 'should not verify token if access_token is valid but app_id is false' do
451
+ expect(subject.send(:verify_token, 'valid_access_token')).to eq(false)
452
+ end
453
+
454
+ it 'should raise error if access_token is invalid' do
455
+ expect do
456
+ subject.send(:verify_token, 'invalid_access_token')
457
+ end.to raise_error(OAuth2::Error)
458
+ end
459
+ end
460
+
461
+ describe 'Yammer profile endpoint support' do
462
+ describe '#profile_endpoint' do
463
+ context 'when no profile endpoint is determined' do
464
+ it 'defaults to Microsoft Graph profile URL' do
465
+ expect(subject.profile_endpoint).to eq('https://graph.microsoft.com/v1.0/me')
466
+ end
467
+ end
468
+
469
+ context 'when profile endpoint is already set' do
470
+ before { subject.instance_variable_set(:@profile_endpoint, 'https://custom.endpoint.com') }
471
+
472
+ it 'returns the previously set endpoint' do
473
+ expect(subject.profile_endpoint).to eq('https://custom.endpoint.com')
474
+ end
475
+ end
476
+ end
477
+
478
+ describe '#determine_profile_endpoint' do
479
+ let(:request) { double('Request', env: request_env) }
480
+
481
+ context 'when scope includes Yammer access_as_user scope' do
482
+ let(:request_env) { { 'omniauth.params' => { 'scope' => 'https://api.yammer.com/access_as_user' } } }
483
+
484
+ it 'returns Yammer profile URL' do
485
+ expect(subject.determine_profile_endpoint(request)).to eq('https://www.yammer.com/api/v1/users/current.json')
486
+ end
487
+ end
488
+
489
+ context 'when scope includes Yammer user_impersonation scope' do
490
+ let(:request_env) { { 'omniauth.params' => { 'scope' => 'openid profile https://api.yammer.com/user_impersonation' } } }
491
+
492
+ it 'returns Yammer profile URL' do
493
+ expect(subject.determine_profile_endpoint(request)).to eq('https://www.yammer.com/api/v1/users/current.json')
494
+ end
495
+ end
496
+
497
+ context 'when scope includes Yammer scope among other scopes' do
498
+ let(:request_env) { { 'omniauth.params' => { 'scope' => 'offline_access openid email profile https://api.yammer.com/access_as_user User.Read' } } }
499
+
500
+ it 'returns Yammer profile URL' do
501
+ expect(subject.determine_profile_endpoint(request)).to eq('https://www.yammer.com/api/v1/users/current.json')
502
+ end
503
+ end
504
+
505
+ context 'when scope includes multiple Yammer scopes' do
506
+ let(:request_env) { { 'omniauth.params' => { 'scope' => 'openid profile https://api.yammer.com/access_as_user https://api.yammer.com/user_impersonation' } } }
507
+
508
+ it 'returns Yammer profile URL' do
509
+ expect(subject.determine_profile_endpoint(request)).to eq('https://www.yammer.com/api/v1/users/current.json')
510
+ end
511
+ end
512
+
513
+ context 'when scope does not include any Yammer scopes' do
514
+ let(:request_env) { { 'omniauth.params' => { 'scope' => 'openid profile User.Read' } } }
515
+
516
+ it 'returns Microsoft Graph profile URL' do
517
+ expect(subject.determine_profile_endpoint(request)).to eq('https://graph.microsoft.com/v1.0/me')
518
+ end
519
+ end
520
+
521
+ context 'when scope is nil' do
522
+ let(:request_env) { { 'omniauth.params' => { 'scope' => nil } } }
523
+
524
+ it 'returns Microsoft Graph profile URL' do
525
+ expect(subject.determine_profile_endpoint(request)).to eq('https://graph.microsoft.com/v1.0/me')
526
+ end
527
+ end
528
+
529
+ context 'when omniauth.params is nil' do
530
+ let(:request_env) { { 'omniauth.params' => nil } }
531
+
532
+ it 'returns Microsoft Graph profile URL' do
533
+ expect(subject.determine_profile_endpoint(request)).to eq('https://graph.microsoft.com/v1.0/me')
534
+ end
535
+ end
536
+ end
537
+ end
538
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.join('bundler', 'setup')
4
+ require 'rspec'