I'm trying to implement a method to allow password to be changed from another service outside devise anyhow.
# Profile password change
def change_password(oldpass, newpass)
pepper = nil
cost = 10
# Encrypt plain text passwords
encrypt_old = ::BCrypt::Password.create("#{oldpass}#{pepper}", :cost => cost).to_s
# Validate old
if self.encrypted_password == encrypt_old
encrypt_new = ::BCrypt::Password.create("#{newpass}#{pepper}", :cost => cost).to_s
self.encrypted_password = encrypt_new
self.save
else
Logger.new("Wrong old password!")
end
end
It seems i got the password encryption wrong oldpass contains a plain text of old password i need to hash it see if it matches the current password then allow new password to be stored. However all that i'm getting is wrong password.
Reworked:
def change_password(oldpass, newpass)
if valid_password?(oldpass)
password = newpass
save
return true
else
return false
end
end
You don't need to encrypt the password yourself, if you are in the application or in Rails console.
Just update the user following way and Devise will take care of it itself.
user.password = new_password
user.save
Devise will then encrypt the password and store it. You just need to ensure that user.password_confirmation is nil. If password_confirmation is anything else, it will be matched against password.
EDIT
You can check the existing password with: user.valid_password?(old_password)
With Devise, you don't need to handle using bcrypt yourself. By default, it handles this and the change password method. You can look at source here or just look in config/intitalizers/devise.rb in your Rails app.
Also, if you use the #update_with_password method provided by Devise, then you can pass it a hash like this:
{ :current_password => 'pass', :password => 'pass_new', :password_confirmation => 'pass_new' }
Or you can omit :password_confirmation if you don't want the user to have to provide a confirmation.
EDIT: I used the wrong field; it should have been 'current_password' instead of 'old_password'.
Here's the method in question in the source for Devise:
# Update record attributes when :current_password matches, otherwise returns
# error on :current_password. It also automatically rejects :password and
# :password_confirmation if they are blank.
def update_with_password(params, *options)
current_password = params.delete(:current_password)
if params[:password].blank?
params.delete(:password)
params.delete(:password_confirmation) if params[:password_confirmation].blank?
end
result = if valid_password?(current_password)
update_attributes(params, *options)
else
self.assign_attributes(params, *options)
self.valid?
self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
false
end
clean_up_passwords
result
end
View on GitHub
Related
In rails 3.2.13 project, I am using devise plugin.
If I am going to change the password then I need to enter current_password for all 3 fields, if I try to enter different password for new_password & confirm_password fields then it will show an error message like "current password is invalid".
I have referred https://github.com/plataformatec/devise/blob/bf5bcd52cb9edaefb002927434d7ede398e74bc5/lib/devise/models/database_authenticatable.rb#L46
In model,
def valid_password?(password)
return false if encrypted_password.blank?
bcrypt = ::BCrypt::Password.new(encrypted_password)
password = ::BCrypt::Engine.hash_secret("#{password}#{self.class.pepper}", bcrypt.salt)
Devise.secure_compare(password, encrypted_password)
end
def update_with_password(params, *options)
current_password = params.delete(:current_password)
if params[:password].blank?
params.delete(:password)
params.delete(:password_confirmation) if params[:password_confirmation].blank?
end
result = if valid_password?(current_password)
update_attributes(params, *options)
else
self.assign_attributes(params, *options)
self.valid?
self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
false
end
result
end
In controller,
#user.update_with_password(params["user"])
Validation should work like,
Current password & new password should be present
If user enters wrong password in current_password field then it should check valid or not
New password & confirm password should match
E.g.:
1st case: (shows invalid password message)
Current Password = "password"
New Password = "abcdefg"
Confirm Password = "abcdefg"
2nd case: (accepts successfully)
Current Password = "password"
New Password = "password"
Confirm Password = "password"
How can I change the password by entering different password instead of current(old) password?
This is from the github page:
require 'bcrypt'
class User < ActiveRecord::Base
# users.password_hash in the database is a :string
include BCrypt
def password
#password ||= Password.new(password_hash)
end
def password=(new_password)
#password = Password.create(new_password)
self.password_hash = #password
end
end
It appears that to access the password method, you need to call it as an attribute from a create method:
#user.password = user_params[:password]
#user.save
Okay...fine? But where's the salt now stored? I just don't get it at all, how is this even remotely secure anymore?
To retrieve a hashed password, you need this method:
def password
#password ||= Password.new(password_hash)
end
And call it as an attribute:
if #user.password == params[:password]
give_token
else
false
end
So it appears everything's working without a salt...how does it do this?
This means I only need one column in my database to do with passowords now, right?password or password_hash instead of password_salt | password_hash?
Well then why does the github page say this:
But even this has weaknesses -- attackers can just run lists of
possible passwords through the same algorithm, store the results in a
big database, and then look up the passwords by their hash:
PrecomputedPassword.find_by_hash(<unique gibberish>).password #=> "secret1"
Salts
And then this is what really gets me:
The solution to this is to add a small chunk of random data -- called a salt -- to the password before it's hashed:
Why are they explaining all of this if bcrypt is handling everything automatically?
hash(salt + p) #=> <really unique gibberish>
The salt is then stored along with the hash in the database, and used to check potentially valid passwords:
<really unique gibberish> =? hash(salt + just_entered_password)
bcrypt-ruby automatically handles the storage and generation of these salts for you.
Could someone explain how bcrypt stores and generates these salts? Why does it say it handles it all for me and then goes on to tell me how to generate a salt? Do I need to run something like this in my model: self.password_hash = hash(salt + p)
Argh so confused I used to get salts and hashes utterly and now they've changed it all. Terrible, unclear docs...they appear to show you how to use bcrypt without a salt with loads of examples, and then briefly mention how to do it properly with a salt down at the bottom.
Could someone please give me an example how to use the new version of bcrypt to generate a salt and hash, and how to authenticate?
Okay, the has_secure_password is really cool. You don't need to worry about salts and hashes anymore, the salt and hash are stored as one attribute ( password_digest) in the database.
It's saved in such a way that bcrypt knows which part of the password_digest string is the salt, and what is the hash.
If you're setting up authentication from scratch, you literally need to do the following:
1) Add the bcrypt rails gem:
gem bcrypt-rails
2) Add the has_secure_password method to the model tasked with handling your user records:
class User < ActiveRecord::Base
has_secure_password
end
3) Make sure your users table has a password_digest column:
class CreateUser < ActiveRecord::Migration
create_table :users do |t|
t.username
t.password_digest
end
end
4) Create a new method to create a a new empty user instance for the form to use:
class UsersController < ApplicationController
def new
#user = User.new
end
end
5) In the new view, make a form that creates populates the params hash' :password and :username entries:
<% form_for( #user ) do |f| %>
<%= f.text_field :username %>
<%= f.password_field :password %>
<% end %>
6) Back in our controller, permit the username and the password using strong params. The whole reason behind strong params is to prevent some cheeky chappy from using dev tools to create their own html form field (such as one pertaining to id) and populating the database with malicious data:
class UsersController < ApplicationController
def new
#user = User.new
end
private
def user_params
params.require(:user).permit(:username, :password)
end
end
7) Let's create the create method that will use these permitted entries to create a new user, populated by the form:
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(user_params)
#user.save
redirect_to root_path
end
private
def user_params
params.require(:user).permit(:username, :password)
end
end
Set up your routes as you see fit, and that's it! The user record's password_digest column will be automatically populated with one string comprised of a salt appended with the hash of the password. Very elegant.
All you need to remember: password -> password_digest.
In order to authorise the user and signout the user, create a sessions controller with a create method and a destroy method:
def create
user = User.find_by_email(params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to admin_root_path, :notice => "Welcome back, #{user.username}"
else
flash.now.alert = "Invalid email or password"
redirect_to root_path
end
end
def destroy
reset_session
flash[:info] = "Signed out successfully!"
redirect_to root_path
end
Hope this helps someone!
bcrypt got everything covered for you. Your password digest consists of a few types of information, bcrypt algorithm type, cost, salt and the checksum.
for example:
my_password = BCrypt::Password.create("my password")
#=> "$2a$10$.kyRS8M3OICtvjBpdDd1seUtlvPKO5CmYz1VM49JL7cJWZDaoYWT."
The first part: $2a$ is the variant of the algorithm see: Where 2x prefix are used in BCrypt?
The second part 10 is the cost parameter, you can increase it to slow down the process (logarithmic value) by providing a hash {cost: 12} as the second argument to create.
Now if you call my_password.salt you get "$2a$10$.kyRS8M3OICtvjBpdDd1se" which identifies the part that is being used as the key to creating your checksum.
And finally, your checksum is "UtlvPKO5CmYz1VM49JL7cJWZDaoYWT.". That's the reason if you call create the second time the string is going to be different as another salt will be used.
But as I mentioned earlier you don't need to do anything extra as all these are being taken care of for you.
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'm trying to set up Omniauth login for Twitter/FB. I created my own authentication system that validates for password and email upon creation of a user. However, I do not want to validate for password or email when my users log in through Twitter/Fb.
I created a user attribute called omniauth_login. I am using it as a boolean to test whether validation is required or not in my should_validate_password? method.
User.rb
attr_accessor :password, :updating_password, :omniauth_login
validates_presence_of :password, :if => :should_validate_password?
validates_confirmation_of :password, :if => :should_validate_password?
def should_validate_password?
(updating_password || new_record?) && !(self.omniauth_login == 'true')
end
def self.create_with_omniauth(auth)
create! do |user|
user.provider = auth["provider"]
user.uid = auth["uid"]
user.name = auth["info"]["name"]
end
end
Here is my controller used to create the user:
sessions_controller.rb
def omniauth_create
auth = request.env["omniauth.auth"]
user = User.new
user.omniauth_login = 'true'
user = User.find_by_provider_and_uid(auth["provider"], auth["uid"]) ||
User.create_with_omniauth(auth)
session[:user_id] = user.id
redirect_to user
end
When I try to create a new user by logging in via twitter, I still get the validation error:
Validation failed: Password can't be blank, Email can't be blank, Email is not valid.
How do I skip validation of password and email if my object is being created in the create_with_omniauth method?
Thanks.
I think the problem here is that you have two separate instances of User, one that has omniauth_login set to 'true', and another that does not. The first is gotten from
user = User.new
user.omniauth_login = 'true'
The second is
User.create_with_omniauth(auth)
. The second instance created here doesn't have omniauth_login set to 'true', so it still runs the validations. Try this instead
user = User.find_by_provider_and_uid(auth["provider"], auth["uid"]) ||
User.create_with_omniauth(auth)
and in create_with_omniauth, add user.omniauth_login = 'true'
.
I'm writing an application for myself, so I've got no rush and the my only target is to do things properly.
For authentication I use devise, but I turned out customizing it a lot.
I've seen some good features coming in Rails 3.1 that could make easier to implement auth myself.
In general, when does Devise stops to be useful and starts getting in your way?
Here is a list of customization I have at the moment, beside views of course, but I still would like to implement at least SEO friendly urls.
# model/User.rb
#this method is called by devise to check for "active" state of the model
def active?
#remember to call the super
#then put our own check to determine "active" state using
#our own "is_active" column
super and self.is_active?
end
protected #====================================================================
# find in db the user with username or email login
def self.find_record(login)
where(attributes).where(["name = :value OR email = :value", { :value => login }]).first
end
# allow no case sensitive email
# (saved downcase, fetched downcase)
def self.find_for_authentication(conditions)
conditions[:email].downcase!
super(conditions)
end
# find the user in the db by username or email
def self.find_for_database_authentication(conditions)
login = conditions.delete(:login)
where(conditions).where(["name = :value OR email = :value", { :value => login }]).first
end
# Attempt to find a user by it's email. If a record is found, send new
# password instructions to it. If not user is found, returns a new user
# with an email not found error.
def self.send_reset_password_instructions(attributes={})
recoverable = find_recoverable_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
recoverable.send_reset_password_instructions if recoverable.persisted?
recoverable
end
def self.find_recoverable_or_initialize_with_errors(required_attributes, attributes, error=:invalid)
case_insensitive_keys.each { |k| attributes[k].try(:downcase!) }
attributes = attributes.slice(*required_attributes)
attributes.delete_if { |key, value| value.blank? }
if attributes.size == required_attributes.size
if attributes.has_key?(:login)
login = attributes.delete(:login)
record = find_record(login)
else
record = where(attributes).first
end
end
unless record
record = new
required_attributes.each do |key|
value = attributes[key]
record.send("#{key}=", value)
record.errors.add(key, value.present? ? error : :blank)
end
end
record
end
# password not required on edit
# see: https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-edit-their-account-without-providing-a-password
def password_required?
new_record?
end
# controllers/registrations_controller.rb
# devise controller for registration
class RegistrationsController < Devise::RegistrationsController
# update_attributes (with final S) without providing password
# overrides devise
# see: https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-edit-their-account-without-providing-a-password
def update
# Devise use update_with_password instead of update_attributes.
# This is the only change we make.
if resource.update_attributes(params[resource_name])
set_flash_message :notice, :updated
# Line below required if using Devise >= 1.2.0
sign_in resource_name, resource, :bypass => true
redirect_to after_update_path_for(resource)
else
clean_up_passwords(resource)
render_with_scope :edit
end
end
end
Thank you
I'd just stick with devise for the time being, your changes aren't huge. However, I'd fork devise and extract the changes you've made into new features. Then attempt to get them pulled into devise itself. That way maintaining them doesn't fall on you, it can fall on the many.
Maintaining a full authentication system can be a real headache and ultimately its reinventing the wheel. It only takes one mistake can leave you wide open.
Also your new find_for_authentication method, this has now been supported in devise, put in your devise initializer...
config.case_insensitive_keys = [ :email ]
Good question - My view would probably be that as long as it makes things easier it's useful. You can always fork devise on github and put your customisation in there to some extent - which is what I've done on one project. I'm also a bit nervous about rolling my own authentication when it can be so important to get it right, especially if other people want to see stuff they shouldn't. But I'll be interested to see what others think.