I started working on a project which had a lot of code already in place. It is a Ruby on Rail application that uses Devise for user authentication. One of the requirements of the application is that when a user changes their password, they are not allowed to use the same password as the last three passwords they previously used. To acomplish this, there is a table that contains a history of passwords for a given user. These passwords are copies of the encrypted passwords that existed prior to any password change on the user.
Here is where the problem comes in. We have a password change form that collects the new password for a given user. I need to be able to take the new password and encrypt it so that I can match the encrypted value of the new password against encrypted values of the old passwords in history.
Technical Stuff
Rails version 3.0.9
Devise version 1.3.4
Using standard BCrypt with Devise. bcrypt_ruby version 2.1.4
To do this we are overrriding the reset_password method supported by Devise. This allows us to introduce our own method, has_repeated_password in the user controller.
The version of has_repeated_password I started with is below:
def has_repeated_password?
return false if self.new_record? || self.version == 1
histories = self.versions.find(:all, :order => 'version DESC', :limit => 3)
histories.detect do |history|
history.encrypted_password == self.class.encryptor_class.digest(self.password, self.class.stretches, history.password_salt, self.class.pepper)
end
end
The problem here is that the encryptor class is never defined, causing an error every time this routine runs. Even through there are many examples that claim this works, I cannot get it to work when Devise is using the default encryption.
A second attempt at this is the following code:
def has_repeated_password?<br>
return false if self.new_record? || self.version == 1
histories = self.versions.find(:all, :order => 'version DESC', :limit => 3)
histories.detect do |history|
pwd = self.password_digest(self.password)
history.encrypted_password == pwd
end
end
In this case, I never get a password that matches any of the stored passwords, even though I have verified that the password in the database is what I expect.
I have been trying to dig through the Devise code to see what I can find there. I know that the autentication has to do this somehow when it matches passwords collected from users against the stored password.
Any help would be appreciated.
I think I found a solution to my own problem. The key sticking point of this was that I was trying to get an encrypted password that was not part of the user model (any more) tied to Devise. This solution does assume that Devise will be using Bcrypt as the standard encryption tool (can't remember which version of Devise made the move). Bcrypt/Devise actually buries the salt for the password in the encrypted password. If you have the salt and the pepper, you can get the same password to generate the same encrypted value.
So here is the updated code for the routine refernced above:
def has_repeated_password?
return false if self.new_record? || self.version == 1
histories = self.versions.find(:all, :order => 'version DESC', :limit => 3)
histories.detect do |history|
bcrypt = ::BCrypt::Password.new(history.encrypted_password)
password = ::BCrypt::Engine.hash_secret("#{self.password}#{self.class.pepper}", bcrypt.salt)
password == history.encrypted_password
end
end
The key here is that the Bcyrpt object has to be created with an existing encrypted password using the same salt that generated the original password. That is accomplished by giving it my stored historical encrypted password (history.encrypted_password). One of the other key elements is that both the history passwords and the proposed new password use the same pepper, which is managed by Devise. So by using the Engne.has_secret call with the intended new password, it can be compared with the history password.
I had to move the bcrypt code into here because all the password methods supported by Devise assume that you want to act on the user password of the current user object.
Related
how can you find out the user's past encrypted password to check if he is using the old password as the new one, in previous "Authlogic" versions it was easy to check, but in newer versions, I cannot find out the password hash, even if I know the old salt and the new password, is there a way to determine if the user is using the old password as the new one?
how can you .. check if [the user] is using the old password as the new one .. I cannot find out the password hash, even if I know the old salt and the new password ..
If you have always used the same crypto_provider, it should be fairly simple.
salt = current_user.password_salt
old_crypted_pw = current_user.crypted_password
new_plaintext_password = params[:new_password]
provider = User.crypto_provider
new_crypted_pw = provider.encrypt(new_plaintext_password, salt)
if new_crypted_pw == old_crypted_pw
raise 'Cannot reuse password'
# ...
Documentation of these methods can be found in authlogic/acts_as_authentic/password.rb.
If you are transitioning from one crypto provider to another (the transition_from_crypto_providers feature) then you must try to match against all User.crypto_providers.
I would like to store and validate passwords in a ruby application that does not use devise, and have them be compatible with a future application that does use devise. What is the default password hashing scheme that devise uses, and is it possible to extract and use just this component from devise?
Devise's DatabaseAuthenticatable module uses BCrpyt to hash passwords, wrapped up in the Devise::Encryptor module. The relevant method, digest, is pretty simple:
def self.digest(klass, password)
if klass.pepper.present?
password = "#{password}#{klass.pepper}"
end
::BCrypt::Password.create(password, cost: klass.stretches).to_s
end
klass is only used to fetch a couple parameters: pepper, a string which is appended onto the password pre-hashing but not stored in the database (unlike salt, which is appended as well but stored with the password in the DB); and cost, a measure of how secure the hash should be (see the docs). Both of these are static and you can hard-code them into your non-Devise app (but make sure to keep pepper secret!).
So, your hash method might be written just as:
def self.digest(password)
password = "#{password}#{ENV['PASSWORD_PEPPER']}"
::BCrypt::Password.create(password, cost: 10).to_s
end
I have a Ruby on Rails (4.2.1) Application that use Devise (3.5.1) for authenticate users, we are rebuilding this application in Rails (5.0.0) and we are using Devise (4.2.0). The problem happen when I copy the users table from the old application to the new application, then in the new application I can not login using the old data. In the devise.rb initialiser I'm using the same secret_key in both applications so not sure why I can not login into the new app using the old data, any ideas?
To start with, use rails console to ensure the issue is connected with passwords, and not the app - i.e. that valid_password? call of your user model will fail with proper password.
Devise uses this method by default to generate password hashes:
def self.digest(klass, password)
if klass.pepper.present?
password = "#{password}#{klass.pepper}"
end
::BCrypt::Password.create(password, cost: klass.stretches).to_s
end
It uses klass.pepper to add to your password if present. klass here would be your model (e.g. user), which can be configured to use pepper:
Besides :stretches, you can define :pepper, :encryptor,
:confirm_within, :remember_for, :timeout_in, :unlock_in among other
options.
cost is complexity of salt generation for new password storing, so should only influence new hashes generation, and wouldn't affect validation of previously generated passwords.
Devise uses this method to compare input password with stored hashed value:
def self.compare(klass, hashed_password, password)
return false if hashed_password.blank?
bcrypt = ::BCrypt::Password.new(hashed_password)
if klass.pepper.present?
password = "#{password}#{klass.pepper}"
end
password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt)
Devise.secure_compare(password, hashed_password)
end
Just debug it in your both app versions to see what input parameters in this method might be different, e.g. if you have different pepper defined for your model, bcrypt.salt is different in your two apps for the same hashed_value.
As to where salt is taken from existing stored hashed password, it's really simple. The stored string is simply split by $ symbol:
# call-seq:
# split_hash(raw_hash) -> version, cost, salt, hash
#
# Splits +h+ into version, cost, salt, and hash and returns them in that order.
def split_hash(h)
_, v, c, mash = h.split('$')
return v.to_str, c.to_i, h[0, 29].to_str, mash[-31, 31].to_str
end
In my Rails app, I periodically require the user to re-enter their password after a certain amount of inactivity--like with sudo on Linux. My app uses Authlogic for authentication and handling password storage and encryption.
I need some method to encrypt the password the user enters using the exact same encryption scheme Authlogic uses to encrypt passwords when it verifies passwords during authentication. I need to 1) encrypt the password the user enters and 2) do a string comparison between this encryption and the encrypted password stored in the database for the user.
Where should I put the method to perform this encryption? Here are some ideas:
Idea 1 (in a new, custom module)
module PasswordCryption
include Authlogic::ActsAsAuthentic::Password
encrypt_password(password)
end
end
Idea 2 (in the User model)
class User
acts_as_authentic <---- makes Authlogic password encryption functionality available
encrypt_password(password)
end
end
Authlogic uses the SHA512 encryption by default. The clue is that authlogic repeated the hexdigest function 20 times. This will solve your problem:
digest = [raw_password, password_salt].join('')
crypted_password = 20.times { digest = Digest::SHA512.hexdigest(digest) }
I know that I can use Query language to find the record I want. I am doing a login page, I want to find the record which match the user name and password, but I don't want to loop all the elements to find out the user I want (<% #users.each do |user| %>), wt should I do in RoR, except typing SQL.
perhaps:
User.first(:conditions => {:login => 'ted', :password => 'secret'})
# returns nil for no match and first match for a good record
# make sure there is a unique index on login
For authentication I would strongly recommend authlogic (railscast)
You can use dynamic finders to find user by user_name and password:
#user = User.find_by_user_name_and_password('scott', 'tiger')
While the other answers provided by Sam and Chandra are technically correct, both solutions implies that passwords are stored in plain text--which is a very bad idea. If somebody who shouldn't gets access to your database, they'll have a full set of usernames (and potentially email addresses), combined with all of their passwords.
Instead, consider using an algorithm to make sure your password is encrypted in the database, such as bcrypt. You'll need the bcrypt-rub gem to use it.
You should also consider leaving out the password from the query altogether. This is good practice as it provides an extra level of security; SQL injections become more difficult to perform. If users have unique usernames, just fetching the username should return the same object, after which you can check if the password is correct:
#user = User.find_by_username(params[:username])
if #user.password == params[:password]
# do something
else
# do something else
end
Ideally, you should both use bcrypt and leave out the password from the query. How to do this is described in the bcrypt-ruby readme on GitHub (the link I provided).