gitlab-org--gitlab-foss/spec/controllers/sessions_controller_spec.rb

559 lines
18 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
describe SessionsController do
include DeviseHelpers
include LdapHelpers
describe '#new' do
before do
set_devise_mapping(context: @request)
end
context 'when auto sign-in is enabled' do
before do
stub_omniauth_setting(auto_sign_in_with_provider: :saml)
allow(controller).to receive(:omniauth_authorize_path).with(:user, :saml)
.and_return('/saml')
end
context 'and no auto_sign_in param is passed' do
it 'redirects to :omniauth_authorize_path' do
get(:new)
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to('/saml')
end
end
context 'and auto_sign_in=false param is passed' do
it 'responds with 200' do
get(:new, params: { auto_sign_in: 'false' })
expect(response).to have_gitlab_http_status(:ok)
end
end
end
context 'with LDAP enabled' do
before do
stub_ldap_setting(enabled: true)
end
it 'assigns ldap_servers' do
get(:new)
expect(assigns[:ldap_servers].first.to_h).to include('label' => 'ldap', 'provider_name' => 'ldapmain')
end
context 'with sign_in disabled' do
before do
stub_ldap_setting(prevent_ldap_sign_in: true)
end
it 'assigns no ldap_servers' do
get(:new)
expect(assigns[:ldap_servers]).to eq []
end
end
end
describe 'tracking data' do
context 'when the user is part of the experimental group' do
before do
stub_experiment_for_user(signup_flow: true)
end
it 'doesn\'t pass tracking parameters to the frontend' do
get(:new)
expect(Gon.tracking_data).to be_nil
end
end
context 'with the experimental signup flow enabled and the user is part of the control group' do
before do
stub_experiment(signup_flow: true)
stub_experiment_for_user(signup_flow: false)
allow_any_instance_of(described_class).to receive(:experimentation_subject_id).and_return('uuid')
end
it 'passes the right tracking parameters to the frontend' do
get(:new)
expect(Gon.tracking_data).to eq(
{
category: 'Growth::Acquisition::Experiment::SignUpFlow',
action: 'start',
label: 'uuid',
property: 'control_group'
}
)
end
end
end
end
describe '#create' do
before do
set_devise_mapping(context: @request)
end
context 'when using standard authentications' do
context 'invalid password' do
it 'does not authenticate user' do
post(:create, params: { user: { login: 'invalid', password: 'invalid' } })
expect(response)
.to set_flash.now[:alert].to /Invalid Login or password/
end
end
context 'when using valid password', :clean_gitlab_redis_shared_state do
let(:user) { create(:user) }
let(:user_params) { { login: user.username, password: user.password } }
it 'authenticates user correctly' do
post(:create, params: { user: user_params })
expect(subject.current_user).to eq user
end
context 'a deactivated user' do
before do
user.deactivate!
post(:create, params: { user: user_params })
end
it 'is allowed to login' do
expect(subject.current_user).to eq user
end
it 'activates the user' do
expect(subject.current_user.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 'with password authentication disabled' do
before do
stub_application_setting(password_authentication_enabled_for_web: false)
end
it 'does not sign in the user' do
post(:create, params: { user: user_params })
expect(@request.env['warden']).not_to be_authenticated
expect(subject.current_user).to be_nil
end
it 'returns status 403' do
post(:create, params: { user: user_params })
expect(response).to have_gitlab_http_status(:forbidden)
end
end
it 'creates an audit log record' do
expect { post(:create, params: { user: user_params }) }.to change { SecurityEvent.count }.by(1)
expect(SecurityEvent.last.details[:with]).to eq('standard')
end
include_examples 'user login request with unique ip limit', 302 do
def request
post(:create, params: { user: user_params })
expect(subject.current_user).to eq user
subject.sign_out user
end
end
it 'updates the user activity' do
expect do
post(:create, params: { user: user_params })
end.to change { user.reload.last_activity_on }.to(Date.today)
end
end
context 'with reCAPTCHA' do
def unsuccesful_login(user_params, sesion_params: {})
# Without this, `verify_recaptcha` arbitrarily returns true in test env
Recaptcha.configuration.skip_verify_env.delete('test')
counter = double(:counter)
expect(counter).to receive(:increment)
expect(Gitlab::Metrics).to receive(:counter)
.with(:failed_login_captcha_total, anything)
.and_return(counter)
post(:create, params: { user: user_params }, session: sesion_params)
end
def succesful_login(user_params, sesion_params: {})
# Avoid test ordering issue and ensure `verify_recaptcha` returns true
Recaptcha.configuration.skip_verify_env << 'test'
counter = double(:counter)
expect(counter).to receive(:increment)
expect(Gitlab::Metrics).to receive(:counter)
.with(:successful_login_captcha_total, anything)
.and_return(counter)
expect(Gitlab::Metrics).to receive(:counter).and_call_original
post(:create, params: { user: user_params }, session: sesion_params)
end
context 'when reCAPTCHA is enabled' do
let(:user) { create(:user) }
let(:user_params) { { login: user.username, password: user.password } }
before do
stub_application_setting(recaptcha_enabled: true)
request.headers[described_class::CAPTCHA_HEADER] = 1
end
it 'displays an error when the reCAPTCHA is not solved' do
# Without this, `verify_recaptcha` arbitrarily returns true in test env
unsuccesful_login(user_params)
expect(response).to render_template(:new)
expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
expect(subject.current_user).to be_nil
end
it 'successfully logs in a user when reCAPTCHA is solved' do
succesful_login(user_params)
expect(subject.current_user).to eq user
end
end
context 'when reCAPTCHA login protection is enabled' do
let(:user) { create(:user) }
let(:user_params) { { login: user.username, password: user.password } }
before do
stub_application_setting(login_recaptcha_protection_enabled: true)
end
context 'when user tried to login 5 times' do
it 'displays an error when the reCAPTCHA is not solved' do
unsuccesful_login(user_params, sesion_params: { failed_login_attempts: 6 })
expect(response).to render_template(:new)
expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
expect(subject.current_user).to be_nil
end
it 'successfully logs in a user when reCAPTCHA is solved' do
succesful_login(user_params, sesion_params: { failed_login_attempts: 6 })
expect(subject.current_user).to eq user
end
end
context 'when there are more than 5 anonymous session with the same IP' do
before do
allow(Gitlab::AnonymousSession).to receive_message_chain(:new, :stored_sessions).and_return(6)
end
it 'displays an error when the reCAPTCHA is not solved' do
unsuccesful_login(user_params)
expect(response).to render_template(:new)
expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
expect(subject.current_user).to be_nil
end
it 'successfully logs in a user when reCAPTCHA is solved' do
expect(Gitlab::AnonymousSession).to receive_message_chain(:new, :cleanup_session_per_ip_entries)
succesful_login(user_params)
expect(subject.current_user).to eq user
end
end
end
end
end
context 'when using two-factor authentication via OTP' do
let(:user) { create(:user, :two_factor) }
def authenticate_2fa(user_params)
post(:create, params: { user: user_params }, session: { otp_user_id: user.id })
end
context 'remember_me field' do
it 'sets a remember_user_token cookie when enabled' do
allow(controller).to receive(:find_user).and_return(user)
expect(controller)
.to receive(:remember_me).with(user).and_call_original
authenticate_2fa(remember_me: '1', otp_attempt: user.current_otp)
expect(response.cookies['remember_user_token']).to be_present
end
it 'does nothing when disabled' do
allow(controller).to receive(:find_user).and_return(user)
expect(controller).not_to receive(:remember_me)
authenticate_2fa(remember_me: '0', otp_attempt: user.current_otp)
expect(response.cookies['remember_user_token']).to be_nil
end
end
context 'with password authentication disabled' do
before do
stub_application_setting(password_authentication_enabled_for_web: false)
end
it 'allows 2FA stage of non-password login' do
authenticate_2fa(otp_attempt: user.current_otp)
expect(@request.env['warden']).to be_authenticated
expect(subject.current_user).to eq user
end
end
##
# See #14900 issue
#
context 'when authenticating with login and OTP of another user' do
context 'when another user has 2FA enabled' do
let(:another_user) { create(:user, :two_factor) }
context 'when OTP is valid for another user' do
it 'does not authenticate' do
authenticate_2fa(login: another_user.username,
otp_attempt: another_user.current_otp)
expect(subject.current_user).not_to eq another_user
end
end
context 'when OTP is invalid for another user' do
it 'does not authenticate' do
authenticate_2fa(login: another_user.username,
otp_attempt: 'invalid')
expect(subject.current_user).not_to eq another_user
end
end
context 'when authenticating with OTP' do
context 'when OTP is valid' do
it 'authenticates correctly' do
authenticate_2fa(otp_attempt: user.current_otp)
expect(subject.current_user).to eq user
end
end
context 'when OTP is invalid' do
before do
authenticate_2fa(otp_attempt: 'invalid')
end
it 'does not authenticate' do
expect(subject.current_user).not_to eq user
end
it 'warns about invalid OTP code' do
expect(response).to set_flash.now[:alert]
.to /Invalid two-factor code/
end
end
end
context 'when the user is on their last attempt' do
before do
user.update(failed_attempts: User.maximum_attempts.pred)
end
context 'when OTP is valid' do
it 'authenticates correctly' do
authenticate_2fa(otp_attempt: user.current_otp)
expect(subject.current_user).to eq user
end
end
context 'when OTP is invalid' do
before do
authenticate_2fa(otp_attempt: 'invalid')
end
it 'does not authenticate' do
expect(subject.current_user).not_to eq user
end
it 'warns about invalid login' do
expect(response).to set_flash.now[:alert].to /Your account is locked./
end
it 'locks the user' do
expect(user.reload).to be_access_locked
end
it 'keeps the user locked on future login attempts' do
post(:create, params: { user: { login: user.username, password: user.password } })
expect(response).to set_flash.now[:alert].to /Your account is locked./
end
end
end
context 'when another user does not have 2FA enabled' do
let(:another_user) { create(:user) }
it 'does not leak that 2FA is disabled for another user' do
authenticate_2fa(login: another_user.username,
otp_attempt: 'invalid')
expect(response).to set_flash.now[:alert]
.to /Invalid two-factor code/
end
end
end
end
it "creates an audit log record" do
expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { SecurityEvent.count }.by(1)
expect(SecurityEvent.last.details[:with]).to eq("two-factor")
end
end
context 'when using two-factor authentication via U2F device' do
let(:user) { create(:user, :two_factor) }
def authenticate_2fa_u2f(user_params)
post(:create, params: { user: user_params }, session: { otp_user_id: user.id })
end
context 'remember_me field' do
it 'sets a remember_user_token cookie when enabled' do
allow(U2fRegistration).to receive(:authenticate).and_return(true)
allow(controller).to receive(:find_user).and_return(user)
expect(controller)
.to receive(:remember_me).with(user).and_call_original
authenticate_2fa_u2f(remember_me: '1', login: user.username, device_response: "{}")
expect(response.cookies['remember_user_token']).to be_present
end
it 'does nothing when disabled' do
allow(U2fRegistration).to receive(:authenticate).and_return(true)
allow(controller).to receive(:find_user).and_return(user)
expect(controller).not_to receive(:remember_me)
authenticate_2fa_u2f(remember_me: '0', login: user.username, device_response: "{}")
expect(response.cookies['remember_user_token']).to be_nil
end
end
it "creates an audit log record" do
allow(U2fRegistration).to receive(:authenticate).and_return(true)
expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1)
expect(SecurityEvent.last.details[:with]).to eq("two-factor-via-u2f-device")
end
end
end
describe "#new" do
before do
set_devise_mapping(context: @request)
end
it "redirects correctly for referer on same host with params" do
host = "test.host"
search_path = "/search?search=seed_project"
request.headers[:HTTP_REFERER] = "http://#{host}#{search_path}"
get(:new, params: { redirect_to_referer: :yes })
expect(controller.stored_location_for(:redirect)).to eq(search_path)
end
end
context 'when login fails' do
before do
set_devise_mapping(context: @request)
@request.env["warden.options"] = { action: 'unauthenticated' }
end
it 'does increment failed login counts for session' do
get(:new, params: { user: { login: 'failed' } })
expect(session[:failed_login_attempts]).to eq(1)
end
end
describe '#set_current_context' do
let_it_be(:user) { create(:user) }
before do
set_devise_mapping(context: @request)
end
context 'when signed in' do
before do
sign_in(user)
end
it 'sets the username and caller_id in the context' do
expect(controller).to receive(:destroy).and_wrap_original do |m, *args|
expect(Labkit::Context.current.to_h)
.to include('meta.user' => user.username,
'meta.caller_id' => 'SessionsController#destroy')
m.call(*args)
end
delete :destroy
end
end
context 'when not signed in' do
it 'sets the caller_id in the context' do
expect(controller).to receive(:new).and_wrap_original do |m, *args|
expect(Labkit::Context.current.to_h)
.to include('meta.caller_id' => 'SessionsController#new')
expect(Labkit::Context.current.to_h)
.not_to include('meta.user')
m.call(*args)
end
get :new
end
end
context 'when the user becomes locked' do
before do
user.update!(failed_attempts: User.maximum_attempts.pred)
end
it 'sets the caller_id in the context' do
allow_any_instance_of(User).to receive(:lock_access!).and_wrap_original do |m, *args|
expect(Labkit::Context.current.to_h)
.to include('meta.caller_id' => 'SessionsController#create')
expect(Labkit::Context.current.to_h)
.not_to include('meta.user')
m.call(*args)
end
post(:create,
params: { user: { login: user.username, password: user.password.succ } })
end
end
end
end