diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 135ef08f9cd..159e784baf2 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -74,10 +74,21 @@ def increment_session_bad_password_count def process_locked_out_session warden.logout(:user) warden.lock! - flash[:error] = t('errors.sign_in.bad_password_limit') + + flash[:error] = t( + 'errors.sign_in.bad_password_limit', + time_left: locked_out_time_remaining, + ) redirect_to root_url end + def locked_out_time_remaining + locked_at = session[:max_bad_passwords_at] + window = IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds + time_lockout_expires = Time.zone.at(locked_at) + window + distance_of_time_in_words(Time.zone.now, time_lockout_expires, true) + end + def valid_captcha_result? return @valid_captcha_result if defined?(@valid_captcha_result) @valid_captcha_result = SignInRecaptchaForm.new(**recaptcha_form_args).submit( diff --git a/config/locales/en.yml b/config/locales/en.yml index 3c931ad39a4..01ae8d1bfac 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -744,7 +744,7 @@ errors.messages.wrong_length.one: is the wrong length (should be 1 character) errors.messages.wrong_length.other: is the wrong length (should be %{count} characters) errors.piv_cac_setup.unique_name: That name is already taken. Please choose a different name. errors.registration.terms: Before you can continue, you must give us permission. Please check the box below and then click continue. -errors.sign_in.bad_password_limit: You have exceeded the maximum sign in attempts. +errors.sign_in.bad_password_limit: You have exceeded the maximum sign in attempts. You must wait %{time_left} before trying again. errors.two_factor_auth_setup.must_select_additional_option: Select an additional authentication method. errors.two_factor_auth_setup.must_select_option: Select an authentication method. errors.verify_personal_key.rate_limited: You tried too many times, please try again in %{timeout}. diff --git a/config/locales/es.yml b/config/locales/es.yml index e32ab74ce44..a45c68330f2 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -755,7 +755,7 @@ errors.messages.wrong_length.one: tiene la longitud incorrecta (debe ser de 1 c errors.messages.wrong_length.other: tiene la longitud incorrecta (debe ser de %{count} caracteres) errors.piv_cac_setup.unique_name: Ese nombre ya fue seleccionado. Elija un nombre diferente. errors.registration.terms: Antes de continuar, debe darnos permiso. Marque la casilla a continuación y luego haga clic en continuar. -errors.sign_in.bad_password_limit: Superó el número máximo de intentos de inicio de sesión. +errors.sign_in.bad_password_limit: Superó el número máximo de intentos de inicio de sesión. Debe esperar %{time_left} antes de volver a intentarlo. errors.two_factor_auth_setup.must_select_additional_option: Seleccione un método de autenticación adicional. errors.two_factor_auth_setup.must_select_option: Seleccione un método de autenticación. errors.verify_personal_key.rate_limited: Lo intentó demasiadas veces; vuelva a intentarlo en %{timeout}. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a70d855ed5f..fac6100576f 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -744,7 +744,7 @@ errors.messages.wrong_length.one: n’est pas de la bonne longueur (devrait êtr errors.messages.wrong_length.other: n’est pas de la bonne longueur (devrait être de %{count} caractères) errors.piv_cac_setup.unique_name: Ce nom est déjà pris. Veuillez choisir un autre nom. errors.registration.terms: Avant de pouvoir continuer, vous devez nous donner la permission. Veuillez cocher la case ci-dessous, puis cliquez sur Suite. -errors.sign_in.bad_password_limit: Vous avez dépassé le nombre maximal de tentatives de connexion. +errors.sign_in.bad_password_limit: Vous avez dépassé le nombre maximal de tentatives de connexion. Vous devez attendre %{time_left} avant de réessayer. errors.two_factor_auth_setup.must_select_additional_option: Sélectionnez une méthode d’authentification supplémentaire. errors.two_factor_auth_setup.must_select_option: Sélectionnez une méthode d’authentification. errors.verify_personal_key.rate_limited: Vous avez essayé trop de fois, veuillez réessayer dans %{timeout}. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index a8b5be89b5e..f77d63a41f5 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -755,7 +755,7 @@ errors.messages.wrong_length.one: 长度不对(应当是 1 个字符) errors.messages.wrong_length.other: 长度不对(应当是 %{count} 个字符) errors.piv_cac_setup.unique_name: 这个名字已被使用。请选择一个不同的名字。 errors.registration.terms: 在你能继续之前,你必须授予我们你的同意。请在下面的框打勾然后点击继续。 -errors.sign_in.bad_password_limit: 你已超出登录尝试允许最多次数。 +errors.sign_in.bad_password_limit: 你已超出登录尝试允许最多次数。你必须等待 %{time_left} 才能重试。 errors.two_factor_auth_setup.must_select_additional_option: 请选择一个额外的身份证实方法。 errors.two_factor_auth_setup.must_select_option: 选择一个身份证实方法。 errors.verify_personal_key.rate_limited: 你尝试了太多次。请在 %{timeout}后再试。 diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index e50c63953fe..2495cf64fcc 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -116,6 +116,35 @@ end end + context 'locked out session' do + let(:locked_at) { Time.zone.now } + let(:user) { create(:user, :fully_registered) } + let(:bad_password_window) { IdentityConfig.store.max_bad_passwords_window_in_seconds } + + before do + session[:bad_password_count] = IdentityConfig.store.max_bad_passwords + 1 + session[:max_bad_passwords_at] = locked_at.to_i + end + + it 'renders an error letting user know they are locked out for a period of time' do + post :create, params: { user: { email: user.email.upcase, password: user.password } } + current_time = Time.zone.now + time_in_hours = distance_of_time_in_words( + current_time, + (locked_at + bad_password_window.seconds), + true, + ) + + expect(response).to redirect_to root_url + expect(flash[:error]).to eq( + t( + 'errors.sign_in.bad_password_limit', + time_left: time_in_hours, + ), + ) + end + end + it 'tracks the unsuccessful authentication for existing user' do user = create(:user, :fully_registered) @@ -157,13 +186,6 @@ post :create, params: { user: { email: 'foo@example.com', password: 'password' } } end - it 'tracks unsuccessful authentication for too many auth failures' do - allow(subject).to receive(:session_bad_password_count_max_exceeded?).and_return(true) - mock_email_parameter = { email: 'bob@example.com' } - - post :create, params: { user: { **mock_email_parameter, password: 'eatCake!' } } - end - it 'tracks unsuccessful authentication for locked out user' do user = create( :user, diff --git a/spec/features/visitors/bad_password_spec.rb b/spec/features/visitors/bad_password_spec.rb index 53fe0364040..1493b96a04c 100644 --- a/spec/features/visitors/bad_password_spec.rb +++ b/spec/features/visitors/bad_password_spec.rb @@ -1,8 +1,10 @@ require 'rails_helper' RSpec.feature 'Visitor signs in with bad passwords and gets locked out' do + include ActionView::Helpers::DateHelper let(:user) { create(:user, :fully_registered) } let(:bad_password) { 'badpassword' } + let(:window) { IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds } scenario 'visitor tries too many bad passwords gets locked out then waits window seconds' do visit new_user_session_path @@ -15,14 +17,33 @@ expect(page).to have_content(error_message) expect(page).to have_current_path(new_user_session_path) end + locked_at = Time.zone.at(page.get_rack_session['max_bad_passwords_at']) + # Need to do this because getting rack session changes the url. + visit new_user_session_path 2.times do fill_in_credentials_and_submit(user.email, bad_password) + expect(page).to have_current_path(new_user_session_path) - expect(page).to have_content(t('errors.sign_in.bad_password_limit')) + new_time = Time.zone.at(locked_at) + window + time_left = distance_of_time_in_words(Time.zone.now, new_time, true) + expect(page).to have_content( + t( + 'errors.sign_in.bad_password_limit', + time_left: time_left, + ), + ) end fill_in_credentials_and_submit(user.email, user.password) expect(page).to have_current_path(new_user_session_path) - expect(page).to have_content(t('errors.sign_in.bad_password_limit')) + new_time = Time.zone.at(locked_at) + window + time_left = distance_of_time_in_words(Time.zone.now, new_time, true) + expect(page).to have_content( + t( + 'errors.sign_in.bad_password_limit', + time_left: time_left, + ), + ) + travel_to(IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds.from_now) do fill_in_credentials_and_submit(user.email, bad_password) expect(page).to have_content(error_message)