ruby-on-railsauthenticationmodelpasswordsbcrypt-ruby

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? && !@user.authenticate(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? && !@user.authenticate(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

Solution

  • 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