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).
Related
I'm trying to implement my first authlogic-based app in ruby on rails. I need to sign user using a custom oauth2 flow, at the end of the process, I want to tell authlogic which user I want him to authenticate. Without providing a login nor password, just directly by passing a user object.
I've found in the README that I can do that using UserSession.create(user, true) but putting this at the end of my OAuth callback step is not persisting my user. There is no error nor exception, everything seems to be fine but UserSession.find returns nil (even in the next line after the UserSession.create). I'm on rails 5.1.6 and authlogic 4.0.1.
User model
class User < ApplicationRecord
acts_as_authentic
end
UserSession model
class UserSession < Authlogic::Session::Base
end
Thanks in advance for any clues about what I did wrong.
This is what I found out, from Authlogic::Session::Base
module Authlogic
module Session # :nodoc:
# This is the most important class in Authlogic. You will inherit this class
# for your own eg. `UserSession`.
#
# Code is organized topically. Each topic is represented by a module. So, to
# learn about password-based authentication, read the `Password` module.
#
# It is common for methods (.initialize and #credentials=, for example) to
# be implemented in multiple mixins. Those methods will call `super`, so the
# order of `include`s here is important.
#
# Also, to fully understand such a method (like #credentials=) you will need
# to mentally combine all of its definitions. This is perhaps the primary
# disadvantage of topical organization using modules.
as I read from their instructions, they do never say that you can authenticate user without email and password, but that you can create a session with the User object
# skip authentication and log the user in directly, the true means "remember me"
UserSession.create(my_user_object, true)
I would read more about the Session.rb
https://github.com/binarylogic/authlogic/blob/master/lib/authlogic/session/session.rb
sorry if I can not give you more help
PS you can also debug this gem by setting a binding.pry in the gem files
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 see plenty that lead to custom authorization strategies for devise and warden, but what I'm sepcifically after is testing these solutions with rspec. Similar to this question: Filtering users who are able to sign in with Devise
What can I do to test this sort of implementation. Rails 3.1.1, Devise (most current), etc.
For those that may do this in the future, here is my solution:
This is the class that sets a new strategy for authentication through Devise (and it could also be used with Warden with a few small changes).
require 'devise/strategies/authenticatable'
module Devise
module Strategies
class AndroidAuthenticatable < Authenticatable
def valid?
# return true/false
return valid_params? && valid_headers?
end
def authenticate!
failure_message = "Authentication failed for device/user"
klass = mapping.to # if we're going to come here, we can mock this
begin
# code to determine whether or not to authenticate this request
# if yes, success!(instance of klass)
# if no, fail!(messsage)
rescue
fail!(failure_message) # always fail if not success
end
end
protected
def valid_params?
# params that show whether request should be here
end
def valid_headers?
# headers that determine if request should be here
end
end
end
end
The previous class is in my lib/.../strategies directory. I also have lib configured for auto-loading through the rails configuration.
From the rspec side, after I created the above class I write out a few stubs/mocks. Here is a basic rspec file to get you started.
# I do this in before block or right before test executes
#request = mock(:request)
#strategy = Devise::Strategies::AndroidAuthenticatable.new(nil)
#request.should_receive(:headers).and_return({#hash of the headers you are testing})
#strategy.should_receive(:params).at_least(:once).and_return({#hash of the params})
#strategy.should_receive(:request).and_return(#request)
# Break these up as needed to test failing and successful
# strategies for your application
lambda {
#strategy.should be_valid
#strategy.authenticate!.should eql :success
}.should_not raise_error
This isn't all inclusive, but I feel it should get us a good head start when adding strategies with Warden or Devise. I actually had to implement what I thought would work and then right tests to prove it after the fact. Now we can do it the other way around perhaps.
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 am trying to convert my custom simple auth system in my rails app to use AuthLogic. I have managed to get everything working fairly easily, but now when I try to login it will not properly validate my credentials. The pertinent code is below:
# app/models/profile.rb
class Profile < ActiveRecord::Base
acts_as_authentic do |c|
c.transition_from_crypto_providers = Authlogic::CryptoProviders::Sha1,
c.crypto_provider = Authlogic::CryptoProviders::Sha512
end
end
I used to use this to hash my password on creation:
# app/models/profile.rb
def hash_password
self.salt = ActiveSupport::SecureRandom.base64(8)
self.hashed_password = Digest:SHA1.hexdigest(self.salt + #password)
end
I have already converted all the necessary table columns to be compatible with AuthLogic.
Is there a way to log what AuthLogic is hashing the password as? What else could be wrong?
I solved my problem, I had to write a custom crypto_provider, looked like this for anyone who is curious:
class MyCryptoProvider
# Turns your raw password into a Sha1 hash.
def self.encrypt(*tokens)
tokens = tokens.flatten
tokens = tokens.reverse
digest = Digest::SHA1.hexdigest([*tokens].join)
digest
end
# Does the crypted password match the tokens? Uses the same tokens that were used to encrypt.
def self.matches?(crypted, *tokens)
encrypt(*tokens) == crypted
end
end