diff --git a/README.rdoc b/README.rdoc index 63818a98..6988b04c 100644 --- a/README.rdoc +++ b/README.rdoc @@ -90,6 +90,16 @@ This line adds devise authenticable automatically for you inside your User class Note that validations aren't added by default, so you're able to customize it. In order to have automatic validations working just include :validatable. +In addition to :except, you can provide some options to devise call: + +* pepper: setup a pepper to generate de encrypted password. By default no pepper is used: + + devise :all, :pepper => 'my_pepper' + +* stretches: configure how many times you want the password is reencrypted. + + devise :all, :stretches => 20 + The next step after setting up your model is to configure your routes for devise. You do this by opening up your config/routes.rb and adding: map.devise_for :users diff --git a/TODO b/TODO index 116c305d..52aec516 100644 --- a/TODO +++ b/TODO @@ -2,7 +2,6 @@ * Add customizable time frame for confirmation and filters * Create generators -* Allow stretches and pepper per model * Use request_ip in session cookies * Session timeout @@ -35,3 +34,4 @@ * Add remember me * Mailer subjects namespaced by model +* Allow stretches and pepper per model diff --git a/lib/devise.rb b/lib/devise.rb index 3b3f5685..72a1a949 100644 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -16,6 +16,16 @@ module Devise }.freeze TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].freeze + + # Default pepper and stretches used in authenticable to create password hash. + # You can setup it inside each model or globaly using this attributes. + # Example: + # Devise.pepper = 'my_pepper_123' + # Devise.stretches = 20 + mattr_accessor :pepper, :stretches + + # Default stretches configuration + self.stretches = 10 end require 'devise/warden' diff --git a/lib/devise/active_record.rb b/lib/devise/active_record.rb index bbdee474..b874d1fb 100644 --- a/lib/devise/active_record.rb +++ b/lib/devise/active_record.rb @@ -1,6 +1,23 @@ module Devise module ActiveRecord - # Shortcut method for including all devise modules inside your model + # Shortcut method for including all devise modules inside your model. + # You can give some extra options while declaring devise in your model: + # + # * except: let's you add all devise modules, except the ones you setup here: + # + # devise :all, :except => :rememberable + # + # * pepper: setup a pepper to generate de encrypted password. By default no + # pepper is used: + # + # devise :all, :pepper => 'my_pepper' + # + # * stretches: configure how many times you want the password is reencrypted. + # + # devise :all, :stretches => 20 + # + # You can refer to Authenticable for more information about writing your own + # method to setup pepper and stretches # # Examples: # @@ -30,6 +47,7 @@ module Devise # def devise(*modules) options = modules.extract_options! + options.assert_valid_keys(:except, :stretches, :pepper) modules = Devise::ALL if modules.include?(:all) modules -= Array(options[:except]) if options.key?(:except) @@ -39,6 +57,13 @@ module Devise devise_modules << m.to_sym include Devise::Models.const_get(m.to_s.classify) end + + if options.key?(:stretches) || options.key?(:pepper) + class_eval <<-END_EVAL, __FILE__, __LINE__ + def stretches; #{options[:stretches]}; end if options.key?(:stretches) + def pepper; '#{options[:pepper]}'; end if options.key?(:pepper) + END_EVAL + end end # Stores all modules included inside the model, so we are able to verify diff --git a/lib/devise/models/authenticable.rb b/lib/devise/models/authenticable.rb index 251415c7..1a5a1b93 100644 --- a/lib/devise/models/authenticable.rb +++ b/lib/devise/models/authenticable.rb @@ -10,8 +10,10 @@ module Devise # pepper: encryption key used for creating encrypted password. Each time # password changes, it's gonna be encrypted again, and this key # is added to the password and salt to create a secure hash. + # def pepper; '1234567890987654321'; end # # stretches: defines how many times the password will be encrypted. + # def stretches; 20; end # # Examples: # @@ -20,12 +22,6 @@ module Devise module Authenticable mattr_accessor :pepper, :stretches - # Pepper for encrypting password - self.pepper = '23c64df433d9b08e464db5c05d1e6202dd2823f0' - - # Encrypt password as many times as possible - self.stretches = 10 - def self.included(base) base.class_eval do extend ClassMethods @@ -36,21 +32,41 @@ module Devise end end + # Regenerates password salt and encrypted password each time password is + # setted. def password=(new_password) @password = new_password self.password_salt = friendly_token self.encrypted_password = password_digest(@password) end - # Verifies whether an incoming_password (ie from login) is the user password + # Verifies whether an incoming_password (ie from login) is the user + # password. def valid_password?(incoming_password) password_digest(incoming_password) == encrypted_password end - private + protected + + # Pepper for encrypting password. Fallback to default configuration if + # no one exists for this specific model. Overwrite inside your model + # to provide specific pepper configuration: + # + # def pepper; 'my_pepper_123'; end + def pepper + @pepper ||= Devise.pepper + end + + # Encrypt password as many times as possible. Fallback to default + # configuration if no one exists for this specific model. + # + # def stretches; 20; end + def stretches + @stretches ||= Devise.stretches + end # Gererates a default password digest based on salt, pepper and the - # incoming password + # incoming password. def password_digest(password_to_digest) digest = pepper stretches.times { digest = secure_digest(password_salt, digest, password_to_digest, pepper) } @@ -64,7 +80,7 @@ module Devise ::Digest::SHA1.hexdigest('--' << tokens.flatten.join('--') << '--') end - # Generate a friendly string randomically to be used as token + # Generate a friendly string randomically to be used as token. def friendly_token ActiveSupport::SecureRandom.base64(15).tr('+/=', '-_ ').strip.delete("\n") end diff --git a/lib/devise/models/confirmable.rb b/lib/devise/models/confirmable.rb index 5c0fd8b0..444a3037 100644 --- a/lib/devise/models/confirmable.rb +++ b/lib/devise/models/confirmable.rb @@ -56,7 +56,7 @@ module Devise end end - private + protected # Remove confirmation date from the user, ensuring after a user update # it's email, it won't be able to sign in without confirming it. diff --git a/lib/devise/models/validatable.rb b/lib/devise/models/validatable.rb index b17da292..e18d2d9b 100644 --- a/lib/devise/models/validatable.rb +++ b/lib/devise/models/validatable.rb @@ -23,7 +23,7 @@ module Devise end end - private + protected # Checks whether a password is needed or not. For validations only. # Passwords are always required if it's a new record, or if the password diff --git a/test/active_record_test.rb b/test/active_record_test.rb index 0e30178f..be621b1c 100644 --- a/test/active_record_test.rb +++ b/test/active_record_test.rb @@ -1,79 +1,96 @@ require 'test/test_helper' -class Authenticable < ActiveRecord::Base +class Authenticable < User devise end -class Confirmable < ActiveRecord::Base +class Confirmable < User devise :confirmable end -class Recoverable < ActiveRecord::Base +class Recoverable < User devise :recoverable end -class Validatable < ActiveRecord::Base +class Rememberable < User + devise :rememberable +end + +class Validatable < User devise :validatable end -class Devisable < ActiveRecord::Base +class Devisable < User devise :all end +class Exceptable < User + devise :all, :except => [:recoverable, :rememberable, :validatable] +end + +class Configurable < User + devise :all, :stretches => 15, :pepper => 'abcdef' +end + class ActiveRecordTest < ActiveSupport::TestCase - def include_authenticable_module?(mod) - mod.devise_modules.include?(:authenticable) && - mod.included_modules.include?(Devise::Models::Authenticable) + def include_module?(klass, mod) + klass.devise_modules.include?(mod) && + klass.included_modules.include?(Devise::Models::const_get(mod.to_s.classify)) end - def include_confirmable_module?(mod) - mod.devise_modules.include?(:confirmable) && - mod.included_modules.include?(Devise::Models::Confirmable) + def assert_include_modules(klass, *modules) + modules.each do |mod| + assert include_module?(klass, mod) + end end - def include_recoverable_module?(mod) - mod.devise_modules.include?(:recoverable) && - mod.included_modules.include?(Devise::Models::Recoverable) + def assert_not_include_modules(klass, *modules) + modules.each do |mod| + assert_not include_module?(klass, mod) + end end - def include_validatable_module?(mod) - mod.devise_modules.include?(:validatable) && - mod.included_modules.include?(Devise::Models::Validatable) + test 'include by default authenticable only' do + assert_include_modules Authenticable, :authenticable + assert_not_include_modules Authenticable, :confirmable, :recoverable, :rememberable, :validatable end - test 'acts as devisable should include by default authenticable only' do - assert include_authenticable_module?(Authenticable) - assert_not include_confirmable_module?(Authenticable) - assert_not include_recoverable_module?(Authenticable) - assert_not include_validatable_module?(Authenticable) + test 'add confirmable module only' do + assert_include_modules Confirmable, :authenticable, :confirmable + assert_not_include_modules Confirmable, :recoverable, :rememberable, :validatable end - test 'acts as devisable should be able to add confirmable module only' do - assert include_authenticable_module?(Confirmable) - assert include_confirmable_module?(Confirmable) - assert_not include_recoverable_module?(Confirmable) - assert_not include_validatable_module?(Confirmable) + test 'add recoverable module only' do + assert_include_modules Recoverable, :authenticable, :recoverable + assert_not_include_modules Recoverable, :confirmable, :rememberable, :validatable end - test 'acts as devisable should be able to add recoverable module only' do - assert include_authenticable_module?(Recoverable) - assert_not include_confirmable_module?(Recoverable) - assert include_recoverable_module?(Recoverable) - assert_not include_validatable_module?(Recoverable) + test 'add rememberable module only' do + assert_include_modules Rememberable, :authenticable, :rememberable + assert_not_include_modules Rememberable, :confirmable, :recoverable, :validatable end - test 'acts as devisable should be able to add validatable module only' do - assert include_authenticable_module?(Validatable) - assert_not include_confirmable_module?(Validatable) - assert_not include_recoverable_module?(Validatable) - assert include_validatable_module?(Validatable) + test 'add validatable module only' do + assert_include_modules Validatable, :authenticable, :validatable + assert_not_include_modules Validatable, :confirmable, :recoverable, :rememberable end - test 'acts as devisable should be able to add all modules' do - assert include_authenticable_module?(Devisable) - assert include_confirmable_module?(Devisable) - assert include_recoverable_module?(Devisable) - assert include_validatable_module?(Devisable) + test 'add all modules' do + assert_include_modules Devisable, + :authenticable, :confirmable, :recoverable, :rememberable, :validatable + end + + test 'configure modules with except option' do + assert_include_modules Exceptable, :authenticable, :confirmable + assert_not_include_modules Exceptable, :recoverable, :rememberable, :validatable + end + + test 'set a default value for stretches' do + assert_equal 15, Configurable.new.send(:stretches) + end + + test 'set a default value for pepper' do + assert_equal 'abcdef', Configurable.new.send(:pepper) end end diff --git a/test/models/authenticable_test.rb b/test/models/authenticable_test.rb index 333edaf5..fcf392fd 100644 --- a/test/models/authenticable_test.rb +++ b/test/models/authenticable_test.rb @@ -74,11 +74,41 @@ class AuthenticableTest < ActiveSupport::TestCase end test 'should encrypt password using a sha1 hash' do - Devise::Models::Authenticable.pepper = 'pepper' - Devise::Models::Authenticable.stretches = 1 - user = create_user - expected_password = ::Digest::SHA1.hexdigest("--#{user.password_salt}--pepper--123456--pepper--") - assert_equal expected_password, user.encrypted_password + user = new_user + assert_equal encrypt_password(user), user.encrypted_password + end + + def encrypt_password(user, pepper=nil, stretches=1) + user.instance_variable_set(:@stretches, stretches) if stretches + user.password = '123456' + ::Digest::SHA1.hexdigest("--#{user.password_salt}--#{pepper}--123456--#{pepper}--") + end + + test 'should fallback to devise pepper default configuring' do + begin + Devise.pepper = '' + user = new_user + assert_equal encrypt_password(user), user.encrypted_password + Devise.pepper = 'new_pepper' + user = new_user + assert_equal encrypt_password(user, 'new_pepper'), user.encrypted_password + Devise.pepper = '123456' + user = new_user + assert_equal encrypt_password(user, '123456'), user.encrypted_password + ensure + Devise.pepper = nil + end + end + + test 'should fallback to devise stretches default configuring' do + begin + default_stretches = Devise.stretches + Devise.stretches = 1 + user = new_user + assert_equal encrypt_password(user, nil, nil), user.encrypted_password + ensure + Devise.stretches = default_stretches + end end test 'should test for a valid password' do