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!
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
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
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With