gitlab-org--gitlab-foss/spec/features/users/email_verification_on_login_spec.rb

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