From 65b890896007572c46c0a4ee0f420861e0935be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 29 Mar 2010 20:52:34 +0200 Subject: [PATCH] Create authenticatable base model and strategy. --- lib/devise.rb | 24 ++++---- lib/devise/models.rb | 4 ++ lib/devise/models/authenticatable.rb | 38 +++++++++++++ lib/devise/models/database_authenticatable.rb | 31 +++-------- lib/devise/models/http_authenticatable.rb | 19 ------- lib/devise/models/token_authenticatable.rb | 2 +- lib/devise/modules.rb | 3 +- lib/devise/strategies/authenticatable.rb | 55 +++++++++++++++++++ lib/devise/strategies/base.rb | 1 + .../strategies/database_authenticatable.rb | 10 +--- lib/devise/strategies/http_authenticatable.rb | 34 ------------ .../devise_install/templates/devise.rb | 3 + test/mapping_test.rb | 3 +- test/models/database_authenticatable_test.rb | 32 ----------- test/models/http_authenticatable_test.rb | 19 ------- test/rails_app/app/active_record/admin.rb | 4 -- test/rails_app/app/data_mapper/admin.rb | 4 -- test/rails_app/app/mongoid/admin.rb | 4 -- 18 files changed, 130 insertions(+), 160 deletions(-) create mode 100644 lib/devise/models/authenticatable.rb delete mode 100644 lib/devise/models/http_authenticatable.rb create mode 100644 lib/devise/strategies/authenticatable.rb delete mode 100644 lib/devise/strategies/http_authenticatable.rb delete mode 100644 test/models/http_authenticatable_test.rb diff --git a/lib/devise.rb b/lib/devise.rb index 4196f94e..cfec32e8 100644 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -54,15 +54,23 @@ module Devise # Keys used when authenticating an user. mattr_accessor :authentication_keys @@authentication_keys = [ :email ] - - # Range validation for password length - mattr_accessor :password_length - @@password_length = 6..20 - + + # If http authentication is enabled by default. + mattr_accessor :http_authenticatable + @@http_authenticatable = true + + # The realm used in Http Basic Authentication. + mattr_accessor :http_authentication_realm + @@http_authentication_realm = "Application" + # Email regex used to validate email formats. Adapted from authlogic. mattr_accessor :email_regexp @@email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i - + + # Range validation for password length + mattr_accessor :password_length + @@password_length = 6..20 + # Time interval where the remember me token is valid. mattr_accessor :remember_for @@remember_for = 2.weeks @@ -122,10 +130,6 @@ module Devise mattr_accessor :token_authentication_key @@token_authentication_key = :auth_token - # The realm used in Http Basic Authentication. - mattr_accessor :http_authentication_realm - @@http_authentication_realm = "Application" - # Private methods to interface with Warden. mattr_reader :warden_config @@warden_config = nil diff --git a/lib/devise/models.rb b/lib/devise/models.rb index 1045e45b..7ace5982 100644 --- a/lib/devise/models.rb +++ b/lib/devise/models.rb @@ -53,6 +53,10 @@ module Devise modules << :database_authenticatable end + if modules.delete(:http_authenticatable) + ActiveSupport::Deprecation.warn ":http_authenticatable as module is deprecated and is on by default. Revert by setting :http_authenticatable => false.", caller + end + @devise_modules = Devise::ALL & modules.map(&:to_sym).uniq devise_modules_hook! do diff --git a/lib/devise/models/authenticatable.rb b/lib/devise/models/authenticatable.rb new file mode 100644 index 00000000..3a5045bd --- /dev/null +++ b/lib/devise/models/authenticatable.rb @@ -0,0 +1,38 @@ +module Devise + module Models + # Authenticable module. Holds common settings for authentication. + # + # Configuration: + # + # You can overwrite configuration values by setting in globally in Devise, + # using devise method or overwriting the respective instance method. + # + # authentication_keys: parameters used for authentication. By default [:email]. + # + # http_authenticatable: if this model allows http authentication. By default true. + # + module Authenticatable + extend ActiveSupport::Concern + + module ClassMethods + Devise::Models.config(self, :authentication_keys, :http_authenticatable) + + alias :http_authenticatable? :http_authenticatable + + # Find first record based on conditions given (ie by the sign in form). + # Overwrite to add customized conditions, create a join, or maybe use a + # namedscope to filter records while authenticating. + # Example: + # + # def self.find_for_authentication(conditions={}) + # conditions[:active] = true + # super + # end + # + def find_for_authentication(conditions) + find(:first, :conditions => conditions) + end + end + end + end +end \ No newline at end of file diff --git a/lib/devise/models/database_authenticatable.rb b/lib/devise/models/database_authenticatable.rb index cb493ec7..7cf8fe91 100644 --- a/lib/devise/models/database_authenticatable.rb +++ b/lib/devise/models/database_authenticatable.rb @@ -1,3 +1,4 @@ +require 'devise/models/authenticatable' require 'devise/strategies/database_authenticatable' module Devise @@ -19,8 +20,6 @@ module Devise # # encryptor: the encryptor going to be used. By default :sha1. # - # authentication_keys: parameters used for authentication. By default [:email] - # # Examples: # # User.authenticate('email@test.com', 'password123') # returns authenticated user or nil @@ -30,6 +29,8 @@ module Devise extend ActiveSupport::Concern included do + include Devise::Models::Authenticatable + attr_reader :password, :current_password attr_accessor :password_confirmation end @@ -89,15 +90,13 @@ module Devise end module ClassMethods - Devise::Models.config(self, :pepper, :stretches, :encryptor, :authentication_keys) + Devise::Models.config(self, :pepper, :stretches, :encryptor) # Authenticate a user based on configured attribute keys. Returns the # authenticated user if it's valid or nil. - def authenticate(attributes={}) - return unless authentication_keys.all? { |k| attributes[k].present? } - conditions = attributes.slice(*authentication_keys) - resource = find_for_authentication(conditions) - resource if resource.try(:valid_for_authentication?, attributes) + def authenticate(conditions) + resource = find_for_database_authentication(conditions.except(:password)) + resource if resource.try(:valid_for_authentication?, conditions) end # Returns the class for the configured encryptor. @@ -105,20 +104,8 @@ module Devise @encryptor_class ||= ::Devise::Encryptors.const_get(encryptor.to_s.classify) end - protected - - # Find first record based on conditions given (ie by the sign in form). - # Overwrite to add customized conditions, create a join, or maybe use a - # namedscope to filter records while authenticating. - # Example: - # - # def self.find_for_authentication(conditions={}) - # conditions[:active] = true - # find(:first, :conditions => conditions) - # end - # - def find_for_authentication(conditions) - find(:first, :conditions => conditions) + def find_for_database_authentication(*args) + find_for_authentication(*args) end end end diff --git a/lib/devise/models/http_authenticatable.rb b/lib/devise/models/http_authenticatable.rb deleted file mode 100644 index 7df43b1b..00000000 --- a/lib/devise/models/http_authenticatable.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'devise/strategies/http_authenticatable' - -module Devise - module Models - # Adds HttpAuthenticatable behavior to your model. It expects that your - # model class responds to authenticate and authentication_keys methods - # (which for example are defined in authenticatable). - module HttpAuthenticatable - extend ActiveSupport::Concern - - module ClassMethods - # Authenticate an user using http. - def authenticate_with_http(username, password) - authenticate(authentication_keys.first => username, :password => password) - end - end - end - end -end diff --git a/lib/devise/models/token_authenticatable.rb b/lib/devise/models/token_authenticatable.rb index 75fd52ec..2e095be0 100644 --- a/lib/devise/models/token_authenticatable.rb +++ b/lib/devise/models/token_authenticatable.rb @@ -68,7 +68,7 @@ module Devise # # def self.find_for_token_authentication(token, conditions = {}) # conditions = {:active => true} - # self.find_by_authentication_token(token, :conditions => conditions) + # super # end # def find_for_token_authentication(token) diff --git a/lib/devise/modules.rb b/lib/devise/modules.rb index fcae6ea6..e589d11f 100644 --- a/lib/devise/modules.rb +++ b/lib/devise/modules.rb @@ -2,9 +2,8 @@ require 'active_support/core_ext/object/with_options' Devise.with_options :model => true do |d| # Strategies first - d.with_options :strategy => true do |s| + d.with_options :strategy => true do |s| s.add_module :database_authenticatable, :controller => :sessions, :flash => :invalid, :route => :session - s.add_module :http_authenticatable s.add_module :token_authenticatable, :controller => :sessions, :flash => :invalid_token, :route => :session s.add_module :rememberable end diff --git a/lib/devise/strategies/authenticatable.rb b/lib/devise/strategies/authenticatable.rb new file mode 100644 index 00000000..1a4cd12b --- /dev/null +++ b/lib/devise/strategies/authenticatable.rb @@ -0,0 +1,55 @@ +require 'devise/strategies/base' + +module Devise + module Strategies + class Authenticatable < Base + attr_accessor :authentication_hash, :password + + def valid? + valid_for_http_auth? || valid_for_params_auth? + end + + private + + def valid_for_http_auth? + mapping.to.http_authenticatable? && request.authorization && set_http_auth_hash + end + + def valid_for_params_auth? + valid_controller? && valid_params? && set_params_auth_hash + end + + def valid_controller? + mapping.controllers[:sessions] == params[:controller] + end + + def valid_params? + params[scope].is_a?(Hash) + end + + def set_http_auth_hash + keys = [authentication_keys.first, :password] + with_authentication_hash Hash[*keys.zip(decode_credentials).flatten] + end + + def decode_credentials + username_and_password = request.authorization.split(' ', 2).last || '' + ActiveSupport::Base64.decode64(username_and_password).split(/:/, 2) + end + + def set_params_auth_hash + with_authentication_hash params[scope] + end + + def with_authentication_hash(hash) + self.authentication_hash = hash.slice(*authentication_keys) + self.password = hash[:password] + authentication_keys.all?{ |k| authentication_hash[k].present? } && password.present? + end + + def authentication_keys + @authentication_keys ||= mapping.to.authentication_keys + end + end + end +end \ No newline at end of file diff --git a/lib/devise/strategies/base.rb b/lib/devise/strategies/base.rb index f7344b1f..0944e5d9 100644 --- a/lib/devise/strategies/base.rb +++ b/lib/devise/strategies/base.rb @@ -12,6 +12,7 @@ module Devise end end + # TODO Move to a module def success!(record) if record.respond_to?(:active?) && !record.active? fail!(record.inactive_message) diff --git a/lib/devise/strategies/database_authenticatable.rb b/lib/devise/strategies/database_authenticatable.rb index a011f4d0..41b18dd3 100644 --- a/lib/devise/strategies/database_authenticatable.rb +++ b/lib/devise/strategies/database_authenticatable.rb @@ -1,19 +1,15 @@ -require 'devise/strategies/base' +require 'devise/strategies/authenticatable' module Devise module Strategies # Default strategy for signing in a user, based on his email and password. # Redirects to sign_in page if it's not authenticated - class DatabaseAuthenticatable < Base - def valid? - valid_controller? && valid_params? - end - + class DatabaseAuthenticatable < Authenticatable # Authenticate a user based on email and password params, returning to warden # success and the authenticated user if everything is okay. Otherwise redirect # to sign in page. def authenticate! - if resource = mapping.to.authenticate(params[scope]) + if resource = mapping.to.authenticate(authentication_hash.merge(:password => password)) success!(resource) else fail(:invalid) diff --git a/lib/devise/strategies/http_authenticatable.rb b/lib/devise/strategies/http_authenticatable.rb deleted file mode 100644 index 0ae9c607..00000000 --- a/lib/devise/strategies/http_authenticatable.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'devise/strategies/base' - -module Devise - module Strategies - # Sign in an user using HTTP authentication. - class HttpAuthenticatable < Base - def valid? - request.authorization - end - - def authenticate! - username, password = username_and_password - - if resource = mapping.to.authenticate_with_http(username, password) - success!(resource) - else - fail!(:invalid) - end - end - - private - - def username_and_password - decode_credentials(request).split(/:/, 2) - end - - def decode_credentials(request) - ActiveSupport::Base64.decode64(request.authorization.split(' ', 2).last || '') - end - end - end -end - -Warden::Strategies.add(:http_authenticatable, Devise::Strategies::HttpAuthenticatable) diff --git a/lib/generators/devise_install/templates/devise.rb b/lib/generators/devise_install/templates/devise.rb index 5062f884..ff6ae6d7 100644 --- a/lib/generators/devise_install/templates/devise.rb +++ b/lib/generators/devise_install/templates/devise.rb @@ -12,6 +12,9 @@ Devise.setup do |config| # session. If you need permissions, you should implement that in a before filter. # config.authentication_keys = [ :email ] + # Tell if authentication for http is enabled. True by default. + # config.http_authenticatable = true + # The realm used in Http Basic Authentication # config.http_authentication_realm = "Application" diff --git a/test/mapping_test.rb b/test/mapping_test.rb index 25e315cd..f6508624 100644 --- a/test/mapping_test.rb +++ b/test/mapping_test.rb @@ -30,8 +30,7 @@ class MappingTest < ActiveSupport::TestCase end test 'has strategies depending on the model declaration' do - assert_equal [:rememberable, :token_authenticatable, - :http_authenticatable, :database_authenticatable], Devise.mappings[:user].strategies + assert_equal [:rememberable, :token_authenticatable, :database_authenticatable], Devise.mappings[:user].strategies assert_equal [:database_authenticatable], Devise.mappings[:admin].strategies end diff --git a/test/models/database_authenticatable_test.rb b/test/models/database_authenticatable_test.rb index cd947b49..57676ee6 100644 --- a/test/models/database_authenticatable_test.rb +++ b/test/models/database_authenticatable_test.rb @@ -98,38 +98,6 @@ class DatabaseAuthenticatableTest < ActiveSupport::TestCase assert_not user.valid_password?('654321') end - test 'should authenticate a valid user with email and password and return it' do - user = create_user - user.confirm! - authenticated_user = User.authenticate(:email => user.email, :password => user.password) - assert_equal authenticated_user, user - end - - test 'should return nil when authenticating an invalid user by email' do - user = create_user - authenticated_user = User.authenticate(:email => 'another.email@email.com', :password => user.password) - assert_nil authenticated_user - end - - test 'should return nil when authenticating an invalid user by password' do - user = create_user - authenticated_user = User.authenticate(:email => user.email, :password => 'another_password') - assert_nil authenticated_user - end - - test 'should use authentication keys to retrieve users' do - swap Devise, :authentication_keys => [:username] do - user = create_user - assert_nil User.authenticate(:email => user.email, :password => user.password) - assert_not_nil User.authenticate(:username => user.username, :password => user.password) - end - end - - test 'should allow overwriting find for authentication conditions' do - admin = Admin.create!(valid_attributes) - assert_not_nil Admin.authenticate(:email => admin.email, :password => admin.password) - end - test 'should respond to current password' do assert new_user.respond_to?(:current_password) end diff --git a/test/models/http_authenticatable_test.rb b/test/models/http_authenticatable_test.rb deleted file mode 100644 index 24dd072f..00000000 --- a/test/models/http_authenticatable_test.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'test_helper' - -class HttpAuthenticatableTest < ActiveSupport::TestCase - test 'should authenticate a valid user with email and password and return it' do - user = create_user - user.confirm! - - authenticated_user = User.authenticate_with_http(user.email, user.password) - assert_equal authenticated_user, user - end - - test 'should return nil when authenticating an invalid user by email' do - user = create_user - user.confirm! - - authenticated_user = User.authenticate_with_http('another.email@email.com', user.password) - assert_nil authenticated_user - end -end diff --git a/test/rails_app/app/active_record/admin.rb b/test/rails_app/app/active_record/admin.rb index 87f8c99e..6247a1e5 100644 --- a/test/rails_app/app/active_record/admin.rb +++ b/test/rails_app/app/active_record/admin.rb @@ -1,7 +1,3 @@ class Admin < ActiveRecord::Base devise :authenticatable, :registerable, :timeoutable, :recoverable - - def self.find_for_authentication(conditions) - last(:conditions => conditions) - end end diff --git a/test/rails_app/app/data_mapper/admin.rb b/test/rails_app/app/data_mapper/admin.rb index 096df9a7..92c7cd1b 100644 --- a/test/rails_app/app/data_mapper/admin.rb +++ b/test/rails_app/app/data_mapper/admin.rb @@ -5,10 +5,6 @@ class Admin property :username, String devise :authenticatable, :registerable, :timeoutable, :recoverable - - def self.find_for_authentication(conditions) - last(conditions) - end def self.create!(*args) create(*args) diff --git a/test/rails_app/app/mongoid/admin.rb b/test/rails_app/app/mongoid/admin.rb index 845111f9..5db64b94 100644 --- a/test/rails_app/app/mongoid/admin.rb +++ b/test/rails_app/app/mongoid/admin.rb @@ -2,10 +2,6 @@ class Admin include Mongoid::Document devise :authenticatable, :timeoutable, :registerable, :recoverable - - def self.find_for_authentication(conditions) - last(:conditions => conditions, :sort => [[:email, :asc]]) - end def self.last(options={}) options.delete(:order) if options[:order] == "id"