gitlab-org--gitlab-foss/spec/controllers/sessions_controller_spec.rb
Małgorzata Ksionek dfcf4cf5f1 Add captcha if there are multiple failed login attempts
Add method to store session ids by ip

Add new specs for storing session ids

Add cleaning up records after login

Add retrieving anonymous sessions

Add login recaptcha setting

Add new setting to sessions controller

Add conditions for showing captcha

Add sessions controller specs

Add admin settings specs for login protection

Add new settings to api

Add stub to devise spec

Add new translation key

Add cr remarks

Rename class call

Add cr remarks

Change if-clause for consistency

Add cr remarks

Add code review remarks

Refactor AnonymousSession class

Add changelog entry

Move AnonymousSession class to lib

Move store unauthenticated sessions to sessions controller

Move link to recaptcha info

Regenerate text file

Improve copy on the spam page

Change action filter for storing anonymous sessions

Fix rubocop offences

Add code review remarks
2019-07-31 11:47:55 +02:00

423 lines
14 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
describe SessionsController do
include DeviseHelpers
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(302)
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(200)
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 '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.status).to eq 403
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 /Invalid Login or password/
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 /Invalid Login or password/
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
end