I have a rails 3 application that I am working on and have implemented devise. I have it working, and now I wish to extend it so that a user is unable to use a old password more than once. Found this functionality on github which to my suprise was good. Disallow previously passwords – Git Hub
I thought this would straight forward but it is clearly not. My code looks like the following:
create_passwrod_histories.rb
class CreatePasswordHistories < ActiveRecord::Migration
def self.up
create_table(:password_histories) do |t|
t.integer :user_id
t.string :encrypted_password
t.timestamps
end
end
def self.down
drop_table :password_histories
end
end
User.rb
class User < ActiveRecord::Base
include ActiveModel::Validations
has_many :roles_users
has_many :roles, :through => :roles_users
has_many :projects
has_many :password_histories
after_save :store_digest
# authorization include this in whichever model that will use ACL9
acts_as_authorization_subject
def has_role?(role_name, object=nil)
!! if object.nil?
self.roles.find_by_name(role_name.to_s) ||
self.roles.member?(get_role(role_name, nil))
else
method = "is_#{role_name.to_s}?".to_sym
object.respond_to?(method) && object.send(method, self)
end
end
def login(user)
post_via_redirect user_session_path, 'user[username]' => user.username, 'user[password]' => user.password
end
# Include default devise modules. Others available are:
# :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable #:registerable,
devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable, :timeoutable
acts_as_authorization_subject :association_name => :roles
attr_accessor :login
# Setup accessible (or protected) attributes for your model
attr_accessible :id, :login, :username, :full_name, :email, :password, :password_confirmation, :remember_me, :role_ids
email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates_presence_of :username, :full_name
validates_format_of :username, :with => /^[-\w\._@]+$/i, :allow_blank => true, :message => "should only contain letters, numbers, or . - _ @"
validates_length_of :username, :minimum => 1, :allow_blank => true
validates_uniqueness_of :username, :email
validates :email, :presence => true,
:format => { :with => email_regex }
validates :password, :unique_password => true
def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
login = conditions.delete(:login)
where(conditions).where(["lower(username) = :value OR lower(email) = :value", { :value => login.downcase }]).first
end
private
def store_digest
if encrypted_password_changed?
PasswordHistory.create(:user => self, :encrypted_password => encrypted_password)
end
end
end
unique_password_validator.rb
require 'bcrypt'
class UniquePasswordValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.password_histories.each do |ph|
bcrypt = ::BCrypt::Password.new(ph.encrypted_password)
hashed_value = ::BCrypt::Engine.hash_secret([value, Devise.pepper].join, bcrypt.salt)
record.errors[attribute] << "has been used previously." and return if hashed_value == ph.encrypted_password
end
end
end
I then run my app and try to use the same password. It then throws up the follwoing error uninitialized constant User::PasswordHistory
The only way that I can see from your code why that would be happening is if you didn’t have the
PasswordHistorymodel object. That code from Github doesn’t actually explicitly tell you to do it, but you certainly need it. So, maybe you created and ran the migration but forgot to create the model, as in: