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.
Related
I am a newbie in rails. I would like to ask if it's possible to insert an encrypted password(encrypted using devise default encryption) to devise model?
e.g:
User.csv
id,name,encrypted_password
1,dude,$2a$10$0.xEu5LvDbnmGVIqgIab8ej5o2b3eKa8KLevsg5bxIX0SHSOl5gye
I want to read this csv file and then insert the data to User model.
But I realized that you can't insert the already encrypted password normally on encrypted_password of User model. I'm thinking to decrypt the password but they say that it is bad for the system's security.
Update: It can now insert an encrypted password but the inserted password is different to the one on the csv since the devise automatically encrypts the password before inserting it to the database. Is there a way for devise model to detect if the password is already encrypted before inserting it to the database?
yes, you can set the encrypted_password value directly:
u = User.find(1)
u.update_attribute(:encrypted_password, "$2a$10$0.xEu5LvDbnmGVIqgIab8ej5o2b3eKa8KLevsg5bxIX0SHSOl5gye")
But you should make sure that you're not setting 'password', if you do this won't work as it'll encrypt it again, so
u = User.new
u.password = 'foo'
u.password_confirmation = 'foo'
u.encrypted_password = "$2a$10$0.xEu5LvDbnmGVIqgIab8ej5o2b3eKa8KLevsg5bxIX0SHSOl5gye" # this line will be ignored
u.save
u = User.new
u.encrypted_password = "$2a$10$0.xEu5LvDbnmGVIqgIab8ej5o2b3eKa8KLevsg5bxIX0SHSOl5gye" # this line will now work
u.save
This guide shows how to use Bcrypt with Rails, but it differs significantly in implementation from this Rails documentation on Bcrypt.
Guide version
Set password
def password=(password)
self.password_digest = BCrypt::Password.create(password)
end
Authenticate password
def is_password?(password)
BCrypt::Password.new(self.password_digest) == password
end
But the documentation does the same thing using built in methods.
Documentation version
Set password
Setting user.password or setting both user.password and user.password_confirmation values, then calling user.save will invoke some callbacks from Bcrypt which will take the password values and generate the digest and save that in the database.
user.password = 'mUc3m00RsqyRe'
user.password_confirmation = 'mUc3m00RsqyRe'
user.save
Authenticate password
The method user.authenticate(password) will return false or the instance variable of user depending on whether or not the password argument matches user.password_digest.
user.authenticate('notright')
user.authenticate('mUc3m00RsqyRe')
Questions
I had always used the documentation version, since I saw it first, but does the guide follow some better practice?
Why does the guide rewrite the wheel? That seems very un-Railsy.
Is this just a difference in versions of Bcrypt or Rails?
The correct way is to use has_secure_password (the documented method) which was available since Rails 3. Maybe the guide was based on a practice prior to Rails 3?
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
I'm trying to migrate a ton of users from an old database. To do this, I'm using activerecord-import and trying to save all my user data directly to DB (bypassing the User model).
My issue: I need to take the old user's plain-text password, encrypt it, and store directly to the DB. I know how to generate a password using Devise, but am wondering if there's a way to get a hashed password that I can store directly to the database.
Hoping to do:
new_hashed_password = Devise.awesome_encrypting_method(old_user.password)
Then store "new_hashed_password" directly into the DB without going through the model. I dug around in Devise and found this:
def password_digest(password)
::BCrypt::Password.create("#{password}#{self.class.pepper}", :cost => self.class.stretches).to_s
end
##stretches defaults to 10 (lib/devise.rb:71) and isn't overridden by my initializer
##pepper defaults to nil (lib/devise.rb:148) and isn't overridden by my initializer
I thought I could manually re-create password_digest() but I think I'm missing something fundamental about Bcrypt because even with setting password and stretches, the resulting hash is different every time.
Any ideas? Thanks for your help!
You should do it like this:
password = 'the secret password'
new_hashed_password = User.new(:password => password).encrypted_password
This is much better than using BCrypt directly as it abstracts away how passwords are generated from your code, making it easier to understand, and also immune to changes in how devise constructs encrypted passwords. Your code should not, and has no reason to know anything about that.
Good news and bad news.
Good news:
The following works to create your user's password manually.
pepper = nil
cost = 10
encrypted_password = ::BCrypt::Password.create("#{password}#{pepper}", :cost => cost).to_s
You can find your pepper and cost in your devise initializer. This method was confirmed using Devise's "valid_password?" method.
Bad news:
The entire reason I was trying to avoid "User.new(password: password).encrypted_password" was because of speed. It's terribly slow. With all my other pieces of my import task, I've intentionally avoided this.
But as it turns out, the major cost here is not instantiating a User object -- but BCrypt itself. There is very little noticeable speed boost when using BCrypt directly because it's intentionally designed to be slow.
My final answer: suck it up, run the rake script, go find a beverage.
None of the other answers above worked for me, so here is what I did:
user.valid_password?(plain_password)
https://github.com/plataformatec/devise/blob/d293e00ef5f431129108c1cbebe942b32e6ba616/lib/devise/models/database_authenticatable.rb#L44
An alternative method is: User.new.send(:password_digest, 'xxx')
Assuming you have a mysql database with a "users" table and a "password" column
And an ActiveRecord model class called "user" that is hooked up to devise
Create an ActiveRecord model class in your app
app/models/old_user.rb
OldUser < ActiveRecord::Base
set_table :users
establish_connection :database => "old_database", :user => "old user", :adapter => "mysql"
end
then create a rake task:
app/lib/tasks/migrate_users.rake
task :migrate_users => :environment do
OldUser.find_each do |old_user|
u = User.new(:email => old_user.email, :password => old_user.password, :password_confirmation => old_user.password);
#if your using confirmation
u.skip_confirmation!
u.save!
end
end
Modify as necessary (make sure you're saving any app-specific user attributes)
Then$ rake migrate_users
I am trying out how Devise works with one of my projects for user authentication. There is a user requirement that their admin should be able to generate a batch of username and user's password from time to time, and then the admin will email the new username and password to his users.
Assume the admin has the knowledge of direct SQL on the MySQL database, how can the generated usernames/passwords recognized by Devise? Thanks!
Use the Devise.friendly_token method:
password_length = 6
password = Devise.friendly_token.first(password_length)
User.create!(:email => 'someone#something.com', :password => password, :password_confirmation => password)
FYI: Devise.friendly_token returns a 20 character token. In the example above, we're chopping off the first password_length characters of the generated token by using the String#first method that Rails provides.
One option would be to use the Devise.generate_token. I.e.
password = User.generate_token('password')
User.create!(:email => 'someone#something.com', :password => password, :password_confirmation => password)
This option has not been available in Devise for quite a while. Please refer to the other answer (friendly_token).
I'm using devise-security gem and have specefic password_complexity requirements as follows:
config.password_complexity = { digit: 1, lower: 1, upper: 1 }
If you use this code: Devise.friendly_token.first(password_length) to generate the password, you are not always guaranteed to get a password that matches your complexity.
So I wrote a password generator that will respect your password_complexity and will generate a random complaint password:
class PasswordGenerator
include ActiveModel::Validations
validates :password, presence: true, 'devise_security/password_complexity': Devise.password_complexity
attr_reader :password
def initialize
#password = Devise.friendly_token.first(Devise.password_length.first) until valid?
end
end
You can use it as follows:
PasswordGenerator.new.password # "qHc165ku"
(quick caveat: I'm a rails newb)
I tried the generate_token but it doesn't do what you think (look at the docs)
(I'm using rails 3.0.5, and devise 1.1.7)
What I found is that Devise will generate all that stuff for you in the background when you do:
User.create!(:email => "me#example.com", :password => "password")
Devise should create the encrypted_password, and salt for you. (pop open a console and try it out there)