Skip to content
Merged
13 changes: 12 additions & 1 deletion app/controllers/users/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down
2 changes: 1 addition & 1 deletion config/locales/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down
2 changes: 1 addition & 1 deletion config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down
2 changes: 1 addition & 1 deletion config/locales/zh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}后再试。
Expand Down
36 changes: 29 additions & 7 deletions spec/controllers/users/sessions_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand Down
25 changes: 23 additions & 2 deletions spec/features/visitors/bad_password_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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'])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice that you found a way to get at the session from the feature specs 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea it was a little annoying because calling that changes page. So I had to call a visit afterwards. railsware/rack_session_access#10 Very weird

# 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)
Expand Down