I have a rails 4 app with devise authentication. I am rebuilding from scratch and would like to write my own authentication system but I have users stored in the database, whose password is stored as encrypted_password, which is what devise uses to store the hashed password. I understand that using bcrypt I should have a password_digestcolumn.
My question is twofold: will bcrypt be able to read what I have stored in my devise encrypted_password column and if so, can I simply rename that database column to password_digestor will this cause problems?
From what I've read, yes, you should be able to just rename the column and use it with your custom authentication.
Refs:
https://github.com/plataformatec/devise/blob/master/lib/devise/models/database_authenticatable.rb#L149-L151
module Devise
def self.bcrypt(klass, password)
ActiveSupport::Deprecation.warn "Devise.bcrypt is deprecated; use Devise::Encryptor.digest instead"
Devise::Encryptor.digest(klass, password)
end
module Models
module DatabaseAuthenticatable
# Digests the password using bcrypt. Custom encryption should override
# this method to apply their own algorithm.
#
# See https://github.com/plataformatec/devise-encryptable for examples
# of other encryption engines.
def password_digest(password)
Devise::Encryptor.digest(self.class, password)
end
and:
https://github.com/plataformatec/devise/blob/master/lib/devise/encryptor.rb#L5-L10
module Devise
module Encryptor
def self.digest(klass, password)
if klass.pepper.present?
password = "#{password}#{klass.pepper}"
end
::BCrypt::Password.create(password, cost: klass.stretches).to_s
end
Related
In my Rails app, I use devise to manage users with devise's default configuration. I need to md5 the user-provided password before going down into the devise layer. That is, two steps are included:
(1) (2)
password_in_plain --- password_in_md5 --- password_in_bcrypt.
The first one(1) is our concern, the last one(2) is not (devise takes care of it).
I generated two devise controllers: registrations and sessions, and added a before_filter to do the job -- md5 the plain password user provided. The user can be registered with a success, but login always fail. Here is my code:
class Users::RegistrationsController < Devise::RegistrationsController
before_filter :md5_password_params, only: [:create]
protected
def md5_password_params
params[:user][:password] = Digest::MD5.hexdigest(params[:user][:password])
params[:user][:password_confirmation] = Digest::MD5.hexdigest(params[:user][:password_confirmation])
end
end
class Users::SessionsController < Devise::SessionsController
before_filter :md5_password, only: [:create]
protected
def md5_password
params[:user][:password] = Digest::MD5.hexdigest(params[:user][:password])
end
end
What's wrong here? Please help. Thanks in advance.
Background: one of my other server apps (written in c++) will create user records in the same database directly. For safety reason, a md5 password will be sent to this server when users make registrations in their phones.
Instead of doing this on the controller level and mucking about with the params you can create your own encryptor for Devise.
One major reason you would want to do this is that if your user fills in the registration form and submits it with invalid values both the password and confirmation fields will contain a MD5 digest of what the user originally typed. This is because you are mutating the params hash before it is fed to the model.
Submitting the form again means that the password is encrypted twice and no longer matches what the user thinks the original password is. On the plus side its extremely secure since nobody can actually guess their password ;)
Based on the current version of Devise you should be able to do something like the following to ensure the password is encrypted before a digest is created and before trying to compare plaintext with the encrypted password.
Make sure to add the devise-encryptable gem to your Gemfile. Its required for anything else than default encryption.
# config/initializers/md5.rb
require 'digest/md5'
module Devise
module Encryptable
module Encryptors
class BCryptPlusMD5 < Base
def self.digest(klass, password)
super(klass, Digest::MD5.hexdigest(password))
end
def self.compare(klass, hashed_password, password)
super(klass, hashed_password, Digest::MD5.hexdigest(password))
end
end
end
end
end
You would can configure what encryptor to use in config/initializers/devise.rb - however I have not been able to find a good source on how the class lookup works.
https://github.com/plataformatec/devise/blob/master/lib/devise/encryptor.rb
I'm currently going through Michael Hartl's book (Ruby on Rails tutorial 3rd edition) and he uses password_digest with his authentication model, I checked out Pragmatic Studio's Rails course and in their sample video, they use passowrd_hash/password_salt in their authentication model. From wikipediaing, it seems that salt is a standard use in cryptography and has a variety of uses for security. All I could really find on password_digest is it works with a rails method has_secure_password.
Is one preferred over the other or do they serve different purposes ultimately?
when using the has_secure_password a lot of the encryption magic is done automatically for you, and a few functions are exposed that make it easier for you to verify the password of a User.
As taken from the has_secure_password documentation:
# Schema: User(name:string, password_digest:string)
class User < ActiveRecord::Base
has_secure_password
end
user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
user.save
# => false, password required
user.password = 'mUc3m00RsqyRe'
user.save
# => false, confirmation doesn't match
user.password_confirmation = 'mUc3m00RsqyRe'
user.save
# => true
user.authenticate('notright')
# => false
user.authenticate('mUc3m00RsqyRe')
# => user
User.find_by(name: 'david').try(:authenticate, 'notright')
# => false
User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe')
# => user
Both systems you mentioned will work, it just comes down on what you wish to use, what options you have available etc.
Manually building a salted hash (based on e.g. SHA1) was indeed a long time the default way to store passwords. However, since a couple of years, it became clear that there need to be better (i.e. more secure) ways to store a password which ensure that even if an attacker could gain access to the raw hashes, they could not easily brute-force them.
As such, in newer Rails versions, passwords are by default saved with has_secure_password using the Bcrypt algorithm which is much more suitable for password storage than a simple salted hash. Note that Bcrypt also uses a salt internally. You just don't have to deal with it yourself.
The method Hartl uses, has_secure_password, uses an algorithm called Bcrypt to salt and hash a password, producing a password_digest string that has three components:
A flag that indicates that the string is a Bcrypt hash
The (unhashed) salt
The result of hashing password+salt
I'm not familiar with the Pragmatic Studio course, but it sounds like they're storing the salt and hash in different attributes.
I am working on a rails 3.2.13 project. I am using devise plugin (devise gem 3.2.2, 1.4.2) for authentication. Using this plugin, how can I validate the current_password field while changing the old password to a new one? Or else, please suggest how I can achieve this by encrypting the given string and matching it with the password already saved without using the devise plugin.
E.g.: One user has encrypted_password like below:
"$2a$10$VrawKYj6zp10XUxbixVzE.7d4QgYjQn9aiuzAuP7fp3PZOLMP5wbu"
while changing the password, if I enter a current_password, it should match the string above (encrypted_password == current_password). How can I validate this?
I believe you need to break your problem down into the following steps:
Determine if the old_password is actually the user's current password.
To do this, you can call:
User.find_by_id([SOME_ID]).valid_password?(old_password)
If this returns true, then you can move on to the next step to begin changing of the password. If it doesn't, then the old_password is incorrect, and you should not allow the changing of password.
The implementation of valid_password? can be found in the Devise gem's /lib/devise/models/database_authenticatable.rb file (at around Line 40). You could use this implementation to roll your own code for validating a password. But, Devise pretty much does it for you if you call valid_password?, so rolling your own seems unnecessary.
If old_password is valid, then verify that new_password matches confirm_new_password.
if (new_password == confirm_new_password)
.
.
.
end
If these match, then set the new password by doing the following:
u = User.find_by_id([SOME ID])
u.password = new_password
u.password_confirmation = confirm_new_password
u.save
You can verify that the password has been changed by:
u.valid_password?(new_password)
Update user with current_password validation:
#user.update_with_password(account_update_params)
# account_update_params - should have :current_password, :password, :password_confirmation
It is default behaviour in Devise::RegistrationsController. If you want update user without password, you should overwrite controller's action
class UsersController < Devise::RegistrationsController
def update_resource(resource, params)
# resource.update_with_password(params)
resource.update_attributes(params)
end
end
Do I understand you right what you want allow users login with encrypted and unencrypted (usual) password?
We have:
user.valid_password?('Password2').should
code on github
So we can overwrite it inside models/user.rb
def valid_password?(password)
encrypted_password == password || super(password)
end
I'm trying to convert an existing Admin model to Devise. We already have a password hash but it's obviously not Devise compatible. What I would like to do is accept the login form and check the provided password against the encrypted password. If it's not correct, use the old hash to check the password and if it matches, empty the old password_hash field and set Devise's password to the provided password and save the model.
What's the best way to move forward? I suspect that I need to override something, perhaps in a custom controller, but I'm not entirely sure how to proceed.
You can let Devise do the "hard work" of encrypting the password with the new crypt scheme, as shown in https://gist.github.com/1704632:
class User < ActiveRecord::Base
alias :devise_valid_password? :valid_password?
def valid_password?(password)
begin
super(password)
rescue BCrypt::Errors::InvalidHash
return false unless Digest::SHA1.hexdigest(password) == encrypted_password
logger.info "User #{email} is using the old password hashing method, updating attribute."
self.password = password
true
end
end
end
Using the bcrypt encryptor in Devise, this is what I ended up doing with my legacy data:
In models/user.rb
# Because we have some old legacy users in the database, we need to override Devises method for checking if a password is valid.
# We first ask Devise if the password is valid, and if it throws an InvalidHash exception, we know that we're dealing with a
# legacy user, so we check the password against the SHA1 algorithm that was used to hash the password in the old database.
alias :devise_valid_password? :valid_password?
def valid_password?(password)
begin
devise_valid_password?(password)
rescue BCrypt::Errors::InvalidHash
Digest::SHA1.hexdigest(password) == encrypted_password
end
end
As you can see, devise throws an InvalidHash exception when it encounters an invalid hash, which it would do when authenticating a legacy user.
I use this to fall back to the hashing-algorithm used to create the original legacy hash.
It doesn't change the password though, but that could simply be added to the method if needed.
First you need to copy password_salt and encrypted_password to your new object model
Using this because I have to export my database User to another application and old,
app are using devise 1.0.x and new app using 2.1.x
Class User < ActiveRecord::Base
alias :devise_valid_password? :valid_password?
def valid_password?(password)
begin
devise_valid_password?(password)
rescue BCrypt::Errors::InvalidHash
salt = password_salt
digest = nil
10.times { digest = ::Digest::SHA1.hexdigest('--' << [salt, digest, password, nil].flatten.join('--') << '--') }
digest
return false unless digest == encrypted_password
logger.info "User #{email} is using the old password hashing method, updating attribute."
self.password = password
self.password_salt = nil # With this you will knew what object already using the new authentication by devise
self.save
true
end
end
end
If you're moving from SHA512, the solution is a bit more involved than moeffju's SHA1 solution:
def valid_password?(password)
if has_legacy_password?
return false unless valid_legacy_password?(password)
convert_legacy_password!(password)
true
else
super(password)
end
end
protected
def has_legacy_password?
password_salt.present?
end
def convert_legacy_password!(password)
self.password = password
self.password_salt = nil
self.save
end
def valid_legacy_password?(password)
stretches = 10
salt = password_salt
pepper = nil
digest = pepper
stretches.times do
tokens = [salt, digest, password, pepper]
digest = Digest::SHA512.hexdigest('--' << tokens.flatten.join('--') << '--')
end
Devise.secure_compare(encrypted_password, digest)
end
Be sure to replace stretches and pepper with the values you used to encrypt the passwords.
follow Thomas Dippel instructions i have made a gist that update password:
https://gist.github.com/1578362
# Because we have some old legacy users in the database, we need to override Devises method for checking if a password is valid.
# We first ask Devise if the password is valid, and if it throws an InvalidHash exception, we know that we're dealing with a
# legacy user, so we check the password against the SHA1 algorithm that was used to hash the password in the old database.
#SOURCES OF SOLUTION:
# http://stackoverflow.com/questions/6113375/converting-existing-password-hash-to-devise
# https://github.com/binarylogic/authlogic/blob/master/lib/authlogic/crypto_providers/sha512.rb
# https://github.com/plataformatec/devise/blob/master/lib/devise/encryptors/authlogic_sha512.rb
alias :devise_valid_password? :valid_password?
def valid_password?(password)
debugger
begin
devise_valid_password?(password)
rescue BCrypt::Errors::InvalidHash
stretches = 20
digest = [password, self.password_salt].flatten.join('')
stretches.times {digest = Digest::SHA512.hexdigest(digest)}
if digest == self.encrypted_password
#Here update old Authlogic SHA512 Password with new Devise ByCrypt password
# SOURCE: https://github.com/plataformatec/devise/blob/master/lib/devise/models/database_authenticatable.rb
# Digests the password using bcrypt.
# Default strategy for Devise is BCrypt
# def password_digest(password)
# ::BCrypt::Password.create("#{password}#{self.class.pepper}", :cost => self.class.stretches).to_s
# end
self.encrypted_password = self.password_digest(password)
self.save
return true
else
# If not BCryt password and not old Authlogic SHA512 password Dosn't my user
return false
end
end
end
I'm migrating from a legacy system that uses simple MD5 unsalted passwords into Devise. While I could roll my own encryptor as recommended on the Devise wiki I actually want to migrate to the bcrypt password mechanism.
This also seems more reasonable than downloading rainbow tables and trying to discover all the plaintext passwords...
So, I'm wondering if there might be any side-effects to the following code, especially around the save! triggering any callbacks that have unintended behavior:
## config/initializers/legacy.rb
require 'bcrypt'
require 'digest/md5'
module Devise
module Models
module DatabaseAuthenticatable
def valid_password?(password)
if self.legacy_password_hash
if ::Digest::MD5.hexdigest(password) == self.legacy_password_hash
## authenticated; now convert to bcrypt password
self.password = password
self.legacy_password_hash = nil
self.save!
return true
else
## so that we don't get a bcrypt invalid hash exception
return false
end
else
return ::BCrypt::Password.new(self.encrypted_password) == "#{password}#{self.class.pepper}"
end
end
end
end
end
Shamelessly stolen from:
http://groups.google.com/group/plataformatec-devise/browse_thread/thread/9dcf87b2225bd11f?pli=1
In short, do not override Devise's default authentication. Just put this method into your authentication model (usually User).