Merge branch 'rs-throttle-reset' into 'master'
Throttle "Forgot your password?" emails Addresses internal https://dev.gitlab.org/gitlab/gitlabhq/issues/2611 See merge request !1476
This commit is contained in:
commit
93522e59ec
7 changed files with 131 additions and 107 deletions
|
@ -1,20 +1,7 @@
|
|||
class PasswordsController < Devise::PasswordsController
|
||||
|
||||
def create
|
||||
email = resource_params[:email]
|
||||
resource_found = resource_class.find_by_email(email)
|
||||
if resource_found && resource_found.ldap_user?
|
||||
flash[:alert] = "Cannot reset password for LDAP user."
|
||||
respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) and return
|
||||
end
|
||||
|
||||
self.resource = resource_class.send_reset_password_instructions(resource_params)
|
||||
if successfully_sent?(resource)
|
||||
respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name))
|
||||
else
|
||||
respond_with(resource)
|
||||
end
|
||||
end
|
||||
before_action :resource_from_email, only: [:create]
|
||||
before_action :prevent_ldap_reset, only: [:create]
|
||||
before_action :throttle_reset, only: [:create]
|
||||
|
||||
def edit
|
||||
super
|
||||
|
@ -35,4 +22,25 @@ class PasswordsController < Devise::PasswordsController
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def resource_from_email
|
||||
email = resource_params[:email]
|
||||
self.resource = resource_class.find_by_email(email)
|
||||
end
|
||||
|
||||
def prevent_ldap_reset
|
||||
return unless resource && resource.ldap_user?
|
||||
|
||||
redirect_to after_sending_reset_password_instructions_path_for(resource_name),
|
||||
alert: "Cannot reset password for LDAP user."
|
||||
end
|
||||
|
||||
def throttle_reset
|
||||
return unless resource && resource.recently_sent_password_reset?
|
||||
|
||||
redirect_to new_password_path(resource_name),
|
||||
alert: I18n.t('devise.passwords.recently_reset')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -328,6 +328,10 @@ class User < ActiveRecord::Base
|
|||
@reset_token
|
||||
end
|
||||
|
||||
def recently_sent_password_reset?
|
||||
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
|
||||
end
|
||||
|
||||
def disable_two_factor!
|
||||
update_attributes(
|
||||
two_factor_enabled: false,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
.devise-errors
|
||||
= devise_error_messages!
|
||||
.clearfix.append-bottom-20
|
||||
= f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email]
|
||||
= f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email], autofocus: true
|
||||
.clearfix
|
||||
= f.submit "Reset password", class: "btn-primary btn"
|
||||
|
||||
|
|
|
@ -1,61 +1,63 @@
|
|||
# Additional translations at http://github.com/plataformatec/devise/wiki/I18n
|
||||
# Additional translations at https://github.com/plataformatec/devise/wiki/I18n
|
||||
|
||||
en:
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: "Your email address has been successfully confirmed."
|
||||
send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
|
||||
send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
|
||||
failure:
|
||||
already_authenticated: "You are already signed in."
|
||||
inactive: "Your account is not activated yet."
|
||||
invalid: "Invalid %{authentication_keys} or password."
|
||||
locked: "Your account is locked."
|
||||
last_attempt: "You have one more attempt before your account is locked."
|
||||
not_found_in_database: "Invalid %{authentication_keys} or password."
|
||||
timeout: "Your session expired. Please sign in again to continue."
|
||||
unauthenticated: "You need to sign in or sign up before continuing."
|
||||
unconfirmed: "You have to confirm your email address before continuing."
|
||||
mailer:
|
||||
confirmation_instructions:
|
||||
subject: "Confirmation instructions"
|
||||
reset_password_instructions:
|
||||
subject: "Reset password instructions"
|
||||
unlock_instructions:
|
||||
subject: "Unlock instructions"
|
||||
password_change:
|
||||
subject: "Password Changed"
|
||||
omniauth_callbacks:
|
||||
failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
|
||||
success: "Successfully authenticated from %{kind} account."
|
||||
passwords:
|
||||
no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
|
||||
recently_reset: "Instructions about how to reset your password have already been sent recently. Please wait a few minutes to try again."
|
||||
send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
|
||||
send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
|
||||
updated: "Your password has been changed successfully. You are now signed in."
|
||||
updated_not_active: "Your password has been changed successfully."
|
||||
registrations:
|
||||
destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
|
||||
signed_up: "Welcome! You have signed up successfully."
|
||||
signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
|
||||
signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
|
||||
signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
|
||||
update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address."
|
||||
updated: "Your account has been updated successfully."
|
||||
sessions:
|
||||
signed_in: "Signed in successfully."
|
||||
signed_out: "Signed out successfully."
|
||||
already_signed_out: "Signed out successfully."
|
||||
unlocks:
|
||||
send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
|
||||
send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
|
||||
unlocked: "Your account has been unlocked successfully. Please sign in to continue."
|
||||
errors:
|
||||
messages:
|
||||
already_confirmed: "was already confirmed, please try signing in"
|
||||
confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
|
||||
expired: "has expired, please request a new one"
|
||||
not_found: "not found"
|
||||
already_confirmed: "was already confirmed, please try signing in"
|
||||
not_locked: "was not locked"
|
||||
not_saved:
|
||||
one: "1 error prohibited this %{resource} from being saved:"
|
||||
other: "%{count} errors prohibited this %{resource} from being saved:"
|
||||
|
||||
devise:
|
||||
failure:
|
||||
already_authenticated: 'You are already signed in.'
|
||||
unauthenticated: 'You need to sign in before continuing.'
|
||||
unconfirmed: 'You have to confirm your account before continuing.'
|
||||
locked: 'Your account is locked.'
|
||||
not_found_in_database: 'Invalid email or password.'
|
||||
invalid: 'Invalid email or password.'
|
||||
invalid_token: 'Invalid authentication token.'
|
||||
timeout: 'Your session expired, please sign in again to continue.'
|
||||
inactive: 'Your account was not activated yet.'
|
||||
sessions:
|
||||
signed_in: ''
|
||||
signed_out: ''
|
||||
users_sessions:
|
||||
user:
|
||||
signed_in: 'Signed in successfully.'
|
||||
passwords:
|
||||
send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.'
|
||||
updated: 'Your password was changed successfully. You are now signed in.'
|
||||
updated_not_active: 'Your password was changed successfully.'
|
||||
send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
|
||||
no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
|
||||
confirmations:
|
||||
send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.'
|
||||
send_paranoid_instructions: 'If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes.'
|
||||
confirmed: 'Your account was successfully confirmed. You are now signed in.'
|
||||
registrations:
|
||||
signed_up: 'Welcome! You have signed up successfully.'
|
||||
updated: 'You updated your account successfully.'
|
||||
destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.'
|
||||
signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.'
|
||||
signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.'
|
||||
signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.'
|
||||
unlocks:
|
||||
send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.'
|
||||
unlocked: 'Your account was successfully unlocked. You are now signed in.'
|
||||
send_paranoid_instructions: 'If your account exists, you will receive an email with instructions about how to unlock it in a few minutes.'
|
||||
omniauth_callbacks:
|
||||
success: 'Successfully authorized from %{kind} account.'
|
||||
failure: 'Could not authorize you from %{kind} because "%{reason}".'
|
||||
mailer:
|
||||
confirmation_instructions:
|
||||
subject: 'Confirmation instructions'
|
||||
reset_password_instructions:
|
||||
subject: 'Reset password instructions'
|
||||
unlock_instructions:
|
||||
subject: 'Unlock Instructions'
|
||||
|
|
|
@ -95,7 +95,7 @@ feature 'Login', feature: true do
|
|||
user = create(:user, password: 'not-the-default')
|
||||
|
||||
login_with(user)
|
||||
expect(page).to have_content('Invalid email or password.')
|
||||
expect(page).to have_content('Invalid login or password.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,53 +1,43 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'Password reset', feature: true do
|
||||
describe 'with two-factor authentication' do
|
||||
let(:user) { create(:user, :two_factor) }
|
||||
|
||||
it 'requires login after password reset' do
|
||||
describe 'throttling' do
|
||||
it 'sends reset instructions when not previously sent' do
|
||||
visit root_path
|
||||
forgot_password(create(:user))
|
||||
|
||||
forgot_password
|
||||
reset_password
|
||||
|
||||
expect(page).to have_content("Your password was changed successfully.")
|
||||
expect(page).not_to have_content("You are now signed in.")
|
||||
expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))
|
||||
expect(current_path).to eq new_user_session_path
|
||||
end
|
||||
|
||||
it 'sends reset instructions when previously sent more than a minute ago' do
|
||||
user = create(:user)
|
||||
user.send_reset_password_instructions
|
||||
user.update_attribute(:reset_password_sent_at, 5.minutes.ago)
|
||||
|
||||
visit root_path
|
||||
forgot_password(user)
|
||||
|
||||
expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))
|
||||
expect(current_path).to eq new_user_session_path
|
||||
end
|
||||
|
||||
it "throttles multiple resets in a short timespan" do
|
||||
user = create(:user)
|
||||
user.send_reset_password_instructions
|
||||
|
||||
visit root_path
|
||||
forgot_password(user)
|
||||
|
||||
expect(page).to have_content(I18n.t('devise.passwords.recently_reset'))
|
||||
expect(current_path).to eq new_user_password_path
|
||||
end
|
||||
end
|
||||
|
||||
describe 'without two-factor authentication' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'requires login after password reset' do
|
||||
visit root_path
|
||||
|
||||
forgot_password
|
||||
reset_password
|
||||
|
||||
expect(page).to have_content("Your password was changed successfully.")
|
||||
expect(current_path).to eq new_user_session_path
|
||||
end
|
||||
end
|
||||
|
||||
def forgot_password
|
||||
def forgot_password(user)
|
||||
click_on 'Forgot your password?'
|
||||
fill_in 'Email', with: user.email
|
||||
click_button 'Reset password'
|
||||
user.reload
|
||||
end
|
||||
|
||||
def get_reset_token
|
||||
mail = ActionMailer::Base.deliveries.last
|
||||
body = mail.body.encoded
|
||||
body.scan(/reset_password_token=(.+)\"/).flatten.first
|
||||
end
|
||||
|
||||
def reset_password(password = 'password')
|
||||
visit edit_user_password_path(reset_password_token: get_reset_token)
|
||||
|
||||
fill_in 'New password', with: password
|
||||
fill_in 'Confirm new password', with: password
|
||||
click_button 'Change your password'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -228,6 +228,26 @@ describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#recently_sent_password_reset?' do
|
||||
it 'is false when reset_password_sent_at is nil' do
|
||||
user = build_stubbed(:user, reset_password_sent_at: nil)
|
||||
|
||||
expect(user.recently_sent_password_reset?).to eq false
|
||||
end
|
||||
|
||||
it 'is false when sent more than one minute ago' do
|
||||
user = build_stubbed(:user, reset_password_sent_at: 5.minutes.ago)
|
||||
|
||||
expect(user.recently_sent_password_reset?).to eq false
|
||||
end
|
||||
|
||||
it 'is true when sent less than one minute ago' do
|
||||
user = build_stubbed(:user, reset_password_sent_at: Time.now)
|
||||
|
||||
expect(user.recently_sent_password_reset?).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#disable_two_factor!' do
|
||||
it 'clears all 2FA-related fields' do
|
||||
user = create(:user, :two_factor)
|
||||
|
|
Loading…
Reference in a new issue