# frozen_string_literal: true require 'spec_helper' RSpec.describe OmniauthCallbacksController, :with_current_organization, type: :controller, feature_category: :system_access do include LoginHelpers shared_examples 'stores value for provider_2FA to session according to saml response' do let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml') } context 'with IDP bypass two factor request' do before do stub_omniauth_setting(allow_bypass_two_factor: true) saml_config.args[:upstream_two_factor_authn_contexts] << "urn:oasis:names:tc:SAML:2.0:ac:classes:Password" sign_in user end it "sets the session variable for provider 2FA" do post :saml, params: { SAMLResponse: mock_saml_response } expect(session[:provider_2FA]).to eq(true) end end context 'without IDP bypass two factor request' do before do stub_omniauth_setting(allow_bypass_two_factor: true) sign_in user end it "sets the session variable as nil" do post :saml, params: { SAMLResponse: mock_saml_response } expect(session[:provider_2FA]).to be_nil end end end shared_examples "sets provider_2FA session variable according to bypass_two_factor return value" do context 'when method returns true' do it "sets value as true" do allow_next_instance_of(Gitlab::Auth::OAuth::User) do |instance| allow(instance).to receive(:bypass_two_factor?).and_return(true) end post provider expect(session[:provider_2FA]).to be(true) end end context 'when method returns false' do it "sets value to nil" do allow_next_instance_of(Gitlab::Auth::OAuth::User) do |instance| allow(instance).to receive(:bypass_two_factor?).and_return(false) end post provider expect(session[:provider_2FA]).to be_nil end end end shared_examples 'omniauth sign in that remembers user' do before do stub_omniauth_setting(allow_bypass_two_factor: allow_bypass_two_factor) (request.env['omniauth.params'] ||= {}).deep_merge!('remember_me' => omniauth_params_remember_me) end if params[:call_remember_me] it 'calls devise method remember_me' do expect(controller).to receive(:remember_me).with(user).and_call_original post_action end else it 'does not calls devise method remember_me' do expect(controller).not_to receive(:remember_me) post_action end end end shared_examples 'omniauth sign in that remembers user with two factor enabled' do using RSpec::Parameterized::TableSyntax subject(:post_action) { post provider } where(:allow_bypass_two_factor, :omniauth_params_remember_me, :call_remember_me) do true | '1' | true true | '0' | false true | nil | false false | '1' | false false | '0' | false false | nil | false end with_them do it_behaves_like 'omniauth sign in that remembers user' end end shared_examples 'omniauth sign in that remembers user with two factor disabled' do context "when user selects remember me for omniauth sign in flow" do using RSpec::Parameterized::TableSyntax subject(:post_action) { post provider } where(:allow_bypass_two_factor, :omniauth_params_remember_me, :call_remember_me) do true | '1' | true true | '0' | false true | nil | false false | '1' | true false | '0' | false false | nil | false end with_them do it_behaves_like 'omniauth sign in that remembers user' end end end shared_examples 'when user has dismissed broadcast messages' do let_it_be(:message_banner) { create(:broadcast_message, broadcast_type: :banner) } let_it_be(:message_notification) { create(:broadcast_message, broadcast_type: :notification) } let_it_be(:other_message) { create(:broadcast_message, broadcast_type: :banner) } before do create(:broadcast_message_dismissal, broadcast_message: message_banner, user: user) create(:broadcast_message_dismissal, broadcast_message: message_notification, user: user) create(:broadcast_message_dismissal, broadcast_message: other_message) sign_in user end it 'creates dismissed cookies based on db records' do expect(cookies["hide_broadcast_message_#{message_banner.id}"]).to be_nil expect(cookies["hide_broadcast_message_#{message_notification.id}"]).to be_nil expect(cookies["hide_broadcast_message_#{other_message.id}"]).to be_nil post_action expect(cookies["hide_broadcast_message_#{message_banner.id}"]).to be(true) expect(cookies["hide_broadcast_message_#{message_notification.id}"]).to be(true) expect(cookies["hide_broadcast_message_#{other_message.id}"]).to be_nil end end describe 'omniauth', :with_current_organization do let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) } let(:omniauth_email) { user.email } let(:additional_info) { {} } before do @original_env_config_omniauth_auth = mock_auth_hash(provider.to_s, extern_uid, omniauth_email, additional_info: additional_info) stub_omniauth_provider(provider, context: request) end after do Rails.application.env_config['omniauth.auth'] = @original_env_config_omniauth_auth end context 'when authentication succeeds', :prometheus do let(:extern_uid) { 'my-uid' } let(:provider) { :github } context 'without signed-in user' do it 'increments Prometheus counter' do expect { post(provider) }.to( change do Gitlab::Metrics.client .get(:gitlab_omniauth_login_total) &.get(omniauth_provider: 'github', status: 'succeeded') .to_f end.by(1) ) end it 'creates an authentication audit event' do expect { post provider }.to change { AuditEvent.where("details LIKE '%authenticated_with_oauth%'").count }.by(1) end end context 'with signed-in user', :prometheus do before do sign_in user end it_behaves_like 'when user has dismissed broadcast messages' do let(:post_action) { post provider } end it 'increments Prometheus counter' do expect { post(provider) }.to( change do Gitlab::Metrics.client .get(:gitlab_omniauth_login_total) &.get(omniauth_provider: 'github', status: 'succeeded') .to_f end.by(1) ) end end it_behaves_like "sets provider_2FA session variable according to bypass_two_factor return value" end context 'for a deactivated user' do let(:provider) { :github } let(:extern_uid) { 'my-uid' } before do user.deactivate! post provider end it 'allows sign in' do expect(request.env['warden']).to be_authenticated end it 'activates the user' do expect(user.reload.active?).to be_truthy end it 'shows reactivation flash message after logging in' do expect(flash[:notice]).to eq('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.') end end context 'when sign in is not valid' do let(:provider) { :github } let(:extern_uid) { 'my-uid' } before do allow_next_instance_of(Gitlab::Auth::OAuth::User) do |instance| allow(instance).to receive(:valid_sign_in?).and_return(false) end post provider end it 'renders omniauth error page' do expect(response).to render_template("errors/omniauth_error") expect(response).to have_gitlab_http_status(:unprocessable_entity) end it "does not set provider_2FA in session" do expect(session[:provider_2FA]).to be_nil end end context 'when the user is trying to sign-in from restricted country' do let(:extern_uid) { 'my-uid' } let(:provider) { :github } let(:error_message) { "It looks like you are visiting GitLab from Mainland China, Macau, or Hong Kong" } before do allow(controller).to receive(:allowed_new_user?).and_raise(OmniauthCallbacksController::SignUpFromRestrictedCountyError) post provider end it 'redirects to root path with message' do expect(response).to redirect_to new_user_session_path expect(flash[:alert]).to include(error_message) end end context 'when the user is on the last sign in attempt' do let(:extern_uid) { 'my-uid' } before do user.update!(failed_attempts: User.maximum_attempts.pred) subject.set_response!(ActionDispatch::Response.new) end context 'when using a form based provider' do let(:provider) { :ldap } it 'locks the user when sign in fails' do allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username)) request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil) subject.send(:failure) expect(user.reload).to be_access_locked end end context 'when using a button based provider' do let(:provider) { :github } it 'does not lock the user when sign in fails' do request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil) subject.send(:failure) expect(user.reload).not_to be_access_locked end end end context 'when sign in fails', :prometheus do include RoutesHelpers let(:extern_uid) { 'my-uid' } let(:provider) { :saml } before do request.env['omniauth.error'] = OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch") request.env['omniauth.error.strategy'] = OmniAuth::Strategies::SAML.new(nil) allow(@routes).to receive(:generate_extras).and_return(['/users/auth/saml/callback', []]) end it 'calls through to the failure handler' do ForgeryProtection.with_forgery_protection do post :failure end expect(flash[:alert]).to match(/Fingerprint mismatch/) end it 'increments Prometheus counter' do ForgeryProtection.with_forgery_protection do expect { post :failure }.to( change do Gitlab::Metrics.client .get(:gitlab_omniauth_login_total) &.get(omniauth_provider: 'saml', status: 'failed') .to_f end.by(1) ) end end end context 'when a redirect fragment is provided' do let(:provider) { :jwt } let(:extern_uid) { 'my-uid' } before do request.env['omniauth.params'] = { 'redirect_fragment' => 'L101' } end it_behaves_like 'omniauth sign in that remembers user with two factor disabled' context 'when a redirect url is stored' do it 'redirects with fragment' do post provider, session: { user_return_to: '/fake/url' } expect(response).to redirect_to('/fake/url#L101') end end context 'when a redirect url with a fragment is stored' do it 'redirects with the new fragment' do post provider, session: { user_return_to: '/fake/url#replaceme' } expect(response).to redirect_to('/fake/url#L101') end end context 'when no redirect url is stored' do it 'does not redirect with the fragment' do post provider expect(response.redirect?).to be true expect(response.location).not_to include('#L101') end end context 'when a user has 2FA enabled' do let(:user) { create(:omniauth_user, :two_factor, extern_uid: extern_uid, provider: provider) } it_behaves_like 'omniauth sign in that remembers user with two factor enabled' end context 'when redirect fragment contains special characters' do before do request.env['omniauth.params'] = { 'redirect_fragment' => 'confirm-merge_request_diff_id-context' } end it 'redirects with fragment' do post provider, session: { user_return_to: '/fake/url' } expect(response).to redirect_to('/fake/url#confirm-merge_request_diff_id-context') end end context 'when stored redirect fragment is malicious' do let(:malicious_redirect_fragment) { '#code=test_code&' } before do request.env['omniauth.params'] = { 'redirect_fragment' => malicious_redirect_fragment } end it 'fails login and redirects to login path' do post provider, session: { user_return_to: '/fake/url#replaceme' } expect(response.redirect?).to be true expect(response).to redirect_to(new_user_session_path) expect(flash[:alert]).to match(/Invalid state/) end context 'when fragment has encoded content' do let_it_be(:malicious_redirect_fragment, reload: true) { '#code%3Dtest_code&L90' } it 'fails login and redirects to login path' do post provider, session: { user_return_to: '/fake/url#replaceme' } expect(response.redirect?).to be true expect(response).to redirect_to(new_user_session_path) expect(flash[:alert]).to match(/Invalid state/) end end end end context 'with strategies' do shared_context 'with sign_up' do let(:user) { double(email: 'new@example.com') } before do stub_omniauth_setting(block_auto_created_users: false) end end context 'for github' do let(:extern_uid) { 'my-uid' } let(:provider) { :github } it_behaves_like 'known sign in' do let(:post_action) { post provider } end it 'allows sign in' do post provider expect(request.env['warden']).to be_authenticated end it 'creates an authentication event record' do expect { post provider }.to change { AuthenticationEvent.count }.by(1) expect(AuthenticationEvent.last.provider).to eq(provider.to_s) end context 'when user has no linked provider' do let(:user) { create(:user) } before do sign_in user end it 'links identity' do expect do post provider user.reload end.to change { user.identities.count }.by(1) end context 'and is not allowed to link the provider' do before do allow_any_instance_of(IdentityProviderPolicy).to receive(:can?).with(:link).and_return(false) end it 'returns 403' do post provider expect(response).to have_gitlab_http_status(:forbidden) end end end it_behaves_like 'omniauth sign in that remembers user with two factor disabled' context 'when a user has 2FA enabled' do render_views let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider) } context 'when a user is unconfirmed' do before do stub_application_setting_enum('email_confirmation_setting', 'hard') user.update!(confirmed_at: nil) end it 'redirects to login page' do post provider expect(response).to redirect_to(new_user_session_path) expect(flash[:alert]).to match(/You have to confirm your email address before continuing./) end end context 'when a user is confirmed' do it 'returns 200 response' do expect(response).to have_gitlab_http_status(:ok) end end it_behaves_like 'omniauth sign in that remembers user with two factor enabled' end context 'for sign up' do include_context 'with sign_up' it 'is allowed' do post provider expect(request.env['warden']).to be_authenticated end it_behaves_like Onboarding::Redirectable do let(:email) { user.email } subject(:post_create) { post provider } end end context 'when OAuth is disabled' do before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') settings = Gitlab::CurrentSettings.current_application_settings settings.update!(disabled_oauth_sign_in_sources: [provider.to_s]) end it 'prevents login via POST' do post provider expect(request.env['warden']).not_to be_authenticated end it 'shows warning when attempting login' do post provider expect(response).to redirect_to new_user_session_path expect(flash[:alert]).to eq('Signing in using GitHub has been disabled') end it 'allows linking the disabled provider' do user.identities.destroy_all # rubocop: disable Cop/DestroyAll sign_in(user) expect { post provider }.to change { user.reload.identities.count }.by(1) end context 'for sign up' do include_context 'with sign_up' it 'is prevented' do post provider expect(request.env['warden']).not_to be_authenticated end end end end context 'for auth0' do let(:extern_uid) { '' } let(:provider) { :auth0 } it_behaves_like 'omniauth sign in that remembers user with two factor disabled' do let(:extern_uid) { 'my-uid' } end it 'does not allow sign in without extern_uid' do post 'auth0' expect(request.env['warden']).not_to be_authenticated expect(response).to have_gitlab_http_status(:found) expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.') end context 'when a user has 2FA enabled' do let(:user) { create(:omniauth_user, :two_factor, extern_uid: extern_uid, provider: provider) } it_behaves_like 'omniauth sign in that remembers user with two factor enabled' do let(:extern_uid) { 'my-uid' } end end end context 'for atlassian_oauth2' do let(:provider) { :atlassian_oauth2 } let(:extern_uid) { 'my-uid' } context 'when the user and identity already exist' do let(:user) { create(:atlassian_user, extern_uid: extern_uid) } it_behaves_like 'omniauth sign in that remembers user with two factor disabled' it 'allows sign-in' do post :atlassian_oauth2 expect(request.env['warden']).to be_authenticated end context 'when a user has 2FA enabled' do let(:user) { create(:atlassian_user, :two_factor, extern_uid: extern_uid) } it_behaves_like 'omniauth sign in that remembers user with two factor enabled' end end context 'for a new user' do before do @original_url = Settings.gitlab.url Settings.gitlab.url = 'https://www.example.com:43/gitlab' stub_omniauth_setting(enabled: true, auto_link_user: true, allow_single_sign_on: ['atlassian_oauth2']) user.destroy! end after do Settings.gitlab.url = @original_url end it 'denies sign-in if sign-up is enabled, but block_auto_created_users is set' do post :atlassian_oauth2 expect(flash[:alert]).to start_with 'Your account is pending approval' end it 'accepts sign-in if sign-up is enabled' do stub_omniauth_setting(block_auto_created_users: false) post :atlassian_oauth2 expect(request.env['warden']).to be_authenticated end it 'denies sign-in if sign-up is not enabled' do stub_omniauth_setting(allow_single_sign_on: false, block_auto_created_users: false) post :atlassian_oauth2 expect(flash[:alert]).to eq('Signing in using your Atlassian account without a pre-existing account in example.com:43/gitlab is not allowed. Create an account in example.com:43/gitlab first, and then connect it to your Atlassian account.') end end end context 'for salesforce' do let(:extern_uid) { 'my-uid' } let(:provider) { :salesforce } let(:additional_info) { { extra: { email_verified: false } } } context 'without verified email' do it 'does not allow sign in' do post 'salesforce' expect(request.env['warden']).not_to be_authenticated expect(response).to have_gitlab_http_status(:found) expect(controller).to set_flash[:alert].to('Email not verified. Please verify your email in Salesforce.') end end context 'with verified email' do include_context 'with sign_up' let(:additional_info) { { extra: { email_verified: true } } } it_behaves_like 'omniauth sign in that remembers user with two factor disabled' do let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) } end it 'allows sign in' do post 'salesforce' expect(request.env['warden']).to be_authenticated end context 'when a user has 2FA enabled' do let(:user) { create(:omniauth_user, :two_factor, extern_uid: extern_uid, provider: provider) } it_behaves_like 'omniauth sign in that remembers user with two factor enabled' end end end end context 'with snowplow tracking', :snowplow do let(:provider) { 'google_oauth2' } let(:extern_uid) { 'my-uid' } context 'when sign_in' do it 'does not track the event' do post provider expect_no_snowplow_event end end context 'when sign_up' do let(:user) { double(email: generate(:email)) } it 'tracks the event' do post provider expect_snowplow_event( category: described_class.name, action: "#{provider}_sso", user: User.find_by(email: user.email) ) end end end context "when user's identity with untrusted extern_uid" do let(:provider) { 'bitbucket' } let(:extern_uid) { 'untrusted-uid' } let(:omniauth_email) { 'bitbucketuser@example.com' } before do user.identities.with_extern_uid(provider, extern_uid).update!(trusted_extern_uid: false) end it 'shows warning when attempting login' do post provider expect(response).to redirect_to new_user_session_path expect(flash[:alert]).to eq('Signing in using your Bitbucket account has been disabled for security reasons. Please sign in to your GitLab account using another authentication method and reconnect to your Bitbucket account.') end end end describe '#openid_connect' do let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) } let(:extern_uid) { 'my-uid' } let(:provider) { 'openid_connect' } before do prepare_provider_route('openid_connect') mock_auth_hash(provider, extern_uid, user.email, additional_info: {}) request.env['devise.mapping'] = Devise.mappings[:user] request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth'] end it_behaves_like 'known sign in' do let(:post_action) { post provider } end it_behaves_like 'omniauth sign in that remembers user with two factor disabled' it_behaves_like 'when user has dismissed broadcast messages' do let(:post_action) { post provider } end it 'allows sign in' do post provider expect(request.env['warden']).to be_authenticated end context 'when a user has 2FA enabled' do let(:user) { create(:omniauth_user, :two_factor, extern_uid: extern_uid, provider: provider) } it_behaves_like 'omniauth sign in that remembers user with two factor enabled' end context 'when multiple OIDC providers are configured' do let(:oidc_providers) do [ { 'name' => 'openid_connect', 'args' => { 'name' => 'openid_connect', 'strategy_class' => "OmniAuth::Strategies::OpenIDConnect", 'client_options' => { 'gitlab' => {} } } }, { 'name' => 'openid_connect2', 'args' => { 'name' => 'openid_connect2', 'strategy_class' => "OmniAuth::Strategies::OpenIDConnect", 'client_options' => { 'gitlab' => {} } } } ] end let(:provider_settings) { oidc_providers.map { |provider| GitlabSettings::Options.new(provider) } } let(:provider_names) { provider_settings.map(&:name).map(&:to_s) } context 'when a non-default provider is used', :aggregate_failures do let(:provider2) { provider_names[1] } let(:user) { create(:omniauth_user, extern_uid: "my-uid2", provider: provider2.to_sym) } controller(described_class) do alias_method :openid_connect2, :handle_omniauth end before do prepare_provider_route(provider2) allow(routes).to receive(:generate_extras).and_return(["/users/auth/#{provider2}/callback", []]) stub_omniauth_setting( enabled: true, block_auto_created_users: false, allow_single_sign_on: provider_names, providers: provider_settings ) request.env['devise.mapping'] = Devise.mappings[:user] request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth'] mock_auth_hash(provider2, "my-uid2", user.email) stub_omniauth_provider(provider2, context: request) end it 'authenticates a user with the non-default provider' do prov_names = provider_names.map(&:to_sym) expect(controller).to receive(provider2.to_sym).and_call_original expect(AuthHelper.oidc_providers).to match(prov_names) post provider2.to_sym expect(request.env['warden']).to be_authenticated end end end it_behaves_like "sets provider_2FA session variable according to bypass_two_factor return value" context 'for step-up authentication' do context 'with different step-up authentication configurations' do using RSpec::Parameterized::TableSyntax let(:ommiauth_provider_config_with_step_up_auth) do GitlabSettings::Options.new( name: "openid_connect", step_up_auth: { admin_mode: { id_token: { required: required_id_token_claims, included: included_id_token_claims } } } ) end before do mock_auth_hash(provider, extern_uid, user.email, additional_info: { extra: { raw_info: mock_auth_hash_extra_raw_info } }) request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth'] session['omniauth_step_up_auth'] = { 'openid_connect' => { 'admin_mode' => { 'state' => 'requested' } } } stub_omniauth_setting(enabled: true, auto_link_user: true, block_auto_created_users: false, providers: [ommiauth_provider_config_with_step_up_auth]) end where(:required_id_token_claims, :included_id_token_claims, :mock_auth_hash_extra_raw_info, :step_up_auth_authenticated) do { claim_1: 'gold' } | nil | { claim_1: 'gold' } | 'succeeded' { claim_1: 'gold' } | nil | { claim_1: 'gold', claim_2: 'mfa' } | 'succeeded' { claim_1: 'gold' } | nil | { claim_1: 'silver' } | 'failed' { claim_1: 'gold' } | nil | { claim_1: 'silver', claim_2: 'other_amr' } | 'failed' { claim_1: 'gold' } | nil | { claim_1: nil } | 'failed' { claim_1: 'gold' } | nil | { claim_3: 'other_value' } | 'failed' { claim_1: 'gold' } | nil | {} | 'failed' nil | { claim_2: %w[mfa fpt] } | { claim_2: 'mfa', claim_3: 'other_value' } | 'succeeded' nil | { claim_2: %w[mfa fpt] } | { claim_2: 'fpt' } | 'succeeded' nil | { claim_2: %w[mfa fpt] } | { claim_2: 'other_amr' } | 'failed' { claim_1: 'gold' } | { claim_1: ['gold'] } | { claim_1: 'gold' } | 'succeeded' { claim_1: 'gold' } | { claim_1: %w[gold silver] } | { claim_1: 'gold' } | 'succeeded' { claim_1: 'gold' } | { claim_1: %w[gold silver] } | { claim_1: 'silver' } | 'failed' { claim_1: 'gold' } | { claim_2: %w[mfa fpt] } | { claim_1: 'gold', claim_2: 'mfa' } | 'succeeded' { claim_1: 'gold' } | { claim_2: %w[mfa fpt] } | { claim_1: 'gold', claim_2: 'other_amr' } | 'failed' { claim_1: 'gold' } | { claim_2: %w[mfa fpt] } | { claim_1: 'silver', claim_2: 'mfa' } | 'failed' { claim_1: 'gold' } | { claim_2: %w[mfa fpt] } | { claim_1: 'silver', claim_2: 'other_amr' } | 'failed' end with_them do context 'when user is signed in' do before do sign_in user end it 'evaluates step-up authentication conditions and stores result in session' do get provider expect(session.to_h.dig('omniauth_step_up_auth', 'openid_connect', 'admin_mode', 'state')) .to eq step_up_auth_authenticated end context 'when feature flag :omniauth_step_up_auth_for_admin_mode disabled' do before do stub_feature_flags(omniauth_step_up_auth_for_admin_mode: false) session.delete 'omniauth_step_up_auth' end it 'does not store step-up authentication evaluation result in session' do get provider expect(session).not_to include 'omniauth_step_up_auth' end end end context 'when user is not signed in' do before do session.delete 'omniauth_step_up_auth' end it 'does not store step-up authentication evaluation result in session' do get provider expect(session).not_to include 'omniauth_step_up_auth' end end end end context 'without step-up authentication configuration' do let(:ommiauth_provider_config_with_step_up_auth) { GitlabSettings::Options.new(name: "openid_connect") } it 'does not add session key "step_up_auth"' do get provider expect(session).not_to include 'step_up_auth' end end end it 'does not log saml_response for debugging' do expect(Gitlab::AuthLogger).not_to receive(:info).with(payload_type: 'saml_response', saml_response: anything) get provider end end describe '#saml' do let(:last_request_id) { 'ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685' } let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') } let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') } let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts } def stub_last_request_id(id) session['last_authn_request_id'] = id end before do stub_last_request_id(last_request_id) stub_omniauth_saml_config( enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config] ) mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response) request.env['devise.mapping'] = Devise.mappings[:user] request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth'] end it_behaves_like 'known sign in' do let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml') } let(:post_action) { post :saml, params: { SAMLResponse: mock_saml_response } } end it_behaves_like 'when user has dismissed broadcast messages' do let(:post_action) { post :saml, params: { SAMLResponse: mock_saml_response } } end context 'for sign up', :with_current_organization do before do user.destroy! end it 'denies login if sign up is enabled, but block_auto_created_users is set' do post :saml, params: { SAMLResponse: mock_saml_response } expect(flash[:alert]).to start_with 'Your account is pending approval' end it 'accepts login if sign up is enabled' do stub_omniauth_setting(block_auto_created_users: false) post :saml, params: { SAMLResponse: mock_saml_response } expect(request.env['warden']).to be_authenticated end describe 'when registering a new account is allowed' do before do allow(Gitlab::CurrentSettings).to receive(:allow_signup?).and_return(true) end it 'denies login if sign up is not enabled' do stub_omniauth_setting(allow_single_sign_on: false, block_auto_created_users: false) post :saml, params: { SAMLResponse: mock_saml_response } expect(flash[:alert]).to eq("Signing in using your SAML account without a pre-existing account in #{Gitlab.config.gitlab.host} is not allowed. Create an account in #{Gitlab.config.gitlab.host} first, and then connect it to your SAML account.") expect(response).to redirect_to(new_user_registration_path) end end describe 'when registering a new account is not allowed' do before do allow(Gitlab::CurrentSettings).to receive(:allow_signup?).and_return(false) end it 'denies login if sign up is not enabled' do stub_omniauth_setting(allow_single_sign_on: false, block_auto_created_users: false) post :saml, params: { SAMLResponse: mock_saml_response } expect(flash[:alert]).to eq("Signing in using your SAML account without a pre-existing account in #{Gitlab.config.gitlab.host} is not allowed.") expect(response).to redirect_to(new_user_session_path) end end it 'logs saml_response for debugging' do expect(Gitlab::AuthLogger).to receive(:info).with(payload_type: 'saml_response', saml_response: anything) post :saml, params: { SAMLResponse: mock_saml_response } end end context 'with GitLab initiated request' do before do post :saml, params: { SAMLResponse: mock_saml_response } end context 'when worth two factors' do let(:mock_saml_response) do File.read('spec/fixtures/authentication/saml_response.xml') .gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN') end it 'expects user to be signed_in' do expect(request.env['warden']).to be_authenticated end end context 'when not worth two factors' do it 'expects user to provide second factor' do expect(response).to render_template('devise/sessions/two_factor') expect(request.env['warden']).not_to be_authenticated end end end context 'with IdP initiated request' do let(:user) { create(:user) } let(:last_request_id) { '99999' } before do sign_in user end it 'lets the user know their account isn\'t linked yet' do post :saml, params: { SAMLResponse: mock_saml_response } expect(flash[:notice]).to eq 'Request to link SAML account must be authorized' end it 'redirects to profile account page' do post :saml, params: { SAMLResponse: mock_saml_response } expect(response).to redirect_to(profile_account_path) end it 'doesn\'t link a new identity to the user' do expect { post :saml, params: { SAMLResponse: mock_saml_response } }.not_to change { user.identities.count } end end context 'with a blocked user trying to log in when there are hooks set up' do let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml') } subject(:post_action) { post :saml, params: { SAMLResponse: mock_saml_response } } before do create(:system_hook) user.block! end it { expect { post_action }.not_to raise_error } end context 'with a non default SAML provider' do let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml') } controller(described_class) do alias_method :saml_okta, :handle_omniauth end before do allow(AuthHelper).to receive(:saml_providers).and_return([:saml, :saml_okta]) allow(@routes).to receive(:generate_extras).and_return(['/users/auth/saml_okta/callback', []]) end it 'authenticate with SAML module' do expect(@controller).to receive(:omniauth_flow).with(Gitlab::Auth::Saml).and_call_original post :saml_okta, params: { SAMLResponse: mock_saml_response } expect(request.env['warden']).to be_authenticated end it 'logs saml_response for debugging' do expect(Gitlab::AuthLogger).to receive(:info).with(payload_type: 'saml_response', saml_response: anything) post :saml_okta, params: { SAMLResponse: mock_saml_response } end end it_behaves_like "stores value for provider_2FA to session according to saml response" it 'logs saml_response for debugging' do expect(Gitlab::AuthLogger).to receive(:info).with(payload_type: 'saml_response', saml_response: anything) post :saml, params: { SAMLResponse: mock_saml_response } end end describe 'enable admin mode' do include_context 'custom session' let(:provider) { :auth0 } let(:extern_uid) { 'my-uid' } let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) } def reauthenticate_and_check_admin_mode(expected_admin_mode:) # Initially admin mode disabled expect(subject.current_user_mode.admin_mode?).to be(false) # Trigger OmniAuth admin mode flow and expect admin mode status post provider expect(request.env['warden']).to be_authenticated expect(subject.current_user_mode.admin_mode?).to be(expected_admin_mode) end context 'when user and admin mode is requested by the same user' do before do sign_in user mock_auth_hash(provider.to_s, extern_uid, user.email, additional_info: {}) stub_omniauth_provider(provider, context: request) end context 'with a regular user' do it 'cannot be enabled' do reauthenticate_and_check_admin_mode(expected_admin_mode: false) expect(response).to redirect_to(root_path) end end context 'with an admin user' do let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider, access_level: :admin) } context 'when requested first' do before do subject.current_user_mode.request_admin_mode! end it 'can be enabled' do reauthenticate_and_check_admin_mode(expected_admin_mode: true) expect(response).to redirect_to(admin_root_path) end end context 'when not requested first' do it 'cannot be enabled' do reauthenticate_and_check_admin_mode(expected_admin_mode: false) expect(response).to redirect_to(root_path) end end end end context 'when user and admin mode is requested by different users' do let(:reauth_extern_uid) { 'another_uid' } let(:reauth_user) { create(:omniauth_user, extern_uid: reauth_extern_uid, provider: provider) } before do sign_in user mock_auth_hash(provider.to_s, reauth_extern_uid, reauth_user.email, additional_info: {}) stub_omniauth_provider(provider, context: request) end context 'with a regular user' do it 'cannot be enabled' do reauthenticate_and_check_admin_mode(expected_admin_mode: false) expect(response).to redirect_to(profile_account_path) end end context 'with an admin user' do let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider, access_level: :admin) } let(:reauth_user) { create(:omniauth_user, extern_uid: reauth_extern_uid, provider: provider, access_level: :admin) } context 'when requested first' do before do subject.current_user_mode.request_admin_mode! end it 'cannot be enabled' do reauthenticate_and_check_admin_mode(expected_admin_mode: false) expect(response).to redirect_to(new_admin_session_path) end end context 'when not requested first' do it 'cannot be enabled' do reauthenticate_and_check_admin_mode(expected_admin_mode: false) expect(response).to redirect_to(profile_account_path) end end end end end end