Commit 93522e59 authored by Douwe Maan's avatar Douwe Maan

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
parents c867c225 d40dd5cf
class PasswordsController < Devise::PasswordsController class PasswordsController < Devise::PasswordsController
before_action :resource_from_email, only: [:create]
def create before_action :prevent_ldap_reset, only: [:create]
email = resource_params[:email] before_action :throttle_reset, only: [:create]
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
def edit def edit
super super
...@@ -35,4 +22,25 @@ class PasswordsController < Devise::PasswordsController ...@@ -35,4 +22,25 @@ class PasswordsController < Devise::PasswordsController
end end
end 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 end
...@@ -328,6 +328,10 @@ class User < ActiveRecord::Base ...@@ -328,6 +328,10 @@ class User < ActiveRecord::Base
@reset_token @reset_token
end end
def recently_sent_password_reset?
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
def disable_two_factor! def disable_two_factor!
update_attributes( update_attributes(
two_factor_enabled: false, two_factor_enabled: false,
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.devise-errors .devise-errors
= devise_error_messages! = devise_error_messages!
.clearfix.append-bottom-20 .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 .clearfix
= f.submit "Reset password", class: "btn-primary btn" = f.submit "Reset password", class: "btn-primary btn"
......
# Additional translations at http://github.com/plataformatec/devise/wiki/I18n # Additional translations at https://github.com/plataformatec/devise/wiki/I18n
en: 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: errors:
messages: 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" expired: "has expired, please request a new one"
not_found: "not found" not_found: "not found"
already_confirmed: "was already confirmed, please try signing in"
not_locked: "was not locked" not_locked: "was not locked"
not_saved: not_saved:
one: "1 error prohibited this %{resource} from being saved:" one: "1 error prohibited this %{resource} from being saved:"
other: "%{count} errors 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 ...@@ -95,7 +95,7 @@ feature 'Login', feature: true do
user = create(:user, password: 'not-the-default') user = create(:user, password: 'not-the-default')
login_with(user) login_with(user)
expect(page).to have_content('Invalid email or password.') expect(page).to have_content('Invalid login or password.')
end end
end end
end end
require 'spec_helper' require 'spec_helper'
feature 'Password reset', feature: true do feature 'Password reset', feature: true do
describe 'with two-factor authentication' do describe 'throttling' do
let(:user) { create(:user, :two_factor) } it 'sends reset instructions when not previously sent' do
it 'requires login after password reset' do
visit root_path visit root_path
forgot_password(create(:user))
forgot_password expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))
reset_password
expect(page).to have_content("Your password was changed successfully.")
expect(page).not_to have_content("You are now signed in.")
expect(current_path).to eq new_user_session_path expect(current_path).to eq new_user_session_path
end end
end
describe 'without two-factor authentication' do it 'sends reset instructions when previously sent more than a minute ago' do
let(:user) { create(:user) } user = create(:user)
user.send_reset_password_instructions
user.update_attribute(:reset_password_sent_at, 5.minutes.ago)
it 'requires login after password reset' do
visit root_path visit root_path
forgot_password(user)
forgot_password expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))
reset_password
expect(page).to have_content("Your password was changed successfully.")
expect(current_path).to eq new_user_session_path expect(current_path).to eq new_user_session_path
end 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 end
def forgot_password def forgot_password(user)
click_on 'Forgot your password?' click_on 'Forgot your password?'
fill_in 'Email', with: user.email fill_in 'Email', with: user.email
click_button 'Reset password' click_button 'Reset password'
user.reload user.reload
end 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 end
...@@ -228,6 +228,26 @@ describe User do ...@@ -228,6 +228,26 @@ describe User do
end end
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 describe '#disable_two_factor!' do
it 'clears all 2FA-related fields' do it 'clears all 2FA-related fields' do
user = create(:user, :two_factor) user = create(:user, :two_factor)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment