I am trying to create a means for a user to update their details without a password. I am using BCrypt with has_secure_password in my user model so that when a user signs up or changes password, the password and password_confirmation fields are checked to match before being saved to password_digest. I also have the following validation for setting a password:
validates :password, presence: true, length: { minimum: 5 }
As is users are able to update their password fine (so long as it meets the validations by being at least 5 characters long). But if a user tries to update their details (I have separate views/forms for updating password and updating other non-required attributes) then it gives an error saying "Password can't be blank, Password is too short (minimum is 5 characters)"
I have read through a lot of previous stackoverflow questions/answers and the closest I've got is adding , allow_blank: true to the end of the validation. This pretty much works as I want it to as BCrypt handles the validation if the password is blank so when signing up a user can't give a blank password, and users are able to change their details without a password. However when updating a password if the user gives a blank password then something weird happens. The blank password doesn't save (As it shouldn't since because BCrypt still needs a matching password and password_confirmation, which can't be blank), however the controller acts as though it does and passes the notice "password updated.". Here is my code in the controller:
def update
if Current.user.update(password_params)
redirect_to root_path, notice: "Password updated."
else
render :edit
end
end
private
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
I am particularly confused as to why Current.user.update(password_params) seems to be returning true (hence redirecting to the root path and passing the notice mentioned before) but the password definitely hasn't been updated. I have checked by logging out and back in again with the previous password and a blank password does not allow me to log in.
In cases like these, your best chance is to go to the source code of has_secure_password. You will find following code there:
define_method("#{attribute}=") do |unencrypted_password|
if unencrypted_password.nil?
self.public_send("#{attribute}_digest=", nil)
elsif !unencrypted_password.empty?
instance_variable_set("##{attribute}", unencrypted_password)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
end
end
when you call has_secure_password this code will create setter where attribute is password. It will dynamically create following method:
def password=(unencrypted_password)
if unencrypted_password.nil?
self.password_digest = nil
elsif !unencrypted_password.empty?
#password = unencrypted_password
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost))
end
end
This method then gets called when you assign attributes to your model:
Current.user.update(password_params)
As you can see newly defined method contains simple condition with two branches:
If you set password to nil it will delete the password_digest
If you set password to nonempty string it will store password hash to password_digest attribute
That is all. When your user sends empty password this method decides to do nothing and ignores the empty string, it doesn't get automatically assigned as empty password. Model pretty much completely ignores input (if password_confirmation is also empty), nothing gets saved, no validation is violated which means that Current.user.update(password_params) returns success and your controller proceeds with "Password updated." branch.
Now, when you know about this behaviour what can you do about it?
Let's say you model looks like this
class User < ApplicationRecord
has_secure_password
validates :password, presence: true, length: { minimum: 5 }
end
And you want your validation to work in two cases
When new user is created
When users update their password
First case is easy, as you mentioned in your question if you use allow_blank: true instead of presence: true validation is handled by has_secure_password if password is empty and if it is not, password will proceed through the validation.
But than we get to other requirements:
User has to be able update other attributes then just password
We still want to validate password if it gets updated
These two requirements rule out the presence: true part of the validation, it cannot be there. Same thing applies to allow_blank: true. In this case you probably want to use conditional validation:
class User < ApplicationRecord
has_secure_password
validates :password, length: { minimum: 5 }, if: :password_required?
private
def password_required?
password.present?
end
end
This code ensures that validation is executed every time user fills in the password. It doesn't matter if it's create or update action.
Now to the last thing.
What if user leaves empty password.
From your question I suppose you want to show validation error if user sends empty password. From description above User model doesn't know that it should validate the password at all. You have to tell your model about it eg. like this:
class User < ApplicationRecord
has_secure_password
validates :password, length: { minimum: 5 }, if: :password_required?
def enforce_password_validation
#enforce_password_validation = true
end
private
def password_required?
#enforce_password_validation || password.present?
end
end
Now you could use it like this in your controller:
def update
user = Current.user
user.enforce_password_validation
if user.update(password_params)
redirect_to root_path, notice: "Password updated."
else
render :edit
end
end
private
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
Now validation should fail even if user submits empty password.
I made the same configuration as the first answer.
This my User.rb model
class User < ApplicationRecord
validates :email, presence: true
validates :password, length: { minimum: 6 }, if: :password_required?
validates :password_confirmation, length: { minimum: 6 }, if: :password_required?
has_secure_password
private
def password_required?
false || password.present?
end
end
It worked adding false at the beginning of the method.
Now, in my controller, I have this:
class UsersController < ApplicationController
...
def confirm_email
user = User.find_by(token: params[:token])
user.update(is_confirmed?: true)
redirect_to login_path, notice: "You've confirmed your account"
end
...
end
I hope this helps!
For the validation of the password in your model try this instead :
validates :password, presence: true, length: { minimum: 5 }, on: :create
This line mean that the validation will be only call on :create method not :update anymore. So that you can remove the allow_bank and your update password will work fine
Related
I'm a programming/rails beginner, and have encountered a bug I cannot wrap my head around.
I'm using/learning about the "has_secure_password" method. When I try and create a user in my console with a mismatched password/confirm_password, the console returns false and the error is "Password confirmation doesn't match Password". But, when I try and do the same thing within the UI given the below code (+ a view), it saves just fine! Now, notice that in my "user_params" method, I accidentally forgot to permit ":password_confirmation" which is how I noticed this issue in the first place. With that ":password_confirmation" added, the view throws an error but that's not the point. Why even without this is the new User record being successfully created with a mismatched password and password confirmation, even though it doesn't save in the console?
Here is my User model:
class User < ActiveRecord::Base
has_secure_password
validates :name, presence: true
validates :email, presence: true, format: /\A\S+#\S+\z/, uniqueness: {case_sensitive: false}
validates :password, length: {minimum: 4, allow_blank: true}
end
And my User controller:
class UsersController < ApplicationController
def index
#users = User.all
end
def show
#user = User.find(params[:id])
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
redirect_to #user, notice: "Thanks for signing up!"
else
render :new
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password)
end
end
This is happening because the password_confirmation attribute is optional. When it isn't supplied to the model that has_secure_password, the model simply accepts the password.
When your password confirmation attribute isn't whitelisted in your controller via user_params, it isn't being passed to the model at all, which is why mismatches appears not to throw an error. In truth the validation isn't taking place at all.
This works in your console because it creates a user without involving a controller or strong parameter whitelisting.
First of all, I believe there must be some people, who already asked this question before but I don't know how can I google this problem. So, if it is duplicate I am sorry.
I am working on a social media site. I have user model, which I use to register users to the site. It validates, name, email, and password when registering.
I use the same model to make users edit their informations, like username.
This is what I have in my update controller:
def update
# Find an existing object using form parameters
#profile = User.find_by_id(current_user.id)
# Update the object
if #profile.update_attributes!(settings_profile_params)
# If save succeeds, redirect to itself
redirect_to request.referrer
else
# If save fails, redisplay the form so user can fix the problems
render('edit')
end
end
private # user_params is not an action, that is why it is private.
def settings_profile_params
params.require(:user).permit(:first_name, :last_name, :username, :school, :program, :website, :information)
end
The problem is, I only want to update strong parameters that I defined there. But I am getting an exception because of password validation. I don't know why am I getting this exception. How can I set up system to update the values in strong parameter only.
Thank you.
You can achieve this by changing you password validation. You need to add a condition on password validation.
# Password
validates :password,
:presence => {:message => 'Password cannot be blank'},
:length => {:within => 8..99, :message => 'Password length should be within 8 and 99 characters'}
:if => Proc.new { new_record? || !password.nil? }
By calling update_attributes you are implicitly invoking the same range of validations as an other update and save. You need to update on the specific params you're targeting (e.g. omitting :password).
Here, we can store that list of permitted keys in a variable that is reusable. Then we call update_attribute on each of those keys — doing so within a reduce that gives the same true/false for the switch to edit or display.
def update
# Find an existing object using form parameters
#profile = User.find_by_id(current_user.id)
# Update the object
if PERMITTED_KEYS.reduce(true) {|bool, key| bool &&= #profile.update_attribute(key, #profile.send(key)) }
# If save succeeds, redirect to itself
redirect_to request.referrer
else
# If save fails, redisplay the form so user can fix the problems
render('edit')
end
end
private
PERMITTED_KEYS = [:first_name, :last_name, :username, :school, :program, :website, :information]
# user_params is not an action, that is why it is private.
def settings_profile_params
params.require(:user).permit(PERMITTED_KEYS)
end
Having not used strong_parameters gem before, I think this would be more idiomatic to the use of the gem:
def update
# Find an existing object using form parameters
#profile = User.find_by_id(current_user.id)
# Update the object
if settings_profile_params.keys.reduce(true) {|bool, key| bool &&= #profile.update_attribute(key, #profile.send(key)) }
# If save succeeds, redirect to itself
redirect_to request.referrer
else
# If save fails, redisplay the form so user can fix the problems
render('edit')
end
end
private
# user_params is not an action, that is why it is private.
def settings_profile_params
params.require(:user).permit(
:first_name, :last_name, :username,
:school, :program,
:website, :information
)
end
Though, I still think this is a duplicate question, since it regard how to update model data without all of the defined validation. I've answered in case the update_attributes loop is felt to be a sufficiently unique solution to warrant non-duplication.
Okay, now I found the problem. First of all, #Muntasim figured out a way to solve this problem. But I actually don't need to use this solution, because there is another easy way to fix this.
In this situation, when I let users to update their profiles, rails should not validate my password or any other column in user model, if I don't ask it to. But why was it validating? Because I have validates :password in user model. Instead it has to be validates :digest_password. Because I am using bcrypt.
I don't know why :password was working fine when I register even though I used bcrypt.
I have a payment api that takes bank account info and user info. I catch the api response and use ajax to send the infomation into my controller where I try to save the information to my user. When I save I get the error Validation failed: Password can't be blank, Password is invalid: Any ideas?
Bank Controller:
def addbank
#user = current_user
#user.phone_number = params[:phone_number]
#user.birth_year = params[:year]
#user.bank_uri = (params['bank_acct_uri'])
#user.save! # <------- ERROR here!
# Code was removed to be more clear
end
User Controller:
def update
# update user controller has been commented out but the error is still there
end
User Model:
class User < ActiveRecord::Base
attr_accessible :email,:password,:password_confirmation,:phone_number,:birth_year
attr_accessor :password
before_save :encrypt_password
before_save { |user| user.email = email.downcase }
VALID_PASSWORD_REGEX = # some reg-expression
VALID_PHONE = # some reg-expression
validates_confirmation_of :password
validates :password, presence: true, format:{ with: VALID_PASSWORD_REGEX }
validates :phone_number, format: { with: VALID_PHONE }, if: :phone_number
end
Edit: Why is saving user not hitting my update user controller?
If you want to avoid the validation of one particular field (password in your case), but you want to do all the other validations (for example phone number), you can do something like this:
attr_accessor :skip_password
validates :password, presence: true, format:{ with: VALID_PASSWORD_REGEX }, unless: :skip_password
Then, in your controller, you could do:
def addbank
#user = current_user
#user.skip_password = true # We don't want to validate this..
#user.phone_number = params[:phone_number]
#user.birth_year = params[:year]
#user.bank_uri = (params['bank_acct_uri'])
#user.save! # <------- ERROR here!
# Code was removed to be more clear
end
Do this at your own risk ~~
Where are you storing the encrypted password?
If you store it in password then it will fail validation every save because it doesn't equal password_confirmation.
I'd recommend putting the password in a separate field.
#from the User Model
attr_accessor :password, :password_confirmation
validates_presence_of :password, :on => :create
validates_confirmation_of :password
def password=(password)
#password = password
self.password_digest = BCrypt::Password.create(#password, :cost => 14)
end
def authenticate(password)
BCrypt::Password.new(self.password_digest) == password
end
This way the password gets hashed and saved to password_digest and won't trigger a validation error on save.
You can try to save without validation:
#user.save(:validate => false)
UPDATE:
if !#user.valid? && #user.errors[:phone_number].any?
#do not save
else
#user.save(:validate => false)
end
I'll post this as I am about 95% certain this is the cause, but I apologize if I'm off.
I believe the cause of this is because the user's password is indeed blank. If you look at your database, you'll see a column probably called encrypted_password, which is never directly accessed via your model, nor is it ever de-crypted into your accessible password and password_confirmation attributes.
In order to update the user, you will have to enter re-enter the password, or use the save(false) method to (potentially dangerously) bypass validations.
I have 4 forms associated with the User model, this is how I'd like them to behave -
Registration (new) - Password REQUIRED - email, name, password, password_confirmation
Edit Settings (edit) -
Password NOT REQUIRED - email, name
Change Password - Password REQUIRED, page for logged in users to change their password - current_password, password, password_confirmation
Reset Password - Password REQUIRED, page for unauthenticated users to change their password, after arriving from a password reset email - password, password_confirmation
The closest validation solution I have is -
validates :password, presence: true, length: { minimum: 6 }, on: :update, if: lambda{ |u| u.password.blank? }
This will satisfy points 1, 3 and 4 but it will not allow a user to update attributes i.e. def update (point 2) because there is no params[:user][:password], so the {presence: true} validation is triggered.
What's the best way to validate_presence_of :password, only when the form contained a password field?
Just try this:
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
it will validate password if and only if password field will be present on the form.
But this is dangerous, if somebody delete password field from the html then this validation won't trigger because then you will not get params of password.So you should do something like this:
validates :password, presence: true, length: { minimum: 6 }, if: ->(record) { record.new_record? || record.password.present? || record.password_confirmation.present? }
To answer your question, It seems best to have change_password and reset_password controller actions and do your validations in there.
First, have your model validate password only on create or when it's already entered as such:
validates :password, :presence => true,
:length => { minimum: 6 },
:if => lambda{ new_record? || !password.nil? }
Then for your new controller actions, something along the lines of:
class UsersController < ApplicationController
def change_password
...
if params[:user][:password].present?
#user.update_attributes(params[:user])
redirect_to root_path, notice: "Success!"
else
flash.now.alert "Please enter password!"
render "change_password"
end
end
end
Do the same for reset_password and you should be golden!
You could do something like this:
validates: validator_method
def validator_method(params_input)
if params.empty?
...
else
your_logic_her
end
You'll have to forgive me for using psuedo-code, but the idea is that you create a separate method to validate instead of using validates:
You can use devise gem or else see its implementaion. here is the source code
https://github.com/plataformatec/devise
I'll start by telling you how I want my settings page set up.
I want users to be able to change their settings without requiring a password, and that's how it is set up now with this as the user model
validates :password, presence: true, length: { minimum: 6 }, :on => :create
validates :password_confirmation, presence: true, :on => :update, :unless => lambda{ |user| user.password.blank? }
This makes it so user's can change all of their settings without requiring a password (I know some might frown on this). But i want users to be able to change their passwords on the page like so...User clicks Change Password, a modal pops up, and users have to give their current password and then the new password and a confirmation of the new one. (I know how to do modal, i just want to know how do password reset).
Does this make sense?? I believe the way Pinterest does it is a good example (although they use Python I think)
My suggestion is to use a form object:
app/forms/change_password_form.rb
class ChangePasswordForm
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
# Add all validations you need
validates_presence_of :old_password, :password, :password_confirmation
validates_confirmation_of :password
validate :verify_old_password
attr_accessor :old_password, :password, :password_confirmation
def initialize(user)
#user = user
end
def submit(params)
self.old_password = params[:old_pasword]
self.password = params[:password]
self.password_confirmation = params[:password_confirmation]
if valid?
#user.password = password
#user.password_confirmation = password_confirmation
#user.save!
true
else
false
end
end
def verify_old_password
self.errors << "Not valid" if #user.password != password
end
# This method is required
def persisted?
false
end
end
In controller initialize the form object #pass_form = ChangePasswordForm.new(current_user) and use the object in your modal: form_for #pass_form... and add the old_password, password and password_confirmation fields.
And finally, for example, in the update action:
#pass_form = ChangePasswordForm.new(current_user)
if #pass_form.submit(params[:change_password_form])
redirect_to some_path
else
render 'new'
end
I haven't tested this code, but you get the idea. Take a look to this Railscasts.