Skip to content

Commit

Permalink
* Make password and login fields optional. This allows you to have an…
Browse files Browse the repository at this point in the history
… alternate authentication method as your main authentication source. Such as OpenID, LDAP, or whatever you want.
  • Loading branch information
binarylogic committed Apr 8, 2009
1 parent 1bb82d1 commit 5c0ac4f
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 116 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rdoc
Expand Up @@ -8,6 +8,7 @@
* Cookies now store the record id as well, for faster lookup. Also to avoid the need to use sessions since sessions are lazily loaded in rails 2.3+
* Add configuration option for Authlogic::ActsAsAuthentic: ignore_blank_passwords
* Fix cookie_domain in rails adapter
* Make password and login fields optional. This allows you to have an alternate authentication method as your main authentication source. Such as OpenID, LDAP, or whatever you want.

== 2.0.5 released 2009-3-30

Expand Down
6 changes: 4 additions & 2 deletions README.rdoc
Expand Up @@ -27,7 +27,7 @@ Authlogic can do all of this and much more, keep reading to see...
**Before contacting me, please read:**
If you find a bug or a problem please post it on lighthouse. If you need help with something, please use google groups. I check both regularly and get emails when anything happens, so that is the best place to get help. Please do not email me directly with issues regarding Authlogic.

== Authlogic "add on" directory
== Authlogic "add ons"

* <b>Authlogic OpenID addon:</b> http://github.com/binarylogic/authlogic_openid

Expand Down Expand Up @@ -189,7 +189,7 @@ This will create a file that looks similar to:
The user model should have the following columns. The names of these columns can be changed with configuration. Better yet, Authlogic tries to guess these names by checking for the existence of common names. See the sub modules of Authlogic::Session for more details, but chances are you won't have to specify any configuration for your field names, even if they aren't the same names as below.

t.string :login, :null => false # optional, you can use email instead, or both
t.string :crypted_password, :null => false # required
t.string :crypted_password, :null => false # optional, see below
t.string :password_salt, :null => false # optional, but highly recommended
t.string :persistence_token, :null => false # required
t.string :single_access_token, :null => false # optional, see Authlogic::Session::Params
Expand All @@ -202,6 +202,8 @@ The user model should have the following columns. The names of these columns can
t.string :current_login_ip # optional, see Authlogic::Session::MagicColumns
t.string :last_login_ip # optional, see Authlogic::Session::MagicColumns

Notice the login and crypted_password fields are optional. If you prefer, you could use OpenID, LDAP, or whatever you want as your main authentication source and not even provide your own authentication system. I recommend providing your own as an option though. Your interface, such as the registration form, can dictate which method is the default. Lastly, adding 3rd party authentication methods should be as easy as installing an Authlogic "add on" gem. See "Authligic add ons" above.

=== 4. Set up your model

Make sure you have a model that you will be authenticating with. Since we are using the User model it should look something like:
Expand Down
4 changes: 2 additions & 2 deletions lib/authlogic/acts_as_authentic/base.rb
Expand Up @@ -73,9 +73,9 @@ def config(key, value, default_value = nil, read_value = nil)
end
end

def first_column_to_exist(*columns_to_check) # :nodoc:
def first_column_to_exist(*columns_to_check)
columns_to_check.each { |column_name| return column_name.to_sym if column_names.include?(column_name.to_s) }
columns_to_check.first ? columns_to_check.first.to_sym : nil
columns_to_check.first && columns_to_check.first.to_sym
end

end
Expand Down
32 changes: 19 additions & 13 deletions lib/authlogic/acts_as_authentic/logged_in_status.rb
Expand Up @@ -27,27 +27,33 @@ def logged_in_timeout(value = nil)
# All methods for the logged in status feature seat.
module Methods
def self.included(klass)
return if !klass.column_names.include?("last_request_at")

klass.class_eval do
include InstanceMethods

named_scope :logged_in, lambda { {:conditions => ["last_request_at > ?", logged_in_timeout.seconds.ago]} }
named_scope :logged_out, lambda { {:conditions => ["last_request_at is NULL or last_request_at <= ?", logged_in_timeout.seconds.ago]} }
end
end

# Returns true if the last_request_at > logged_in_timeout.
def logged_in?
raise "Can not determine the records login state because there is no last_request_at column" if !respond_to?(:last_request_at)
!last_request_at.nil? && last_request_at > logged_in_timeout.seconds.ago
end

