diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-01 18:08:30 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-01 18:08:30 +0300 |
commit | a0fdcfcdd514c2af98f18cadfa75f8a6a85b4d2c (patch) | |
tree | ecba106fd4d1426cc2109a6ba3da091be2de1f87 /spec/features/users | |
parent | 2828f81d2a41f46b89e13dc057b982f27aeee547 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/features/users')
-rw-r--r-- | spec/features/users/email_verification_on_login_spec.rb | 357 |
1 files changed, 357 insertions, 0 deletions
diff --git a/spec/features/users/email_verification_on_login_spec.rb b/spec/features/users/email_verification_on_login_spec.rb new file mode 100644 index 00000000000..54eb11ca857 --- /dev/null +++ b/spec/features/users/email_verification_on_login_spec.rb @@ -0,0 +1,357 @@ +# 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) { true } + + 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 |