Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 4 - Allow password change only if current password is correct

In my app, users can edit their profile information. On the edit profile form, the user can make changes to all fields (name, title, and more). On this same form are three fields: current_password, password, and password_confirmation. I am using bcrypt's has_secure_password feature for password authentication. I am NOT using Devise at all.

I want users to only be able to change their password if they have supplied a correct current password. I've got this working before with the following code in the update method of my Users controller:

# Check if the user tried changing his/her password and CANNOT be authenticated with the entered current password
if !the_params[:password].blank? && [email protected](the_params[:current_password])
  # Add an error that states the user's current password is incorrect
  @user.errors.add(:base, "Current password is incorrect.")
else    
  # Try to update the user
  if @user.update_attributes(the_params)
    # Notify the user that his/her profile was updated
    flash.now[:success] = "Your changes have been saved"
  end
end

However, the problem with this approach is that it discards all changes to the user model if just the current password is incorrect. I want to save all changes to the user model but NOT the password change if the current password is incorrect. I've tried splitting up the IF statements like so:

# Check if the user tried changing his/her password and CANNOT be authenticated with the entered current password
if !the_params[:password].blank? && [email protected](the_params[:current_password])
  # Add an error that states the user's current password is incorrect
  @user.errors.add(:base, "Current password is incorrect.")
end

# Try to update the user
if @user.update_attributes(the_params)
  # Notify the user that his/her profile was updated
  flash.now[:success] = "Your changes have been saved"
end

This doesn't work because the user is able to change his/her password even if the current password is incorrect. When stepping through the code, although the "Current password is incorrect." error is added to @user, after running through the update_attributes method, it seems to ignore this error message.

By the way, the current_password field is a virtual attribute in my User model:

attr_accessor :current_password

I've been stuck trying to figure this out for a couple of hours now, so I can really use some help.

Thanks!


Solution

Thanks to papirtiger, I got this working. I changed the code around a little bit from his answer. Below is my code. Note that either code snippet will work just fine.

In the User model (user.rb)

class User < ActiveRecord::Base
  has_secure_password

  attr_accessor :current_password

  # Validate current password when the user is updated
  validate :current_password_is_correct, on: :update

  # Check if the inputted current password is correct when the user tries to update his/her password
  def current_password_is_correct
    # Check if the user tried changing his/her password
    if !password.blank?
      # Get a reference to the user since the "authenticate" method always returns false when calling on itself (for some reason)
      user = User.find_by_id(id)

      # Check if the user CANNOT be authenticated with the entered current password
      if (user.authenticate(current_password) == false)
        # Add an error stating that the current password is incorrect
        errors.add(:current_password, "is incorrect.")
      end
    end
  end
end

And the code in my Users controller is now simply:

# Try to update the user
if @user.update_attributes(the_params)
  # Notify the user that his/her profile was updated
  flash.now[:success] = "Your changes have been saved"
end
like image 720
Alexander Avatar asked May 18 '15 00:05

Alexander


3 Answers

You could add a custom validation on the model level which checks if the password has changed:

class User < ActiveRecord::Base
  has_secure_password

  validate :current_password_is_correct,
           if: :validate_password?, on: :update

  def current_password_is_correct
    # For some stupid reason authenticate always returns false when called on self
    if User.find(id).authenticate(current_password) == false
      errors.add(:current_password, "is incorrect.")
    end
  end

  def validate_password?
    !password.blank?
  end

  attr_accessor :current_password
end
like image 125
max Avatar answered Sep 19 '22 13:09

max


So thinking from a user perspective, if someone enters the wrong password wouldn't you want the other stuff to not change as well? Normally people will have a password update where it is just email and password. If the current password is incorrect then don't update anything.

If you have to do it this way then just move the logic and have two sets of params or delete the password from the params. Here would be psuedocode for it.

if not_authenticated_correctly
  params = params_minus_password_stuff (or use slice, delete, etc)
end

#Normal update user logic
like image 29
Austio Avatar answered Sep 18 '22 13:09

Austio


Another approach is to use a custom validator instead of embedding this validation within the model. You can store these custom validators in app/validators and they will be automatically loaded by Rails. I called this one password_match_validator.rb.

In addition to being reusable, this strategy also removes the need to re-query for User when authenticating because the User instance is passed to the validator automatically by rails as the "record" argument.

class PasswordMatchValidator < ActiveModel::EachValidator

   # Password Match Validator
   #
   # We need to validate the users current password
   # matches what we have on-file before we change it
   #
   def validate_each(record, attribute, value)
     unless value.present? && password_matches?(record, value)
       record.errors.add attribute, "does not match"
     end
   end

   private

   # Password Matches?
   #
   # Need to validate if the current password matches
   # based on what the password_digest was. has_secure_password
   # changes the password_digest whenever password is changed.
   #
   # @return Boolean
   #
   def password_matches?(record, value)
     BCrypt::Password.new(record.password_digest_was).is_password?(value)
   end
 end

Once you add the validator to your project you can use it in any model as shown below.

class User < ApplicationRecord

  has_secure_password

  # Add an accessor so you can have a field to validate
  # that is seperate from password, password_confirmation or 
  # password_digest...
  attr_accessor :current_password

  # Validation should only happen if the user is updating 
  # their password after the account has been created.
  validates :current_password, presence: true, password_match: true, on: :update, if: :password_digest_changed?

end

If you don't want to add the attr_accessor to every model you could combine this with a concern but that is probably overkill. Works well if you have separate models for an administrator vs a user. Note that the name of the file, the class name AND the key used on the validator all have to match.

like image 34
John Saltarelli Avatar answered Sep 17 '22 13:09

John Saltarelli