mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-01 16:04:19 +00:00
1230 lines
42 KiB
Ruby
1230 lines
42 KiB
Ruby
# 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 <a href="/help/user/profile/_index.md#sign-in-services">connect it to your Atlassian account</a>.')
|
|
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 <a href=\"/help/user/profile/_index.md#sign-in-services\">connect it to your SAML account</a>.")
|
|
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
|