# Opposite of logged_in?
def logged_out?
!logged_in?
end
module InstanceMethods
# Returns true if the last_request_at > logged_in_timeout.
def logged_in?
raise "Can not determine the records login state because there is no last_request_at column" if !respond_to?(:last_request_at)
!last_request_at.nil? && last_request_at > logged_in_timeout.seconds.ago
end

private
def logged_in_timeout
self.class.logged_in_timeout
# Opposite of logged_in?
def logged_out?
!logged_in?
end

private
def logged_in_timeout
self.class.logged_in_timeout
end
end
end
end
end
Expand Down
171 changes: 89 additions & 82 deletions lib/authlogic/acts_as_authentic/password.rb
Expand Up @@ -18,7 +18,7 @@ module Config
# * <tt>Default:</tt> :crypted_password, :encrypted_password, :password_hash, or :pw_hash
# * <tt>Accepts:</tt> Symbol
def crypted_password_field(value = nil)
config(:crypted_password_field, value, first_column_to_exist(:crypted_password, :encrypted_password, :password_hash, :pw_hash))
config(:crypted_password_field, value, first_column_to_exist(nil, :crypted_password, :encrypted_password, :password_hash, :pw_hash))
end
alias_method :crypted_password_field=, :crypted_password_field

Expand Down Expand Up @@ -118,6 +118,7 @@ module Callbacks
]

def self.included(klass)
return if !klass.column_names.include?(klass.crypted_password_field.to_s)
klass.define_callbacks *METHODS
end

Expand All @@ -134,7 +135,11 @@ def #{method}
# The methods related to the password field.
module Methods
def self.included(klass)
return if !klass.column_names.include?(klass.crypted_password_field.to_s)

klass.class_eval do
include InstanceMethods

if validate_password_field
validates_length_of :password, validates_length_of_password_field_options
validates_confirmation_of :password, validates_confirmation_of_password_field_options
Expand All @@ -143,107 +148,109 @@ def self.included(klass)
end
end

# The password
def password
@password
end
module InstanceMethods
# The password
def password
@password
end

# This is a virtual method. Once a password is passed to it, it will create new password salt as well as encrypt
# the password.
def password=(pass)
return if ignore_blank_passwords? && pass.blank?
before_password_set
@password = pass
send("#{password_salt_field}=", Authlogic::Random.friendly_token) if password_salt_field
send("#{crypted_password_field}=", crypto_provider.encrypt(*encrypt_arguments(@password, act_like_restful_authentication? ? :restful_authentication : nil)))
@password_changed = true
after_password_set
end
# This is a virtual method. Once a password is passed to it, it will create new password salt as well as encrypt
# the password.
def password=(pass)
return if ignore_blank_passwords? && pass.blank?
before_password_set
@password = pass
send("#{password_salt_field}=", Authlogic::Random.friendly_token) if password_salt_field
send("#{crypted_password_field}=", crypto_provider.encrypt(*encrypt_arguments(@password, act_like_restful_authentication? ? :restful_authentication : nil)))
@password_changed = true
after_password_set
end

# Accepts a raw password to determine if it is the correct password or not.
def valid_password?(attempted_password)
return false if attempted_password.blank? || send(crypted_password_field).blank?
# Accepts a raw password to determine if it is the correct password or not.
def valid_password?(attempted_password)
return false if attempted_password.blank? || send(crypted_password_field).blank?

before_password_verification
before_password_verification

crypto_providers = [crypto_provider] + transition_from_crypto_providers
crypto_providers.each_with_index do |encryptor, index|
# The arguments_type of for the transitioning from restful_authentication
arguments_type = (act_like_restful_authentication? && index == 0) ||
(transition_from_restful_authentication? && index > 0 && encryptor == Authlogic::CryptoProviders::Sha1) ?
:restful_authentication : nil
crypto_providers = [crypto_provider] + transition_from_crypto_providers
crypto_providers.each_with_index do |encryptor, index|
# The arguments_type of for the transitioning from restful_authentication
arguments_type = (act_like_restful_authentication? && index == 0) ||
(transition_from_restful_authentication? && index > 0 && encryptor == Authlogic::CryptoProviders::Sha1) ?
:restful_authentication : nil

