357 lines
12 KiB
Ruby
357 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting do
|
|
include EmailHelpers
|
|
|
|
let_it_be(:user) { create(:user) }
|
|
|
|
let(:require_email_verification_enabled) { user }
|
|
|
|
before do
|
|
stub_feature_flags(require_email_verification: require_email_verification_enabled)
|
|
end
|
|
|
|
shared_examples 'email verification required' do
|
|
before do
|
|
allow(Gitlab::AppLogger).to receive(:info)
|
|
end
|
|
|
|
it 'requires email verification before being able to access GitLab' do
|
|
perform_enqueued_jobs do
|
|
# When logging in
|
|
gitlab_sign_in(user)
|
|
expect_log_message(message: "Account Locked: username=#{user.username}")
|
|
expect_log_message('Instructions Sent')
|
|
|
|
# Expect the user to be locked and the unlock_token to be set
|
|
user.reload
|
|
expect(user.locked_at).not_to be_nil
|
|
expect(user.unlock_token).not_to be_nil
|
|
|
|
# Expect to see the verification form on the login page
|
|
expect(page).to have_current_path(new_user_session_path)
|
|
expect(page).to have_content('Help us protect your account')
|
|
|
|
# Expect an instructions email to be sent with a code
|
|
code = expect_instructions_email_and_extract_code
|
|
|
|
# Signing in again prompts for the code and doesn't send a new one
|
|
gitlab_sign_in(user)
|
|
expect(page).to have_current_path(new_user_session_path)
|
|
expect(page).to have_content('Help us protect your account')
|
|
|
|
# Verify the code
|
|
verify_code(code)
|
|
expect_log_message('Successful')
|
|
expect_log_message(message: "Successful Login: username=#{user.username} "\
|
|
"ip=127.0.0.1 method=standard admin=false")
|
|
|
|
# Expect the user to be unlocked
|
|
expect_user_to_be_unlocked
|
|
|
|
# Expect a confirmation page with a meta refresh tag for 3 seconds to the root
|
|
expect(page).to have_current_path(users_successful_verification_path)
|
|
expect(page).to have_content('Verification successful')
|
|
expect(page).to have_selector("meta[http-equiv='refresh'][content='3; url=#{root_path}']", visible: false)
|
|
end
|
|
end
|
|
|
|
describe 'resending a new code' do
|
|
it 'resends a new code' do
|
|
perform_enqueued_jobs do
|
|
# When logging in
|
|
gitlab_sign_in(user)
|
|
|
|
# Expect an instructions email to be sent with a code
|
|
code = expect_instructions_email_and_extract_code
|
|
|
|
# Request a new code
|
|
click_link 'Resend code'
|
|
expect_log_message('Instructions Sent', 2)
|
|
new_code = expect_instructions_email_and_extract_code
|
|
|
|
# Verify the old code is different from the new code
|
|
expect(code).not_to eq(new_code)
|
|
end
|
|
end
|
|
|
|
it 'rate limits resends' do
|
|
# When logging in
|
|
gitlab_sign_in(user)
|
|
|
|
# It shows a resend button
|
|
expect(page).to have_link 'Resend code'
|
|
|
|
# Resend more than the rate limited amount of times
|
|
10.times do
|
|
click_link 'Resend code'
|
|
end
|
|
|
|
# Expect the link to be gone
|
|
expect(page).not_to have_link 'Resend code'
|
|
|
|
# Wait for 1 hour
|
|
travel 1.hour
|
|
|
|
# Now it's visible again
|
|
gitlab_sign_in(user)
|
|
expect(page).to have_link 'Resend code'
|
|
end
|
|
end
|
|
|
|
describe 'verification errors' do
|
|
it 'rate limits verifications' do
|
|
perform_enqueued_jobs do
|
|
# When logging in
|
|
gitlab_sign_in(user)
|
|
|
|
# Expect an instructions email to be sent with a code
|
|
code = expect_instructions_email_and_extract_code
|
|
|
|
# Verify an invalid token more than the rate limited amount of times
|
|
11.times do
|
|
verify_code('123456')
|
|
end
|
|
|
|
# Expect an error message
|
|
expect_log_message('Failed Attempt', reason: 'rate_limited')
|
|
expect(page).to have_content("You've reached the maximum amount of tries. "\
|
|
'Wait 10 minutes or resend a new code and try again.')
|
|
|
|
# Wait for 10 minutes
|
|
travel 10.minutes
|
|
|
|
# Now it works again
|
|
verify_code(code)
|
|
expect_log_message('Successful')
|
|
end
|
|
end
|
|
|
|
it 'verifies invalid codes' do
|
|
# When logging in
|
|
gitlab_sign_in(user)
|
|
|
|
# Verify an invalid code
|
|
verify_code('123456')
|
|
|
|
# Expect an error message
|
|
expect_log_message('Failed Attempt', reason: 'invalid')
|
|
expect(page).to have_content('The code is incorrect. Enter it again, or resend a new code.')
|
|
end
|
|
|
|
it 'verifies expired codes' do
|
|
perform_enqueued_jobs do
|
|
# When logging in
|
|
gitlab_sign_in(user)
|
|
|
|
# Expect an instructions email to be sent with a code
|
|
code = expect_instructions_email_and_extract_code
|
|
|
|
# Wait for the code to expire before verifying
|
|
travel VerifiesWithEmail::TOKEN_VALID_FOR_MINUTES.minutes + 1.second
|
|
verify_code(code)
|
|
|
|
# Expect an error message
|
|
expect_log_message('Failed Attempt', reason: 'expired')
|
|
expect(page).to have_content('The code has expired. Resend a new code and try again.')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples 'no email verification required' do |**login_args|
|
|
it 'does not lock the user and redirects to the root page after logging in' do
|
|
gitlab_sign_in(user, **login_args)
|
|
|
|
expect_user_to_be_unlocked
|
|
|
|
expect(page).to have_current_path(root_path)
|
|
end
|
|
end
|
|
|
|
shared_examples 'no email verification required when 2fa enabled or ff disabled' do
|
|
context 'when 2FA is enabled' do
|
|
let_it_be(:user) { create(:user, :two_factor) }
|
|
|
|
it_behaves_like 'no email verification required', two_factor_auth: true
|
|
end
|
|
|
|
context 'when the feature flag is disabled' do
|
|
let(:require_email_verification_enabled) { false }
|
|
|
|
it_behaves_like 'no email verification required'
|
|
end
|
|
end
|
|
|
|
describe 'when failing to login the maximum allowed number of times' do
|
|
before do
|
|
# See comment in RequireEmailVerification::MAXIMUM_ATTEMPTS on why this is divided by 2
|
|
(RequireEmailVerification::MAXIMUM_ATTEMPTS / 2).times do
|
|
gitlab_sign_in(user, password: 'wrong_password')
|
|
end
|
|
end
|
|
|
|
it 'locks the user, but does not set the unlock token', :aggregate_failures do
|
|
user.reload
|
|
expect(user.locked_at).not_to be_nil
|
|
expect(user.unlock_token).to be_nil # The unlock token is only set after logging in with valid credentials
|
|
expect(user.failed_attempts).to eq(RequireEmailVerification::MAXIMUM_ATTEMPTS)
|
|
end
|
|
|
|
it_behaves_like 'email verification required'
|
|
it_behaves_like 'no email verification required when 2fa enabled or ff disabled'
|
|
|
|
describe 'when waiting for the auto unlock time' do
|
|
before do
|
|
travel User::UNLOCK_IN + 1.second
|
|
end
|
|
|
|
it_behaves_like 'no email verification required'
|
|
end
|
|
end
|
|
|
|
describe 'when no previous authentication event exists' do
|
|
it_behaves_like 'no email verification required'
|
|
end
|
|
|
|
describe 'when a previous authentication event exists for another ip address' do
|
|
before do
|
|
create(:authentication_event, :successful, user: user, ip_address: '1.2.3.4')
|
|
end
|
|
|
|
it_behaves_like 'email verification required'
|
|
it_behaves_like 'no email verification required when 2fa enabled or ff disabled'
|
|
end
|
|
|
|
describe 'when a previous authentication event exists for the same ip address' do
|
|
before do
|
|
create(:authentication_event, :successful, user: user)
|
|
end
|
|
|
|
it_behaves_like 'no email verification required'
|
|
end
|
|
|
|
describe 'rate limiting password guessing' do
|
|
before do
|
|
5.times { gitlab_sign_in(user, password: 'wrong_password') }
|
|
gitlab_sign_in(user)
|
|
end
|
|
|
|
it 'shows an error message on on the login page' do
|
|
expect(page).to have_current_path(new_user_session_path)
|
|
expect(page).to have_content('Maximum login attempts exceeded. Wait 10 minutes and try again.')
|
|
end
|
|
end
|
|
|
|
describe 'inconsistent states' do
|
|
context 'when the feature flag is toggled off after being prompted for a verification token' do
|
|
before do
|
|
create(:authentication_event, :successful, user: user, ip_address: '1.2.3.4')
|
|
end
|
|
|
|
it 'still accepts the token' do
|
|
perform_enqueued_jobs do
|
|
# The user is prompted for a verification code
|
|
gitlab_sign_in(user)
|
|
expect(page).to have_content('Help us protect your account')
|
|
code = expect_instructions_email_and_extract_code
|
|
|
|
# We toggle the feature flag off
|
|
stub_feature_flags(require_email_verification: false)
|
|
|
|
# Resending and veryfying the code work as expected
|
|
click_link 'Resend code'
|
|
new_code = expect_instructions_email_and_extract_code
|
|
|
|
verify_code(code)
|
|
expect(page).to have_content('The code is incorrect. Enter it again, or resend a new code.')
|
|
|
|
travel VerifiesWithEmail::TOKEN_VALID_FOR_MINUTES.minutes + 1.second
|
|
|
|
verify_code(new_code)
|
|
expect(page).to have_content('The code has expired. Resend a new code and try again.')
|
|
|
|
click_link 'Resend code'
|
|
another_code = expect_instructions_email_and_extract_code
|
|
|
|
verify_code(another_code)
|
|
expect_user_to_be_unlocked
|
|
expect(page).to have_current_path(users_successful_verification_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when the feature flag is toggled on after Devise sent unlock instructions' do
|
|
let(:require_email_verification_enabled) { false }
|
|
|
|
before do
|
|
perform_enqueued_jobs do
|
|
(User.maximum_attempts / 2).times do
|
|
gitlab_sign_in(user, password: 'wrong_password')
|
|
end
|
|
end
|
|
end
|
|
|
|
it 'the unlock link still works' do
|
|
# The user is locked and unlock instructions are sent
|
|
expect(page).to have_content('Invalid login or password.')
|
|
user.reload
|
|
expect(user.locked_at).not_to be_nil
|
|
expect(user.unlock_token).not_to be_nil
|
|
mail = find_email_for(user)
|
|
|
|
expect(mail.to).to match_array([user.email])
|
|
expect(mail.subject).to eq('Unlock instructions')
|
|
unlock_url = mail.body.parts.first.to_s[/http.*/]
|
|
|
|
# We toggle the feature flag on
|
|
stub_feature_flags(require_email_verification: true)
|
|
|
|
# Unlocking works as expected
|
|
visit unlock_url
|
|
expect_user_to_be_unlocked
|
|
expect(page).to have_current_path(new_user_session_path)
|
|
expect(page).to have_content('Your account has been unlocked successfully')
|
|
|
|
gitlab_sign_in(user)
|
|
expect(page).to have_current_path(root_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
def expect_user_to_be_unlocked
|
|
user.reload
|
|
|
|
aggregate_failures do
|
|
expect(user.locked_at).to be_nil
|
|
expect(user.unlock_token).to be_nil
|
|
expect(user.failed_attempts).to eq(0)
|
|
end
|
|
end
|
|
|
|
def expect_instructions_email_and_extract_code
|
|
mail = find_email_for(user)
|
|
expect(mail.to).to match_array([user.email])
|
|
expect(mail.subject).to eq('Verify your identity')
|
|
code = mail.body.parts.first.to_s[/\d{#{VerifiesWithEmail::TOKEN_LENGTH}}/]
|
|
reset_delivered_emails!
|
|
code
|
|
end
|
|
|
|
def verify_code(code)
|
|
fill_in 'Verification code', with: code
|
|
click_button 'Verify code'
|
|
end
|
|
|
|
def expect_log_message(event = nil, times = 1, reason: '', message: nil)
|
|
expect(Gitlab::AppLogger).to have_received(:info)
|
|
.exactly(times).times
|
|
.with(message || hash_including(message: 'Email Verification',
|
|
event: event,
|
|
username: user.username,
|
|
ip: '127.0.0.1',
|
|
reason: reason))
|
|
end
|
|
end
|