I'm using has_secure_password in a User model. I have implemented a way for users to change their password outside of the model, but to keep things DRY, I'm trying to move the validations needed from the controller to the model.
The User model looks something like this:
class User
include Mongoid::Document
include ActiveModel::SecurePassword
has_secure_password
field: :password_digest, type: String
attr_accessible :password, :password_confirmation, :current_password
end
Users change their passwords by submitting the following:
user[current_password] - Currently stored password
user[password] - New password
user[password_confirmation] - New password confirmation
I'm using update_attributes(params[:user]) on the User model for the current user. My problem is that calling update_attributes updates the password_digest before using validations, so the following code won't work:
def password_validation_required?
password_digest.blank? || !password.blank? || !password_confirmation.blank?
end
validate(on: :update, if: :password_validation_required?) do
unless authenticate(current_password)
add(:current_password, 'invalid password')
end
end
authenticate is authenticating based on the new password_digest generated from user[password]. Is there an elegant way to access the old password_digest value for authentication? One idea I had was to re-query the user to gain access to another authenticate method that will authenticate against the old password_digest value. The problem is that it's not a clean solution.
I think this one's a bit cleaner than #Parazuce's:
validate :validates_current_password
private
def validates_current_password
return if password_digest_was.nil? || !password_digest_changed?
unless BCrypt::Password.new(password_digest_was) == current_password
errors.add(:current_password, "is incorrect")
end
end
The password_digest field has ActiveModel::Dirty methods associated with it, so I decided to go with:
validate(on: :update, if: :password_validation_required?) do
unless BCrypt::Password.new(password_digest_was) == current_password
errors.add(:current_password, "is incorrect")
end
end
This prevents the need to override password= with additional logic which could introduce bugs in the future if other features used password=.
Related
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
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.
Just posting it, works for ror 6.x
form.erb file:
<div class="field">
<%= form.label :current_password, 'Current password:' %>
<%= form.password_field :current_password, size: 40 %>
</div>
<div class="field">
<%= form.label :password, 'Password:'%>
<%= form.password_field :password, size:40 %>
</div>
<div class="field">
<%= form.label :password_confirmation, 'Confirm:' %>
<%= form.password_field :password_confirmation, id: :user_password_confirmation, size:40 %>
</div>
<div class="actions">
<%= form.submit %>
</div>
user.rb:
has_secure_password
# virtual attribute
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
return 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(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
users_controller.rb:
only need to add ":current_password" to def user_params or pass change wont work and in server log will be written:
Unpermitted parameter: :current_password
I'm not sure on how to best tackle this issue - I need to validate a user's password when adding a new record and I also need to be able to validate when updating the passoword too. But how can I let a user update just part of their profile and perhaps leaving the password blank.
Note before you suggest allow_blank I am aware of this option but this is not suitable because when a user needs to change their password as a result of losing/forgetting it I don't want to allow user to have a blank password.
validates :password, presence: true, if: lambda { |user| user.password_changed? }
by ActiveModel::Dirty (available on all models by default)
You can pass an if statement to a validation:
`validates_presence_of :password, :if => :should_validate_password?
You should be able to pass a condition in here to catch whether the user is updating their password or not:
Model
def should_validate_password?
updating_password || new_record?
end
Controller
#user.updating_password = true
#user.save
See here for more details: http://railscasts.com/episodes/41-conditional-validations
Updated:
In your case I would create an if statement in the controller that detected whether any new password was being passed in the params. If so I would set #user.updating_password = true which would trigger the validation in the model. If not then #user.updating_password would be nil and the validation wouldn't trigger.
Fully custom way
Usually, #password= and #password_confirmation= are just virtual setters, the true attribute is #hashed_password or something. So, you could do something like this :
class User
attr_accessor :password, :password_confirmation
validate :validates_password
validates_presence_of :hashed_password
private
def validates_password
if password or password_confirmation
if password != password_confirmation
errors.add( :password, 'your message' )
end
# your others validations
self.hashed_password = hash_password
end
end
def hash_password
# your hashing code
end
end
When user is created providing password, virtual attributes password and password_confirmation are set, so if condition is true and validations are enforced.
When password is already set and user does not change it (an edit form without providing passwords), validation will not be enforced, because of if password and password_confirmation.
When password is already set and user change it, password and password_confirmation are set, so validation is triggered.
When you want to reset password, just set in your action hashed_password to nil. The model is now invalid because of validates_presence_of :hashed_password and user has to provide a new one.
Using #has_secure_password
With #has_secure_password, rails will handle most of this, especially :
it creates virtual atttributes
it will trigger validation for confirmation match only if password attributes are provided
it will hash password
So, what you need is just adding your own validations, doing it only when password or password_confirmation are present.
class User
has_secure_password
validate :validates_password
private
def validates_password
if password or password_confirmation
unless <your_test>
errors.add( :password, '<your error message>' )
end
end
end
end
As previously, this will only be triggered if password and password_confirmation are provided, which only happens if user submitted them as form data (as real attribute is password_digest).
In my app, only admins can create new User records. The user is emailed an activation link where they set their password.
I'd like to use the has_secure_passord method (railscast):
class User < ActiveRecord::Base
has_secure_password
...
end
Works great, but it automatically validates presence of password digest...so when the admin creates the record the validations fail. I there a way to skip just the automatically added password_digest validation without skipping the others I've added?
Since 4.X versions of rails, has_secure_password takes an option :validations. If you set it to false, it will not run validations.
The 3.X version of the gem does not support this parameter. However you can backport the activemodel/lib/active_model/secure_password.rb from latest 4.0.XRC code which supports this argument.
So with that, your code will look like this:
class User < ActiveRecord::Base
has_secure_password :validations => false
...
end
I decided to do my own custom authentication. The following solution will validate passwords but only when they are being set. This allows admins to create users without adding a password.
class User < ActiveRecord::Base
include BCrypt
attr_accessor :password, :password_confirmation
validates :password, length: (6..32), confirmation: true, if: :setting_password?
def password=(password)
#password = password
self.password_hash = Password.create(password)
end
def authenticate(password)
password.present? && password_hash.present? && Password.new(password_hash) == password
end
private
def setting_password?
password || password_confirmation
end
end
If someone posts an answer that allows me to still use the has_secure_password method, I'll accept it instead.
There's no way to skip the validation, but it would be easy enough to write your own version of the method that allows you to pass an argument to determine whether or not to validate presence of the password_digest field.
Just extend ActiveModel the same way they do in the SecurePassword module (via ActiveSupport::Concern) and add your own secure password method.
i.e.
module ActiveModel
module MySecurePassword
extend ActiveSupport::Concern
module ClassMethods
def my_has_secure_password(validate_password_digest=true)
# you custom logic
end
end
end
end
Piggybacking off of Journeyer and Tybro's answers I wanted to see if you can add a private method that returns true or false to the validations hash. It worked and I used it for a different use case but in this context it would look something like this.
class User < ActiveRecord::Base
has_secure_password validations: !:setting_password
#If setting_password is true then this will be the same as
validations: false
.....
private
def setting_password?
#Some logic that determines if a user is setting a password and resolves
to true or false
password || password_confirmation
end
...
end
I'm using authlogic with rails 3. I have this in my user model:
validates :last_name, :presence => true
acts_as_authentic do |c|
c.validates_length_of_password_field_options = {:minimum => 7}
end
And then I have a controller action that updates the user's name:
def update_name
if #current_user.update_attributes(params[:user])
flash[:success_name] = true
redirect_to edit_user_via_account_settings_path
else
render 'edit_user_via_account_settings'
end
end
If the user enters a blank last name and attempts to update their name with this controller action, the #current_user model correctly has errors on last name, but it also has errors on password (password must be a minimum of 7 chars). How can I only validate the password if the password is being updated?
You need to use the merge_validates_* config methods instead of the validates_* methods. The former keeps the conditionals (like ignore blank password) and the latter overwrites them. That should clear everything up. And don't use the assignment on the merge_* methods.
a.merge_validates_length_of_password_field_options :minimum => 7
I think the config you are looking for is found here
http://rdoc.info/github/binarylogic/authlogic/master/Authlogic/ActsAsAuthentic/Password/Config#ignore_blank_passwords-instance_method
Unless its a new user, if no password is supplied it does not validate the password.
My User model contains :name, :email, and :password fields. All 3 have validations for length. An "update account" web page allows the user to update his name and email address, but not password. When submitted, params[:user] is
{"name"=>"Joe User", "email"=>"user#example.com"}
Note there is no "password" key because the form doesn't contain such an input field.
When I call
#user.update_attributes(params[:user])
the password validation fails. However, since I'm not attempting to update the password, I don't want the password validation to run on this update. I'm confused why the password validation is running when params[:user] doesn't contain a "password" key.
Note that I want to have a separate web page elsewhere that allows the user to update his password. And for that submission, the password validation should run.
Thank you.
My application does something like this
attr_accessor :updating_password
validates_confirmation_of :password, :if => should_validate_password?
def should_validate_password?
updating_password || new_record?
end
so you have to model.updating_password = true for the verification to take place, and you don't have to do this on creation.
Which I found at a good railscast at http://railscasts.com/episodes/41-conditional-validations
In your user model, you could just ignore the password validation if it's not set.
validates_length_of :password, :minimum => N, :unless => lambda {|u| u.password.nil? }
Using update_attributes will not change the value of the password if there is no key for it in the params hash.
Validation doesn't run against the changed fields only. It validates existing values too.
Your validation must be failing because the password field contains some invalid content that's already saved in the database. I'm guessing it's probably because you're hashing it after validation and you're trying to validate the hashed string.
You can use a virtual attribute (an instance variable or method) that you validate with a custom method, and then assign the hash to the stored password field. Have a look at this technique for ideas.
An app that I am working on uses the following:
validates_confirmation_of :password,
:if => Proc.new { |account|
!account.password.blank?
|| !account.password_confirmation.blank?
|| account.new_record? }
Depending on your requirements, you might want to remove the new_record? check
When password is added then only confirmation will be called and presence will call on create action only
**validates_presence_of :password, :on =>:create**
**validates_confirmation_of :password**