1# frozen_string_literal: true
2
3require 'spec_helper'
4
5RSpec.describe OmniauthCallbacksController, type: :controller do
6  include LoginHelpers
7
8  describe 'omniauth' do
9    let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
10    let(:additional_info) { {} }
11
12    before do
13      @original_env_config_omniauth_auth = mock_auth_hash(provider.to_s, extern_uid, user.email, additional_info: additional_info )
14      stub_omniauth_provider(provider, context: request)
15    end
16
17    after do
18      Rails.application.env_config['omniauth.auth'] = @original_env_config_omniauth_auth
19    end
20
21    context 'a deactivated user' do
22      let(:provider) { :github }
23      let(:extern_uid) { 'my-uid' }
24
25      before do
26        user.deactivate!
27        post provider
28      end
29
30      it 'allows sign in' do
31        expect(request.env['warden']).to be_authenticated
32      end
33
34      it 'activates the user' do
35        expect(user.reload.active?).to be_truthy
36      end
37
38      it 'shows reactivation flash message after logging in' do
39        expect(flash[:notice]).to eq('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
40      end
41    end
42
43    context 'when sign in is not valid' do
44      let(:provider) { :github }
45      let(:extern_uid) { 'my-uid' }
46
47      it 'renders omniauth error page' do
48        allow_next_instance_of(Gitlab::Auth::OAuth::User) do |instance|
49          allow(instance).to receive(:valid_sign_in?).and_return(false)
50        end
51
52        post provider
53
54        expect(response).to render_template("errors/omniauth_error")
55        expect(response).to have_gitlab_http_status(:unprocessable_entity)
56      end
57    end
58
59    context 'when the user is on the last sign in attempt' do
60      let(:extern_uid) { 'my-uid' }
61
62      before do
63        user.update!(failed_attempts: User.maximum_attempts.pred)
64        subject.response = ActionDispatch::Response.new
65      end
66
67      context 'when using a form based provider' do
68        let(:provider) { :ldap }
69
70        it 'locks the user when sign in fails' do
71          allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username))
72          request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil)
73
74          subject.send(:failure)
75
76          expect(user.reload).to be_access_locked
77        end
78      end
79
80      context 'when using a button based provider' do
81        let(:provider) { :github }
82
83        it 'does not lock the user when sign in fails' do
84          request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil)
85
86          subject.send(:failure)
87
88          expect(user.reload).not_to be_access_locked
89        end
90      end
91    end
92
93    context 'when sign in fails' do
94      include RoutesHelpers
95
96      let(:extern_uid) { 'my-uid' }
97      let(:provider) { :saml }
98
99      def stub_route_as(path)
100        allow(@routes).to receive(:generate_extras) { [path, []] }
101      end
102
103      it 'calls through to the failure handler' do
104        request.env['omniauth.error'] = OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch")
105        request.env['omniauth.error.strategy'] = OmniAuth::Strategies::SAML.new(nil)
106        stub_route_as('/users/auth/saml/callback')
107
108        ForgeryProtection.with_forgery_protection do
109          post :failure
110        end
111
112        expect(flash[:alert]).to match(/Fingerprint mismatch/)
113      end
114    end
115
116    context 'when a redirect fragment is provided' do
117      let(:provider) { :jwt }
118      let(:extern_uid) { 'my-uid' }
119
120      before do
121        request.env['omniauth.params'] = { 'redirect_fragment' => 'L101' }
122      end
123
124      context 'when a redirect url is stored' do
125        it 'redirects with fragment' do
126          post provider, session: { user_return_to: '/fake/url' }
127
128          expect(response).to redirect_to('/fake/url#L101')
129        end
130      end
131
132      context 'when a redirect url with a fragment is stored' do
133        it 'redirects with the new fragment' do
134          post provider, session: { user_return_to: '/fake/url#replaceme' }
135
136          expect(response).to redirect_to('/fake/url#L101')
137        end
138      end
139
140      context 'when no redirect url is stored' do
141        it 'does not redirect with the fragment' do
142          post provider
143
144          expect(response.redirect?).to be true
145          expect(response.location).not_to include('#L101')
146        end
147      end
148    end
149
150    context 'strategies' do
151      shared_context 'sign_up' do
152        let(:user) { double(email: 'new@example.com') }
153
154        before do
155          stub_omniauth_setting(block_auto_created_users: false)
156        end
157      end
158
159      context 'github' do
160        let(:extern_uid) { 'my-uid' }
161        let(:provider) { :github }
162
163        it_behaves_like 'known sign in' do
164          let(:post_action) { post provider }
165        end
166
167        it 'allows sign in' do
168          post provider
169
170          expect(request.env['warden']).to be_authenticated
171        end
172
173        it 'creates an authentication event record' do
174          expect { post provider }.to change { AuthenticationEvent.count }.by(1)
175          expect(AuthenticationEvent.last.provider).to eq(provider.to_s)
176        end
177
178        context 'when user has no linked provider' do
179          let(:user) { create(:user) }
180
181          before do
182            sign_in user
183          end
184
185          it 'links identity' do
186            expect do
187              post provider
188              user.reload
189            end.to change { user.identities.count }.by(1)
190          end
191
192          context 'and is not allowed to link the provider' do
193            before do
194              allow_any_instance_of(IdentityProviderPolicy).to receive(:can?).with(:link).and_return(false)
195            end
196
197            it 'returns 403' do
198              post provider
199
200              expect(response).to have_gitlab_http_status(:forbidden)
201            end
202          end
203        end
204
205        context 'when user with 2FA is unconfirmed' do
206          render_views
207
208          let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider) }
209
210          before do
211            user.update_column(:confirmed_at, nil)
212          end
213
214          it 'redirects to login page' do
215            post provider
216
217            expect(response).to redirect_to(new_user_session_path)
218            expect(flash[:alert]).to match(/You have to confirm your email address before continuing./)
219          end
220        end
221
222        context 'sign up' do
223          include_context 'sign_up'
224
225          it 'is allowed' do
226            post provider
227
228            expect(request.env['warden']).to be_authenticated
229          end
230        end
231
232        context 'when OAuth is disabled' do
233          before do
234            stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
235            settings = Gitlab::CurrentSettings.current_application_settings
236            settings.update!(disabled_oauth_sign_in_sources: [provider.to_s])
237          end
238
239          it 'prevents login via POST' do
240            post provider
241
242            expect(request.env['warden']).not_to be_authenticated
243          end
244
245          it 'shows warning when attempting login' do
246            post provider
247
248            expect(response).to redirect_to new_user_session_path
249            expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
250          end
251
252          it 'allows linking the disabled provider' do
253            user.identities.destroy_all # rubocop: disable Cop/DestroyAll
254            sign_in(user)
255
256            expect { post provider }.to change { user.reload.identities.count }.by(1)
257          end
258
259          context 'sign up' do
260            include_context 'sign_up'
261
262            it 'is prevented' do
263              post provider
264
265              expect(request.env['warden']).not_to be_authenticated
266            end
267          end
268        end
269      end
270
271      context 'auth0' do
272        let(:extern_uid) { '' }
273        let(:provider) { :auth0 }
274
275        it 'does not allow sign in without extern_uid' do
276          post 'auth0'
277
278          expect(request.env['warden']).not_to be_authenticated
279          expect(response).to have_gitlab_http_status(:found)
280          expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
281        end
282      end
283
284      context 'atlassian_oauth2' do
285        let(:provider) { :atlassian_oauth2 }
286        let(:extern_uid) { 'my-uid' }
287
288        context 'when the user and identity already exist' do
289          let(:user) { create(:atlassian_user, extern_uid: extern_uid) }
290
291          it 'allows sign-in' do
292            post :atlassian_oauth2
293
294            expect(request.env['warden']).to be_authenticated
295          end
296
297          it 'sets the username and caller_id in the context' do
298            expect(controller).to receive(:atlassian_oauth2).and_wrap_original do |m, *args|
299              m.call(*args)
300
301              expect(Gitlab::ApplicationContext.current)
302                .to include('meta.user' => user.username,
303                            'meta.caller_id' => 'OmniauthCallbacksController#atlassian_oauth2')
304            end
305
306            post :atlassian_oauth2
307          end
308        end
309
310        context 'for a new user' do
311          before do
312            stub_omniauth_setting(enabled: true, auto_link_user: true, allow_single_sign_on: ['atlassian_oauth2'])
313
314            user.destroy!
315          end
316
317          it 'denies sign-in if sign-up is enabled, but block_auto_created_users is set' do
318            post :atlassian_oauth2
319
320            expect(flash[:alert]).to start_with 'Your account is pending approval'
321          end
322
323          it 'accepts sign-in if sign-up is enabled' do
324            stub_omniauth_setting(block_auto_created_users: false)
325
326            post :atlassian_oauth2
327
328            expect(request.env['warden']).to be_authenticated
329          end
330
331          it 'denies sign-in if sign-up is not enabled' do
332            stub_omniauth_setting(allow_single_sign_on: false, block_auto_created_users: false)
333
334            post :atlassian_oauth2
335
336            expect(flash[:alert]).to start_with 'Signing in using your Atlassian account without a pre-existing GitLab account is not allowed.'
337          end
338        end
339      end
340
341      context 'salesforce' do
342        let(:extern_uid) { 'my-uid' }
343        let(:provider) { :salesforce }
344        let(:additional_info) { { extra: { email_verified: false } } }
345
346        context 'without verified email' do
347          it 'does not allow sign in' do
348            post 'salesforce'
349
350            expect(request.env['warden']).not_to be_authenticated
351            expect(response).to have_gitlab_http_status(:found)
352            expect(controller).to set_flash[:alert].to('Email not verified. Please verify your email in Salesforce.')
353          end
354        end
355
356        context 'with verified email' do
357          include_context 'sign_up'
358          let(:additional_info) { { extra: { email_verified: true } } }
359
360          it 'allows sign in' do
361            post 'salesforce'
362
363            expect(request.env['warden']).to be_authenticated
364          end
365        end
366      end
367    end
368  end
369
370  describe '#saml' do
371    let(:last_request_id) { 'ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685' }
372    let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') }
373    let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
374    let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
375
376    def stub_last_request_id(id)
377      session['last_authn_request_id'] = id
378    end
379
380    before do
381      stub_last_request_id(last_request_id)
382      stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
383                                  providers: [saml_config])
384      mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response)
385      request.env['devise.mapping'] = Devise.mappings[:user]
386      request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
387    end
388
389    it_behaves_like 'known sign in' do
390      let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml') }
391      let(:post_action) { post :saml, params: { SAMLResponse: mock_saml_response } }
392    end
393
394    context 'sign up' do
395      before do
396        user.destroy!
397      end
398
399      it 'denies login if sign up is enabled, but block_auto_created_users is set' do
400        post :saml, params: { SAMLResponse: mock_saml_response }
401
402        expect(flash[:alert]).to start_with 'Your account is pending approval'
403      end
404
405      it 'accepts login if sign up is enabled' do
406        stub_omniauth_setting(block_auto_created_users: false)
407
408        post :saml, params: { SAMLResponse: mock_saml_response }
409
410        expect(request.env['warden']).to be_authenticated
411      end
412
413      it 'denies login if sign up is not enabled' do
414        stub_omniauth_setting(allow_single_sign_on: false, block_auto_created_users: false)
415
416        post :saml, params: { SAMLResponse: mock_saml_response }
417
418        expect(flash[:alert]).to start_with 'Signing in using your saml account without a pre-existing GitLab account is not allowed.'
419      end
420    end
421
422    context 'with GitLab initiated request' do
423      before do
424        post :saml, params: { SAMLResponse: mock_saml_response }
425      end
426
427      context 'when worth two factors' do
428        let(:mock_saml_response) do
429          File.read('spec/fixtures/authentication/saml_response.xml')
430            .gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN')
431        end
432
433        it 'expects user to be signed_in' do
434          expect(request.env['warden']).to be_authenticated
435        end
436      end
437
438      context 'when not worth two factors' do
439        it 'expects user to provide second factor' do
440          expect(response).to render_template('devise/sessions/two_factor')
441          expect(request.env['warden']).not_to be_authenticated
442        end
443      end
444    end
445
446    context 'with IdP initiated request' do
447      let(:user) { create(:user) }
448      let(:last_request_id) { '99999' }
449
450      before do
451        sign_in user
452      end
453
454      it 'lets the user know their account isn\'t linked yet' do
455        post :saml, params: { SAMLResponse: mock_saml_response }
456
457        expect(flash[:notice]).to eq 'Request to link SAML account must be authorized'
458      end
459
460      it 'redirects to profile account page' do
461        post :saml, params: { SAMLResponse: mock_saml_response }
462
463        expect(response).to redirect_to(profile_account_path)
464      end
465
466      it 'doesn\'t link a new identity to the user' do
467        expect { post :saml, params: { SAMLResponse: mock_saml_response } }.not_to change { user.identities.count }
468      end
469
470      it 'sets the username and caller_id in the context' do
471        expect(controller).to receive(:saml).and_wrap_original do |m, *args|
472          m.call(*args)
473
474          expect(Gitlab::ApplicationContext.current)
475            .to include('meta.user' => user.username,
476                        'meta.caller_id' => 'OmniauthCallbacksController#saml')
477        end
478
479        post :saml, params: { SAMLResponse: mock_saml_response }
480      end
481    end
482
483    context 'with a blocked user trying to log in when there are hooks set up' do
484      let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml') }
485
486      subject(:post_action) { post :saml, params: { SAMLResponse: mock_saml_response } }
487
488      before do
489        create(:system_hook)
490        user.block!
491      end
492
493      it { expect { post_action }.not_to raise_error }
494    end
495  end
496
497  describe 'enable admin mode' do
498    include_context 'custom session'
499
500    let(:provider) { :auth0 }
501    let(:extern_uid) { 'my-uid' }
502    let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
503
504    def reauthenticate_and_check_admin_mode(expected_admin_mode:)
505      # Initially admin mode disabled
506      expect(subject.current_user_mode.admin_mode?).to be(false)
507
508      # Trigger OmniAuth admin mode flow and expect admin mode status
509      post provider
510
511      expect(request.env['warden']).to be_authenticated
512      expect(subject.current_user_mode.admin_mode?).to be(expected_admin_mode)
513    end
514
515    context 'user and admin mode requested by the same user' do
516      before do
517        sign_in user
518
519        mock_auth_hash(provider.to_s, extern_uid, user.email, additional_info: {})
520        stub_omniauth_provider(provider, context: request)
521      end
522
523      context 'with a regular user' do
524        it 'cannot be enabled' do
525          reauthenticate_and_check_admin_mode(expected_admin_mode: false)
526
527          expect(response).to redirect_to(root_path)
528        end
529      end
530
531      context 'with an admin user' do
532        let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider, access_level: :admin) }
533
534        context 'when requested first' do
535          before do
536            subject.current_user_mode.request_admin_mode!
537          end
538
539          it 'can be enabled' do
540            reauthenticate_and_check_admin_mode(expected_admin_mode: true)
541
542            expect(response).to redirect_to(admin_root_path)
543          end
544        end
545
546        context 'when not requested first' do
547          it 'cannot be enabled' do
548            reauthenticate_and_check_admin_mode(expected_admin_mode: false)
549
550            expect(response).to redirect_to(root_path)
551          end
552        end
553      end
554    end
555
556    context 'user and admin mode requested by different users' do
557      let(:reauth_extern_uid) { 'another_uid' }
558      let(:reauth_user) { create(:omniauth_user, extern_uid: reauth_extern_uid, provider: provider) }
559
560      before do
561        sign_in user
562
563        mock_auth_hash(provider.to_s, reauth_extern_uid, reauth_user.email, additional_info: {})
564        stub_omniauth_provider(provider, context: request)
565      end
566
567      context 'with a regular user' do
568        it 'cannot be enabled' do
569          reauthenticate_and_check_admin_mode(expected_admin_mode: false)
570
571          expect(response).to redirect_to(profile_account_path)
572        end
573      end
574
575      context 'with an admin user' do
576        let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider, access_level: :admin) }
577        let(:reauth_user) { create(:omniauth_user, extern_uid: reauth_extern_uid, provider: provider, access_level: :admin) }
578
579        context 'when requested first' do
580          before do
581            subject.current_user_mode.request_admin_mode!
582          end
583
584          it 'cannot be enabled' do
585            reauthenticate_and_check_admin_mode(expected_admin_mode: false)
586
587            expect(response).to redirect_to(new_admin_session_path)
588          end
589        end
590
591        context 'when not requested first' do
592          it 'cannot be enabled' do
593            reauthenticate_and_check_admin_mode(expected_admin_mode: false)
594
595            expect(response).to redirect_to(profile_account_path)
596          end
597        end
598      end
599    end
600  end
601end
602