module Authlogic
module ActiveRecord # :nodoc:
# = Acts As Authentic
# Provides the acts_as_authentic method to include in your models to help with authentication. See method below.
module ActsAsAuthentic
# Call this method in your model to add in basic authentication madness that your authlogic session expects.
#
# === Methods
# For example purposes lets assume you have a User model.
#
# Class method name Description
# User.crypto_provider The class that you set in your :crypto_provider option
# User.forget_all! Finds all records, loops through them, and calls forget! on each record. This is paginated to save on memory.
# User.unique_token returns unique token generated by your :crypto_provider
#
# Named Scopes
# User.logged_in Find all users who are logged in, based on your :logged_in_timeout option.
# User.logged_out Same as above, but logged out.
#
# Isntace method name
# user.password= Method name based on the :password_field option. This is used to set the password. Pass the *raw* password to this.
# user.confirm_password= Confirms the password, needed to change the password.
# user.valid_password?(pass) Determines if the password passed is valid. The password could be encrypted or raw.
# user.reset_password! Basically resets the password to a random password using only letters and numbers.
# user.logged_in? Based on the :logged_in_timeout option. Tells you if the user is logged in or not.
# user.forget! Changes their remember token, making their cookie and session invalid. A way to log the user out withouth changing their password.
#
# === Options
#
# * session_class: default: "#{name}Session",
# This is the related session class. A lot of the configuration will be based off of the configuration values of this class.
#
# * crypto_provider: default: Authlogic::Sha512CryptoProvider,
# This is the class that provides your encryption. By default Authlogic provides its own crypto provider that uses Sha512 encrypton.
#
# * login_field: default: options[:session_class].login_field,
# The name of the field used for logging in, this is guess based on what columns are in your db. Only specify if you aren't using:
# login, username, or email
#
# * login_field_type: default: options[:login_field] == :email ? :email : :login,
# Tells authlogic how to validation the field, what regex to use, etc. If the field name is email it will automatically use email,
# otherwise it uses login.
#
# * login_field_regex: default: if email then typical email regex, otherwise typical login regex.
# This is used in validates_format_of for the login_field.
#
# * login_field_regex_message: the message to use when the validates_format_of for the login field fails.
#
# * password_field: default: options[:session_class].password_field,
# This is the name of the field to set the password, *NOT* the field the encrypted password is stored.
#
# * crypted_password_field: default: depends on which columns are present,
# The name of the database field where your encrypted password is stored. If the name of the field is different from any of the following
# you need to specify it with this option: crypted_password, encrypted_password, password_hash, pw_hash
#
# * password_salt_field: default: depends on which columns are present,
# This is the name of the field in your database that stores your password salt. If the name of the field is different from any of the
# following then you need to specify it with this option: password_salt, pw_salt, salt
#
# * remember_token_field: default: options[:session_class].remember_token_field,
# This is the name of the field your remember_token is stored. The remember token is a unique token that is stored in the users cookie and
# session. This way you have complete control of when session expire and you don't have to change passwords to expire sessions. This also
# ensures that stale sessions can not be persisted. By stale, I mean sessions that are logged in using an outdated password. If the name
# of the field is anything other than the following you need to specify it with this option: remember_token, remember_key, cookie_token,
# cookie_key
#
# * scope: default: nil,
# This scopes validations. If all of your users belong to an account you might want to scope everything to the account. Just pass :account_id
#
# * logged_in_timeout: default: 10.minutes,
# This is really just a nifty feature to tell if a user is logged in or not. It's based on activity. So if the user in inactive longer than
# the value you pass here they are assumed "logged out".
#
# * session_ids: default: [nil],
# The sessions that we want to automatically reset when a user is created or updated so you don't have to worry about this. Set to [] to disable.
# Should be an array of ids. See the Authlogic::Session documentation for information on ids. The order is important.
# The first id should be your main session, the session they need to log into first. This is generally nil. When you don't specify an id
# in your session you are really just inexplicitly saying you want to use the id of nil.
def acts_as_authentic(options = {})
# If we don't have a database, skip all of this, solves initial setup errors
begin
column_names
rescue Exception
return
end
# Setup default options
begin
options[:session_class] ||= "#{name}Session".constantize
rescue NameError
raise NameError.new("You must create a #{name}Session class before a model can act_as_authentic. If that is not the name of the class pass the class constant via the :session_class option.")
end
options[:crypto_provider] ||= Sha512CryptoProvider
options[:crypto_provider_type] ||= options[:crypto_provider].respond_to?(:decrypt) ? :encryption : :hash
options[:login_field] ||= options[:session_class].login_field
options[:login_field_type] ||= options[:login_field] == :email ? :email : :login
options[:password_field] ||= options[:session_class].password_field
options[:crypted_password_field] ||=
(column_names.include?("crypted_password") && :crypted_password) ||
(column_names.include?("encrypted_password") && :encrypted_password) ||
(column_names.include?("password_hash") && :password_hash) ||
(column_names.include?("pw_hash") && :pw_hash) ||
:crypted_password
options[:password_salt_field] ||=
(column_names.include?("password_salt") && :password_salt) ||
(column_names.include?("pw_salt") && :pw_salt) ||
(column_names.include?("salt") && :salt) ||
:password_salt
options[:remember_token_field] ||= options[:session_class].remember_token_field
options[:logged_in_timeout] ||= 10.minutes
options[:session_ids] ||= [nil]
# Validations
case options[:login_field_type]
when :email
validates_length_of options[:login_field], :within => 6..100
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)'
options[:login_field_regex] ||= /\A#{email_name_regex}@#{domain_head_regex}#{domain_tld_regex}\z/i
options[:login_field_regex_message] ||= "should look like an email address."
validates_format_of options[:login_field], :with => options[:login_field_regex], :message => options[:login_field_regex_message]
else
validates_length_of options[:login_field], :within => 2..100
options[:login_field_regex] ||= /\A\w[\w\.\-_@ ]+\z/
options[:login_field_regex_message] ||= "use only letters, numbers, spaces, and .-_@ please."
validates_format_of options[:login_field], :with => options[:login_field_regex], :message => options[:login_field_regex_message]
end
validates_uniqueness_of options[:login_field], :scope => options[:scope]
validates_uniqueness_of options[:remember_token_field]
validate :validate_password
validates_numericality_of :login_count, :only_integer => :true, :greater_than_or_equal_to => 0, :allow_nil => true if column_names.include?("login_count")
if column_names.include?("last_request_at")
named_scope :logged_in, lambda { {:conditions => ["last_request_at > ?", options[:logged_in_timeout].ago]} }
named_scope :logged_out, lambda { {:conditions => ["last_request_at is NULL or last_request_at <= ?", options[:logged_in_timeout].ago]} }
end
before_save :get_session_information, :if => :update_sessions?
after_save :maintain_sessions!, :if => :update_sessions?
# Attributes
attr_writer "confirm_#{options[:password_field]}"
attr_accessor "tried_to_set_#{options[:password_field]}"
# Class methods
class_eval <<-"end_eval", __FILE__, __LINE__
def self.unique_token
# Force using the Sha512 because all that we are doing is creating a unique token, a hash is perfect for this
Authlogic::Sha512CryptoProvider.encrypt(Time.now.to_s + (1..10).collect{ rand.to_s }.join)
end
def self.crypto_provider
#{options[:crypto_provider]}
end
def self.forget_all!
# Paginate these to save on memory
records = nil
i = 0
begin
records = find(:all, :limit => 50, :offset => i)
records.each { |record| record.forget! }
i += 50
end while !records.blank?
end
end_eval
# Instance methods
if column_names.include?("last_request_at")
class_eval <<-"end_eval", __FILE__, __LINE__
def logged_in?
!last_request_at.nil? && last_request_at > #{options[:logged_in_timeout].to_i}.seconds.ago
end
end_eval
end
class_eval <<-"end_eval", __FILE__, __LINE__
def #{options[:password_field]}=(pass)
return if pass.blank?
self.tried_to_set_#{options[:password_field]} = true
@#{options[:password_field]} = pass
self.#{options[:remember_token_field]} = self.class.unique_token
self.#{options[:password_salt_field]} = self.class.unique_token
self.#{options[:crypted_password_field]} = crypto_provider.encrypt(@#{options[:password_field]} + #{options[:password_salt_field]})
end
def valid_#{options[:password_field]}?(attempted_password)
return false if attempted_password.blank? || #{options[:crypted_password_field]}.blank? || #{options[:password_salt_field]}.blank?
attempted_password == #{options[:crypted_password_field]} ||
(crypto_provider.respond_to?(:decrypt) && crypto_provider.decrypt(#{options[:crypted_password_field]}) == attempted_password + #{options[:password_salt_field]}) ||
(!crypto_provider.respond_to?(:decrypt) && crypto_provider.encrypt(attempted_password + #{options[:password_salt_field]}) == #{options[:crypted_password_field]})
end
end_eval
class_eval <<-"end_eval", __FILE__, __LINE__
def #{options[:password_field]}; end
def confirm_#{options[:password_field]}; end
def crypto_provider
self.class.crypto_provider
end
def forget!
self.#{options[:remember_token_field]} = self.class.unique_token
save_without_session_maintenance(false)
end
def reset_#{options[:password_field]}!
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
newpass = ""
1.upto(10) { |i| newpass << chars[rand(chars.size-1)] }
self.#{options[:password_field]} = newpass
self.confirm_#{options[:password_field]} = newpass
save_without_session_maintenance(false)
end
alias_method :randomize_password!, :reset_password!
def save_without_session_maintenance(*args)
@skip_session_maintenance = true
result = save(*args)
@skip_session_maintenance = false
result
end
protected
def update_sessions?
!@skip_session_maintenance && #{options[:session_class]}.activated? && !#{options[:session_ids].inspect}.blank? && #{options[:remember_token_field]}_changed?
end
def get_session_information
# Need to determine if we are completely logged out, or logged in as another user
@_sessions = []
@_logged_out = true
#{options[:session_ids].inspect}.each do |session_id|
session = #{options[:session_class]}.find(*[session_id].compact)
if session
if !session.record.blank?
@_logged_out = false
@_sessions << session if session.record == self
end
end
end
end
def maintain_sessions!
if @_logged_out
create_session!
elsif !@_sessions.blank?
update_sessions!
end
end
def create_session!
# We only want to automatically login into the first session, since this is the main session. The other sessions are sessions
# that need to be created after logging into the main session.
session_id = #{options[:session_ids].inspect}.first
# If we are already logged in, ignore this completely. All that we care about is updating ourself.
next if #{options[:session_class]}.find(*[session_id].compact)
# Log me in
args = [self, session_id].compact
#{options[:session_class]}.create(*args)
end
def update_sessions!
# We found sessions above, let's update them with the new info
@_sessions.each do |stale_session|
stale_session.unauthorized_record = self
stale_session.save
end
end
def tried_to_set_password?
tried_to_set_password == true
end
def validate_password
if new_record? || tried_to_set_#{options[:password_field]}?
if @#{options[:password_field]}.blank?
errors.add(:#{options[:password_field]}, "can not be blank")
else
errors.add(:confirm_#{options[:password_field]}, "did not match") if @confirm_#{options[:password_field]} != @#{options[:password_field]}
end
end
end
end_eval
end
end
end
end
ActiveRecord::Base.extend Authlogic::ActiveRecord::ActsAsAuthentic