require 'bcrypt' module ModelMixin def self.included(base) base.send :extend, ClassMethods end module ClassMethods # Adds methods to set and authenticate against a password stored encrypted by BCrypt. # Also adds methods to generate and clear a token, used to retrieve the record of a # user who has forgotten their password. # # Note that if a password isn't supplied when creating a user, the error will be on # the `password_digest` attribute. def authenticates send :include, InstanceMethodsOnActivation attr_reader :password attr_protected :password_digest validates :username, :presence => true, :uniqueness => true, :if => :should_authenticate? validates :password_digest, :presence => true, :if => :should_authenticate? scope :valid_token, lambda { |token| where("token = ? AND token_created_at > ?", token, 3.hours.ago) } instance_eval <<-END, __FILE__, __LINE__ + 1 # Returns the user with the given username if the given password is # correct, and nil otherwise. def authenticate(username, plain_text_password) user = where(:username => username).first if user && user.has_matching_password?(plain_text_password) user else nil end end def find_by_salt(id, salt) # :nodoc: user = find_by_id id if user && user.has_matching_salt?(salt) user else nil end end END end end module InstanceMethodsOnActivation # Override this in your model if you need to bypass the Quo Vadis validations. def should_authenticate? true end def password=(plain_text_password) # :nodoc: @password = plain_text_password unless @password.blank? self.password_digest = BCrypt::Password.create plain_text_password end end # Generates a unique, timestamped token which can be used in URLs, and # saves the record. This is part of the forgotten-password workflow. def generate_token # :nodoc: begin self.token = url_friendly_token end while self.class.exists?(:token => token) self.token_created_at = Time.now.utc save end # Clears the user's timestamped token and saves the record. # This is part of the forgotten-password workflow. def clear_token # :nodoc: update_attributes :token => nil, :token_created_at => nil end # Returns true if the given plain_text_password is the user's # password, and false otherwise. def has_matching_password?(plain_text_password) # :nodoc: BCrypt::Password.new(password_digest) == plain_text_password end # Returns true if the given salt is the user's salt, # and false otherwise. def has_matching_salt?(salt) # :nodoc: password_salt == salt end def password_salt # :nodoc: BCrypt::Password.new(password_digest).salt end private def url_friendly_token # :nodoc: SecureRandom.base64(10).tr('+/=', 'xyz') end end end