if encryptor.matches?(send(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?(send(crypted_password_field)))
self.password = attempted_password
save(false)
end
if encryptor.matches?(send(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?(send(crypted_password_field)))
self.password = attempted_password
save(false)
end

after_password_verification
after_password_verification

return true
return true
end
end
end

false
end
false
end

# Resets the password to a random friendly token.
def reset_password
friendly_token = Authlogic::Random.friendly_token
self.password = friendly_token
self.password_confirmation = friendly_token
end
alias_method :randomize_password, :reset_password
# Resets the password to a random friendly token.
def reset_password
friendly_token = Authlogic::Random.friendly_token
self.password = friendly_token
self.password_confirmation = friendly_token
end
alias_method :randomize_password, :reset_password

# Resets the password to a random friendly token and then saves the record.
def reset_password!
reset_password
save_without_session_maintenance(false)
end
alias_method :randomize_password!, :reset_password!
# Resets the password to a random friendly token and then saves the record.
def reset_password!
reset_password
save_without_session_maintenance(false)
end
alias_method :randomize_password!, :reset_password!

private
def encrypt_arguments(raw_password, arguments_type = nil)
salt = password_salt_field ? send(password_salt_field) : nil
case arguments_type
when :restful_authentication
[REST_AUTH_SITE_KEY, salt, raw_password, REST_AUTH_SITE_KEY].compact
else
[raw_password, salt].compact
private
def encrypt_arguments(raw_password, arguments_type = nil)
salt = password_salt_field ? send(password_salt_field) : nil
case arguments_type
when :restful_authentication
[REST_AUTH_SITE_KEY, salt, raw_password, REST_AUTH_SITE_KEY].compact
else
[raw_password, salt].compact
end
end
end

def require_password?
new_record? || password_changed? || send(crypted_password_field).blank?
end
def require_password?
new_record? || password_changed? || send(crypted_password_field).blank?
end

def ignore_blank_passwords?
self.class.ignore_blank_passwords == true
end
def ignore_blank_passwords?
self.class.ignore_blank_passwords == true
end

def password_changed?
@password_changed == true
end
def password_changed?
@password_changed == true
end

def crypted_password_field
self.class.crypted_password_field
end
def crypted_password_field
self.class.crypted_password_field
end

def password_salt_field
self.class.password_salt_field
end
def password_salt_field
self.class.password_salt_field
end

def crypto_provider
self.class.crypto_provider
end
def crypto_provider
self.class.crypto_provider
end

def transition_from_crypto_providers
self.class.transition_from_crypto_providers
end
def transition_from_crypto_providers
self.class.transition_from_crypto_providers
end
end
end
end
end
Expand Down
39 changes: 22 additions & 17 deletions lib/authlogic/session/password.rb
Expand Up @@ -5,7 +5,7 @@ module Password
def self.included(klass)
klass.class_eval do
extend Config
include InstanceMethods
include Methods
validate :validate_by_password, :if => :authenticating_with_password?

class << self
Expand Down Expand Up @@ -41,19 +41,19 @@ def find_by_login_method(value = nil)
# 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> Uses the configuration option in your model: User.login_field
# * <tt>Default:</tt> klass.login_field || klass.email_field
# * <tt>Accepts:</tt> Symbol or String
def login_field(value = nil)
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.
# 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)
config(:password_field, value, :password)
config(:password_field, value, login_field && :password)
end
alias_method :password_field=, :password_field

Expand All @@ -68,21 +68,26 @@ def verify_password_method(value = nil)
end

# Password related instance methods
module InstanceMethods
module Methods
def initialize(*args)
if !self.class.configured_password_methods
self.class.send(:attr_writer, login_field) if !respond_to?("#{login_field}=")
self.class.send(:attr_reader, login_field) if !respond_to?(login_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)
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. The prevent this we just create this method that is private.
def protected_#{password_field}
@#{password_field}
end
end_eval
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. The 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
Expand Down Expand Up @@ -114,7 +119,7 @@ def credentials=(value)

private
def authenticating_with_password?
!send(login_field).nil? || !send("protected_#{password_field}").nil?
login_field && (!send(login_field).nil? || !send("protected_#{password_field}").nil?)
end

def validate_by_password
Expand Down

0 comments on commit 5c0ac4f

Please sign in to comment.