241 lines
10 KiB
Ruby
241 lines
10 KiB
Ruby
module Authlogic
|
|
module Session
|
|
# Handles authenticating via a traditional username and password.
|
|
module Password
|
|
def self.included(klass)
|
|
klass.class_eval do
|
|
extend Config
|
|
include InstanceMethods
|
|
validate :validate_by_password, :if => :authenticating_with_password?
|
|
|
|
class << self
|
|
attr_accessor :configured_password_methods
|
|
end
|
|
end
|
|
end
|
|
|
|
# Password configuration
|
|
module Config
|
|
# Authlogic tries to validate the credentials passed to it. One part of validation is actually finding the user and
|
|
# making sure it exists. What method it uses the do this is up to you.
|
|
#
|
|
# Let's say you have a UserSession that is authenticating a User. By default UserSession will call User.find_by_login(login).
|
|
# You can change what method UserSession calls by specifying it here. Then in your User model you can make that method do
|
|
# anything you want, giving you complete control of how users are found by the UserSession.
|
|
#
|
|
# Let's take an example: You want to allow users to login by username or email. Set this to the name of the class method
|
|
# that does this in the User model. Let's call it "find_by_username_or_email"
|
|
#
|
|
# class User < ActiveRecord::Base
|
|
# def self.find_by_username_or_email(login)
|
|
# find_by_username(login) || find_by_email(login)
|
|
# end
|
|
# end
|
|
#
|
|
# Now just specify the name of this method for this configuration option and you are all set. You can do anything you
|
|
# want here. Maybe you allow users to have multiple logins and you want to search a has_many relationship, etc. The sky is the limit.
|
|
#
|
|
# * <tt>Default:</tt> "find_by_smart_case_login_field"
|
|
# * <tt>Accepts:</tt> Symbol or String
|
|
def find_by_login_method(value = nil)
|
|
rw_config(:find_by_login_method, value, "find_by_smart_case_login_field")
|
|
end
|
|
alias_method :find_by_login_method=, :find_by_login_method
|
|
|
|
# The text used to identify credentials (username/password) combination when a bad login attempt occurs.
|
|
# When you show error messages for a bad login, it's considered good security practice to hide which field
|
|
# the user has entered incorrectly (the login field or the password field). For a full explanation, see
|
|
# http://www.gnucitizen.org/blog/username-enumeration-vulnerabilities/
|
|
#
|
|
# Example of use:
|
|
#
|
|
# class UserSession < Authlogic::Session::Base
|
|
# generalize_credentials_error_messages true
|
|
# end
|
|
#
|
|
# This would make the error message for bad logins and bad passwords look identical:
|
|
#
|
|
# Login/Password combination is not valid
|
|
#
|
|
# Alternatively you may use a custom message:
|
|
#
|
|
# class UserSession < AuthLogic::Session::Base
|
|
# generalize_credentials_error_messages "Your login information is invalid"
|
|
# end
|
|
#
|
|
# This will instead show your custom error message when the UserSession is invalid.
|
|
#
|
|
# The downside to enabling this is that is can be too vague for a user that has a hard time remembering
|
|
# their username and password combinations. It also disables the ability to to highlight the field
|
|
# with the error when you use form_for.
|
|
#
|
|
# If you are developing an app where security is an extreme priority (such as a financial application),
|
|
# then you should enable this. Otherwise, leaving this off is fine.
|
|
#
|
|
# * <tt>Default</tt> false
|
|
# * <tt>Accepts:</tt> Boolean
|
|
def generalize_credentials_error_messages(value = nil)
|
|
rw_config(:generalize_credentials_error_messages, value, false)
|
|
end
|
|
alias_method :generalize_credentials_error_messages=, :generalize_credentials_error_messages
|
|
|
|
# The name of the method you want Authlogic to create for storing the login / username. Keep in mind this is just for your
|
|
# Authlogic::Session, if you want it can be something completely different than the field in your model. So if you wanted people to
|
|
# login with a field called "login" and then find users by email this is compeltely doable. See the find_by_login_method configuration
|
|
# option for more details.
|
|
#
|
|
# * <tt>Default:</tt> klass.login_field || klass.email_field
|
|
# * <tt>Accepts:</tt> Symbol or String
|
|
def login_field(value = nil)
|
|
rw_config(:login_field, value, klass.login_field || klass.email_field)
|
|
end
|
|
alias_method :login_field=, :login_field
|
|
|
|
# Works exactly like login_field, but for the password instead. Returns :password if a login_field exists.
|
|
#
|
|
# * <tt>Default:</tt> :password
|
|
# * <tt>Accepts:</tt> Symbol or String
|
|
def password_field(value = nil)
|
|
rw_config(:password_field, value, login_field && :password)
|
|
end
|
|
alias_method :password_field=, :password_field
|
|
|
|
# The name of the method in your model used to verify the password. This should be an instance method. It should also
|
|
# be prepared to accept a raw password and a crytped password.
|
|
#
|
|
# * <tt>Default:</tt> "valid_password?"
|
|
# * <tt>Accepts:</tt> Symbol or String
|
|
def verify_password_method(value = nil)
|
|
rw_config(:verify_password_method, value, "valid_password?")
|
|
end
|
|
alias_method :verify_password_method=, :verify_password_method
|
|
end
|
|
|
|
# Password related instance methods
|
|
module InstanceMethods
|
|
def initialize(*args)
|
|
if !self.class.configured_password_methods
|
|
if login_field
|
|
self.class.send(:attr_writer, login_field) if !respond_to?("#{login_field}=")
|
|
self.class.send(:attr_reader, login_field) if !respond_to?(login_field)
|
|
end
|
|
|
|
if password_field
|
|
self.class.send(:attr_writer, password_field) if !respond_to?("#{password_field}=")
|
|
self.class.send(:define_method, password_field) {} if !respond_to?(password_field)
|
|
|
|
self.class.class_eval <<-"end_eval", __FILE__, __LINE__
|
|
private
|
|
# The password should not be accessible publicly. This way forms using form_for don't fill the password with the
|
|
# attempted password. To prevent this we just create this method that is private.
|
|
def protected_#{password_field}
|
|
@#{password_field}
|
|
end
|
|
end_eval
|
|
end
|
|
|
|
self.class.configured_password_methods = true
|
|
end
|
|
|
|
super
|
|
end
|
|
|
|
# Returns the login_field / password_field credentials combination in hash form.
|
|
def credentials
|
|
if authenticating_with_password?
|
|
details = {}
|
|
details[login_field.to_sym] = send(login_field)
|
|
details[password_field.to_sym] = "<protected>"
|
|
details
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
# Accepts the login_field / password_field credentials combination in hash form.
|
|
def credentials=(value)
|
|
super
|
|
values = value.is_a?(Array) ? value : [value]
|
|
if values.first.is_a?(Hash)
|
|
values.first.with_indifferent_access.slice(login_field, password_field).each do |field, value|
|
|
next if value.blank?
|
|
send("#{field}=", value)
|
|
end
|
|
end
|
|
end
|
|
|
|
def invalid_password?
|
|
invalid_password == true
|
|
end
|
|
|
|
private
|
|
def authenticating_with_password?
|
|
login_field && (!send(login_field).nil? || !send("protected_#{password_field}").nil?)
|
|
end
|
|
|
|
def validate_by_password
|
|
self.invalid_password = false
|
|
|
|
errors.add(login_field, I18n.t('error_messages.login_blank', :default => "cannot be blank")) if send(login_field).blank?
|
|
errors.add(password_field, I18n.t('error_messages.password_blank', :default => "cannot be blank")) if send("protected_#{password_field}").blank?
|
|
return if errors.count > 0
|
|
|
|
self.attempted_record = search_for_record(find_by_login_method, send(login_field))
|
|
if attempted_record.blank?
|
|
generalize_credentials_error_messages? ?
|
|
add_general_credentials_error :
|
|
errors.add(login_field, I18n.t('error_messages.login_not_found', :default => "is not valid"))
|
|
return
|
|
end
|
|
|
|
if !attempted_record.send(verify_password_method, send("protected_#{password_field}"))
|
|
self.invalid_password = true
|
|
generalize_credentials_error_messages? ?
|
|
add_general_credentials_error :
|
|
errors.add(password_field, I18n.t('error_messages.password_invalid', :default => "is not valid"))
|
|
return
|
|
end
|
|
end
|
|
|
|
def invalid_password
|
|
@invalid_password
|
|
end
|
|
|
|
def invalid_password=(value)
|
|
@invalid_password = value
|
|
end
|
|
|
|
def find_by_login_method
|
|
self.class.find_by_login_method
|
|
end
|
|
|
|
def login_field
|
|
self.class.login_field
|
|
end
|
|
|
|
def add_general_credentials_error
|
|
error_message =
|
|
if self.class.generalize_credentials_error_messages.is_a? String
|
|
self.class.generalize_credentials_error_messages
|
|
else
|
|
"#{login_field.to_s.humanize}/Password combination is not valid"
|
|
end
|
|
errors.add(:base, I18n.t('error_messages.general_credentials_error', :default => error_message))
|
|
end
|
|
|
|
def generalize_credentials_error_messages?
|
|
self.class.generalize_credentials_error_messages
|
|
end
|
|
|
|
def password_field
|
|
self.class.password_field
|
|
end
|
|
|
|
def verify_password_method
|
|
self.class.verify_password_method
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|