39

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!

jmccartie
  • 4,956
  • 8
  • 50
  • 71
  • You should probably be able to extract the encryption method source from the source of Devise at github. Then you can write your custom generator with rake or something to run it against the old passwords – Ben Nov 04 '11 at 21:20
  • Yup. That's what I'm looking for right now. Hoping someone has already figured this out... – jmccartie Nov 04 '11 at 21:21
  • Devise uses a salt I think for encryption where your BCrypt call doesnt. – Ben Nov 04 '11 at 21:22

5 Answers5

72

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.

Robert Kajic
  • 8,689
  • 4
  • 44
  • 43
  • OMG thank you. Don't know anything to Ruby, had to change a password in DB, and was able to execute this in the console. – Mat Mar 20 '18 at 15:41
  • This is genius, exactly how you avoid to use Bcrypt yourself (and suffer from changes within it in the future) – Ricardo Jacas Jan 12 '22 at 14:18
  • 1
    I found I had to use: ` User.find_by_id([SOME_ID]).valid_password?(entered_password) ` – David Gleba Jul 30 '22 at 14:23
19

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.

jmccartie
  • 4,956
  • 8
  • 50
  • 71
  • 17
    " It's terribly slow. " That's by design! A password encryption has to be slow, so that it is harder to crack by brute force – Klaus Aug 29 '12 at 16:35
  • 5
    The `User.new(password: password).encrypted_password` method is much preferable since it abstracts away the password encryption from your code, making it easier to understand and immune to changes in how devise generates passwords. – Robert Kajic Jul 02 '13 at 10:21
  • This doesn't seem to give the same answer as [this](https://stackoverflow.com/a/17423050/5783745) one. I tried changing the cost to several values 10 -13 but cannot seem to get it to match? – stevec Aug 29 '20 at 18:09
7

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

  • This answer worked for me. The methd User.new(password: input_password).encrypted_password generated different password. – Shuaib Zahda Oct 27 '22 at 07:24
3

An alternative method is: User.new.send(:password_digest, 'xxx')

Paul Odeon
  • 4,407
  • 1
  • 37
  • 37
1

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

jacobsimeon
  • 2,012
  • 1
  • 18
  • 20
  • Thanks, Jacob. But this is directly using the User model --> I've got a few hundred thousand rows, so I'm trying to go straight to the DB without using the User model. – jmccartie Nov 04 '11 at 23:15