module Authlogic
module ORMAdapters
module ActiveRecordAdapter
module ActsAsAuthentic
# = Credentials
#
# Handles any credential specific code, such as validating the login, encrpyting the password, etc.
#
# === Class Methods
#
# * friendly_unique_token - returns a random string of 20 alphanumeric characters. Used when resetting the password. This is a more user friendly token then a long Sha512 hash.
#
# === Instance Methods
#
# * {options[:password_field]}=(value) - encrypts a raw password and sets it to your crypted_password_field. Also sets the password_salt to a random token.
# * valid_{options[:password_field]}?(password_to_check) - checks is the password is valid. The password passed must be the raw password, not encrypted.
# * reset_{options[:password_field]} - resets the password using the friendly_unique_token class method
# * reset_{options[:password_field]}! - calls reset_password and then saves the record
module Credentials
def acts_as_authentic_with_credentials(options = {})
acts_as_authentic_without_credentials(options)
if options[:validate_fields]
email_name_regex = '[\w\.%\+\-]+'
domain_head_regex = '(?:[A-Z0-9\-]+\.)+'
domain_tld_regex = '(?:[A-Z]{2}|com|org|net|edu|gov|mil|biz|info|mobi|name|aero|jobs|museum)'
email_field_regex ||= /\A#{email_name_regex}@#{domain_head_regex}#{domain_tld_regex}\z/i
if options[:validate_login_field]
case options[:login_field_type]
when :email
validates_length_of options[:login_field], {:within => 6..100}.merge(options[:login_field_validates_length_of_options])
validates_format_of options[:login_field], {:with => email_field_regex, :message => "should look like an email address."}.merge(options[:login_field_validates_format_of_options])
else
validates_length_of options[:login_field], {:within => 2..100}.merge(options[:login_field_validates_length_of_options])
validates_format_of options[:login_field], {:with => /\A\w[\w\.\-_@ ]+\z/, :message => "should use only letters, numbers, spaces, and .-_@ please."}.merge(options[:login_field_validates_format_of_options])
end
validates_uniqueness_of options[:login_field], {:allow_blank => true}.merge(options[:login_field_validates_uniqueness_of_options].merge(:if => "#{options[:login_field]}_changed?".to_sym))
end
if options[:validate_password_field]
validates_length_of options[:password_field], {:minimum => 4}.merge(options[:password_field_validates_length_of_options].merge(:if => "validate_#{options[:password_field]}?".to_sym))
validates_confirmation_of options[:password_field], options[:password_field_validates_confirmation_of_options].merge(:if => "validate_#{options[:password_field]}?".to_sym)
validates_presence_of "#{options[:password_field]}_confirmation", :if => "validate_#{options[:password_field]}?".to_sym
end
if options[:validate_email_field] && options[:email_field]
validates_length_of options[:email_field], {:within => 6..100}.merge(options[:email_field_validates_length_of_options])
validates_format_of options[:email_field], {:with => email_field_regex, :message => "should look like an email address."}.merge(options[:email_field_validates_format_of_options])
validates_uniqueness_of options[:email_field], options[:email_field_validates_uniqueness_of_options].merge(:if => "#{options[:email_field]}_changed?".to_sym)
end
end
attr_reader options[:password_field]
class_eval <<-"end_eval", __FILE__, __LINE__
def self.friendly_unique_token
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
newpass = ""
1.upto(20) { |i| newpass << chars[rand(chars.size-1)] }
newpass
end
def #{options[:password_field]}=(pass)
return if pass.blank?
@#{options[:password_field]} = pass
self.#{options[:password_salt_field]} = self.class.unique_token
self.#{options[:crypted_password_field]} = #{options[:crypto_provider]}.encrypt(*encrypt_arguments(@#{options[:password_field]}, #{options[:act_like_restful_authentication].inspect} ? :restful_authentication : nil))
end
alias_method :update_#{options[:password_field]}, :#{options[:password_field]}= # this is to avoids the method chain, so we are ONLY changing the password
def valid_#{options[:password_field]}?(attempted_password)
return false if attempted_password.blank? || #{options[:crypted_password_field]}.blank? || #{options[:password_salt_field]}.blank?
([#{options[:crypto_provider]}] + #{options[:transition_from_crypto_provider].inspect}).each_with_index do |encryptor, index|
# The arguments_type of for the transitioning from restful_authentication
arguments_type = (#{options[:act_like_restful_authentication].inspect} && index == 0) ||
(#{options[:transition_from_restful_authentication].inspect} && index > 0 && encryptor == Authlogic::CryptoProviders::Sha1) ?
:restful_authentication : nil
if encryptor.matches?(#{options[:crypted_password_field]}, *encrypt_arguments(attempted_password, arguments_type))
# If we are transitioning from an older encryption algorithm and the password is still using the old algorithm
# then let's reset the password using the new algorithm. If the algorithm has a cost (BCrypt) and the cost has changed, update the password with
# the new cost.
if index > 0 || (encryptor.respond_to?(:cost_matches?) && !encryptor.cost_matches?(#{options[:crypted_password_field]}))
update_#{options[:password_field]}(attempted_password)
save(false)
end
return true
end
end
false
end
def reset_#{options[:password_field]}
friendly_token = self.class.friendly_unique_token
self.#{options[:password_field]} = friendly_token
self.#{options[:password_field]}_confirmation = friendly_token
end
alias_method :randomize_password, :reset_password
def confirm_#{options[:password_field]}
raise "confirm_#{options[:password_field]} has been removed, please use #{options[:password_field]}_confirmation. " +
"As this is the field that ActiveRecord automatically creates with validates_confirmation_of."
end
def reset_#{options[:password_field]}!
reset_#{options[:password_field]}
save_without_session_maintenance(false)
end
alias_method :randomize_password!, :reset_password!
def validate_#{options[:password_field]}?
new_record? || #{options[:crypted_password_field]}_changed?
end
private
def encrypt_arguments(raw_password, arguments_type = nil)
case arguments_type
when :restful_authentication
[REST_AUTH_SITE_KEY, #{options[:password_salt_field]}, raw_password, REST_AUTH_SITE_KEY]
else
[raw_password, #{options[:password_salt_field]}]
end
end
end_eval
end
end
end
end
end
end
ActiveRecord::Base.class_eval do
class << self
include Authlogic::ORMAdapters::ActiveRecordAdapter::ActsAsAuthentic::Credentials
alias_method_chain :acts_as_authentic, :credentials
end
end