devise-security 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +41 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +22 -2
  5. data/.ruby-version +1 -1
  6. data/.travis.yml +15 -3
  7. data/Appraisals +19 -0
  8. data/Gemfile +1 -0
  9. data/README.md +15 -10
  10. data/Rakefile +3 -1
  11. data/app/controllers/devise/paranoid_verification_code_controller.rb +1 -1
  12. data/app/controllers/devise/password_expired_controller.rb +1 -1
  13. data/app/views/devise/paranoid_verification_code/show.html.erb +2 -2
  14. data/app/views/devise/password_expired/show.html.erb +5 -5
  15. data/config/locales/de.yml +7 -7
  16. data/config/locales/en.yml +8 -8
  17. data/config/locales/es.yml +8 -8
  18. data/devise-security.gemspec +12 -6
  19. data/gemfiles/rails_4.1_stable.gemfile +8 -0
  20. data/gemfiles/rails_4.2_stable.gemfile +8 -0
  21. data/gemfiles/rails_5.0_stable.gemfile +8 -0
  22. data/gemfiles/rails_5.1_stable.gemfile +8 -0
  23. data/gemfiles/rails_5.2_rc1.gemfile +8 -0
  24. data/lib/devise-security/controllers/helpers.rb +2 -2
  25. data/lib/devise-security/hooks/session_limitable.rb +3 -3
  26. data/lib/devise-security/models/compatibility.rb +22 -0
  27. data/lib/devise-security/models/expirable.rb +13 -13
  28. data/lib/devise-security/models/old_password.rb +1 -1
  29. data/lib/devise-security/models/paranoid_verification.rb +5 -2
  30. data/lib/devise-security/models/password_archivable.rb +34 -38
  31. data/lib/devise-security/models/password_expirable.rb +1 -1
  32. data/lib/devise-security/models/secure_validatable.rb +16 -14
  33. data/lib/devise-security/models/security_questionable.rb +1 -2
  34. data/lib/devise-security/models/session_limitable.rb +3 -3
  35. data/lib/devise-security/orm/active_record.rb +1 -3
  36. data/lib/devise-security/patches/confirmations_controller_captcha.rb +2 -2
  37. data/lib/devise-security/patches/confirmations_controller_security_question.rb +2 -2
  38. data/lib/devise-security/patches/passwords_controller_captcha.rb +2 -2
  39. data/lib/devise-security/patches/passwords_controller_security_question.rb +2 -2
  40. data/lib/devise-security/patches/registrations_controller_captcha.rb +2 -2
  41. data/lib/devise-security/patches/sessions_controller_captcha.rb +3 -3
  42. data/lib/devise-security/patches/unlocks_controller_captcha.rb +2 -2
  43. data/lib/devise-security/patches/unlocks_controller_security_question.rb +2 -2
  44. data/lib/devise-security/rails.rb +2 -2
  45. data/lib/devise-security/routes.rb +2 -3
  46. data/lib/devise-security/schema.rb +11 -6
  47. data/lib/devise-security/version.rb +1 -1
  48. data/test/dummy/app/models/application_record.rb +3 -0
  49. data/test/dummy/app/models/captcha_user.rb +1 -1
  50. data/test/dummy/app/models/security_question_user.rb +2 -3
  51. data/test/dummy/app/models/user.rb +21 -4
  52. data/test/dummy/app/models/widget.rb +4 -0
  53. data/test/dummy/config/environments/test.rb +10 -2
  54. data/test/dummy/config/initializers/devise.rb +1 -0
  55. data/test/dummy/config/secrets.yml +1 -2
  56. data/test/dummy/db/migrate/20120508165529_create_tables.rb +9 -3
  57. data/test/dummy/db/migrate/20180318103603_add_expireable_columns.rb +6 -0
  58. data/test/dummy/db/migrate/20180318105329_add_confirmable_columns.rb +8 -0
  59. data/test/dummy/db/migrate/20180318105732_add_rememberable_columns.rb +5 -0
  60. data/test/dummy/db/migrate/20180318111336_add_recoverable_columns.rb +6 -0
  61. data/test/dummy/db/migrate/20180319114023_add_widget.rb +8 -0
  62. data/test/test_captcha_controller.rb +13 -13
  63. data/test/test_helper.rb +7 -0
  64. data/test/test_paranoid_verification.rb +2 -2
  65. data/test/test_password_archivable.rb +27 -13
  66. data/test/test_password_expirable.rb +2 -2
  67. data/test/test_password_expired_controller.rb +25 -10
  68. data/test/test_security_question_controller.rb +45 -21
  69. metadata +90 -13
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "omniauth"
6
+ gem "rails", "~> 5.0.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "omniauth"
6
+ gem "rails", "~> 5.1.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "omniauth"
6
+ gem "rails", "~> 5.2.0.rc1"
7
+
8
+ gemspec path: "../"
@@ -77,11 +77,11 @@ module DeviseSecurity
77
77
 
78
78
  # redirect for password update with alert message
79
79
  def redirect_for_password_change(scope)
80
- redirect_to change_password_required_path_for(scope), :alert => I18n.t('change_required', {:scope => 'devise.password_expired'})
80
+ redirect_to change_password_required_path_for(scope), alert: I18n.t('change_required', {scope: 'devise.password_expired'})
81
81
  end
82
82
 
83
83
  def redirect_for_paranoid_verification(scope)
84
- redirect_to paranoid_verification_code_path_for(scope), :alert => I18n.t('code_required', {:scope => 'devise.paranoid_verify'})
84
+ redirect_to paranoid_verification_code_path_for(scope), alert: I18n.t('code_required', {scope: 'devise.paranoid_verify'})
85
85
  end
86
86
 
87
87
  # path for change password
@@ -2,7 +2,7 @@
2
2
  # This is only triggered when the user is explicitly set (with set_user)
3
3
  # and on authentication. Retrieving the user from session (:fetch) does
4
4
  # not trigger it.
5
- Warden::Manager.after_set_user :except => :fetch do |record, warden, options|
5
+ Warden::Manager.after_set_user except: :fetch do |record, warden, options|
6
6
  if record.respond_to?(:update_unique_session_id!) && warden.authenticated?(options[:scope])
7
7
  unique_session_id = Devise.friendly_token
8
8
  warden.session(options[:scope])['unique_session_id'] = unique_session_id
@@ -13,7 +13,7 @@ end
13
13
  # Each time a record is fetched from session we check if a new session from another
14
14
  # browser was opened for the record or not, based on a unique session identifier.
15
15
  # If so, the old account is logged out and redirected to the sign in page on the next request.
16
- Warden::Manager.after_set_user :only => :fetch do |record, warden, options|
16
+ Warden::Manager.after_set_user only: :fetch do |record, warden, options|
17
17
  scope = options[:scope]
18
18
  env = warden.request.env
19
19
 
@@ -21,7 +21,7 @@ Warden::Manager.after_set_user :only => :fetch do |record, warden, options|
21
21
  if record.unique_session_id != warden.session(scope)['unique_session_id'] && !env['devise.skip_session_limitable']
22
22
  warden.raw_session.clear
23
23
  warden.logout(scope)
24
- throw :warden, :scope => scope, :message => :session_limited
24
+ throw :warden, scope: scope, message: :session_limited
25
25
  end
26
26
  end
27
27
  end
@@ -0,0 +1,22 @@
1
+ module Devise
2
+ module Models
3
+ module Compatibility
4
+ extend ActiveSupport::Concern
5
+
6
+ # for backwards compatibility with Rails < 5.1.x
7
+ unless Devise.activerecord51?
8
+ def saved_change_to_encrypted_password?
9
+ encrypted_password_changed?
10
+ end
11
+
12
+ def encrypted_password_before_last_save
13
+ previous_changes['encrypted_password'].try(:first)
14
+ end
15
+
16
+ def will_save_change_to_encrypted_password?
17
+ changed_attributes['encrypted_password'].present?
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,7 +2,7 @@ require 'devise-security/hooks/expirable'
2
2
 
3
3
  module Devise
4
4
  module Models
5
- # Deactivate the account after a configurable amount of time. To be able to
5
+ # Deactivate the account after a configurable amount of time. To be able to
6
6
  # tell, it tracks activity about your account with the following columns:
7
7
  #
8
8
  # * last_activity_at - A timestamp updated when the user requests a page (only signed in)
@@ -11,10 +11,10 @@ module Devise
11
11
  # +:expire_after+ - Time interval to expire accounts after
12
12
  #
13
13
  # == Additions
14
- # Best used with two cron jobs. One for expiring accounts after inactivity,
15
- # and another, that deletes accounts, which have expired for a given amount
14
+ # Best used with two cron jobs. One for expiring accounts after inactivity,
15
+ # and another, that deletes accounts, which have expired for a given amount
16
16
  # of time (for example 90 days).
17
- #
17
+ #
18
18
  module Expirable
19
19
  extend ActiveSupport::Concern
20
20
 
@@ -37,13 +37,13 @@ module Devise
37
37
 
38
38
  # Expire an account. This is for cron jobs and manually expiring of accounts.
39
39
  #
40
- # @example
40
+ # @example
41
41
  # User.expire!
42
42
  # User.expire! 1.week.from_now
43
43
  # @note +expired_at+ can be in the future as well
44
44
  def expire!(at = Time.now.utc)
45
45
  self.expired_at = at
46
- save(:validate => false)
46
+ save(validate: false)
47
47
  end
48
48
 
49
49
  # Overwrites active_for_authentication? from Devise::Models::Activatable
@@ -55,7 +55,7 @@ module Devise
55
55
  super && !self.expired?
56
56
  end
57
57
 
58
- # The message sym, if {#active_for_authentication?} returns +false+. E.g. needed
58
+ # The message sym, if {#active_for_authentication?} returns +false+. E.g. needed
59
59
  # for i18n.
60
60
  def inactive_message
61
61
  !self.expired? ? super : :expired
@@ -82,17 +82,17 @@ module Devise
82
82
  where('expired_at < ?', time.seconds.ago)
83
83
  end
84
84
 
85
- # Sample method for daily cron to delete all expired entries after a
85
+ # Sample method for daily cron to delete all expired entries after a
86
86
  # given amount of +time+.
87
87
  #
88
- # In your overwritten method you can "blank out" the object instead of
88
+ # In your overwritten method you can "blank out" the object instead of
89
89
  # deleting it.
90
90
  #
91
91
  # *Word of warning*: You have to handle the dependent method
92
- # on the +resource+ relations (+:destroy+ or +:nullify+) and catch this
92
+ # on the +resource+ relations (+:destroy+ or +:nullify+) and catch this
93
93
  # behavior (see http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Deleting+from+associations).
94
94
  #
95
- # @example
95
+ # @example
96
96
  # Resource.delete_all_expired_for 90.days
97
97
  # @example You can overide this in your +resource+ model
98
98
  # def self.delete_all_expired_for(time = 90.days)
@@ -108,7 +108,7 @@ module Devise
108
108
  expired_for(time).delete_all
109
109
  end
110
110
 
111
- # Version of {#delete_all_expired_for} without arguments (uses
111
+ # Version of {#delete_all_expired_for} without arguments (uses
112
112
  # configured +delete_expired_after+ default value).
113
113
  # @see #delete_all_expired_for
114
114
  def delete_all_expired
@@ -117,4 +117,4 @@ module Devise
117
117
  end
118
118
  end
119
119
  end
120
- end
120
+ end
@@ -1,4 +1,4 @@
1
1
  require 'active_record'
2
2
  class OldPassword < ActiveRecord::Base
3
- belongs_to :password_archivable, :polymorphic => true
3
+ belongs_to :password_archivable, polymorphic: true
4
4
  end
@@ -17,7 +17,9 @@ module Devise
17
17
  generate_paranoid_code
18
18
  elsif code == paranoid_verification_code
19
19
  attempt = 0
20
- update_without_password paranoid_verification_code: nil, paranoid_verified_at: Time.now, paranoid_verification_attempt: attempt
20
+ update_without_password paranoid_verification_code: nil,
21
+ paranoid_verified_at: Time.now,
22
+ paranoid_verification_attempt: attempt
21
23
  else
22
24
  update_without_password paranoid_verification_attempt: attempt
23
25
  end
@@ -28,7 +30,8 @@ module Devise
28
30
  end
29
31
 
30
32
  def generate_paranoid_code
31
- update_without_password paranoid_verification_code: Devise.verification_code_generator.call(), paranoid_verification_attempt: 0
33
+ update_without_password paranoid_verification_code: Devise.verification_code_generator.call(),
34
+ paranoid_verification_attempt: 0
32
35
  end
33
36
  end
34
37
  end
@@ -1,45 +1,47 @@
1
+ require_relative 'compatibility'
2
+
1
3
  module Devise
2
4
  module Models
3
- # PasswordArchivable
5
+ # PasswordArchivable, this depends on the DatabaseAuthenticatable module from devise
4
6
  module PasswordArchivable
5
7
  extend ActiveSupport::Concern
8
+ include Devise::Models::Compatibility
9
+ include Devise::Models::DatabaseAuthenticatable
6
10
 
7
11
  included do
8
12
  has_many :old_passwords, as: :password_archivable, dependent: :destroy
9
- before_update :archive_password
10
- validate :validate_password_archive
13
+ before_update :archive_password, if: :will_save_change_to_encrypted_password?
14
+ validate :validate_password_archive, if: :password_present?
11
15
  end
12
16
 
17
+ delegate :present?, to: :password, prefix: true
18
+
13
19
  def validate_password_archive
14
- errors.add(:password, :taken_in_past) if encrypted_password_changed? && password_archive_included?
20
+ errors.add(:password, :taken_in_past) if will_save_change_to_encrypted_password? && password_archive_included?
15
21
  end
16
22
 
17
- # validate is the password used in the past
18
- def password_archive_included?
19
- unless deny_old_passwords.is_a? 1.class
20
- if deny_old_passwords.is_a?(TrueClass) && archive_count > 0
21
- self.deny_old_passwords = archive_count
22
- else
23
- self.deny_old_passwords = 0
24
- end
25
- end
26
-
27
- if self.class.deny_old_passwords > 0 && !self.password.nil?
28
- old_passwords_including_cur_change = self.old_passwords.order(:id).reverse_order.limit(self.class.deny_old_passwords).to_a
29
- old_passwords_including_cur_change << OldPassword.new(old_password_params) # include most recent change in list, but don't save it yet!
30
- old_passwords_including_cur_change.each do |old_password|
31
- dummy = self.class.new
32
- dummy.encrypted_password = old_password.encrypted_password
33
- return true if dummy.valid_password?(password)
34
- end
23
+ # @return [Integer] max number of old passwords to store and check
24
+ def max_old_passwords
25
+ case deny_old_passwords
26
+ when true
27
+ [1, archive_count].max
28
+ when false
29
+ 0
30
+ else
31
+ deny_old_passwords.to_i
35
32
  end
36
-
37
- false
38
33
  end
39
34
 
40
- def password_changed_to_same?
41
- pass_change = encrypted_password_change
42
- pass_change && pass_change.first == pass_change.last
35
+ # validate is the password used in the past
36
+ # @return [true] if current password was used previously
37
+ # @return [false] if disabled or not previously used
38
+ def password_archive_included?
39
+ return false unless max_old_passwords > 0
40
+ old_passwords_including_cur_change = old_passwords.order(:id).reverse_order.limit(max_old_passwords).pluck(:encrypted_password)
41
+ old_passwords_including_cur_change << encrypted_password_was # include most recent change in list, but don't save it yet!
42
+ old_passwords_including_cur_change.any? do |old_password|
43
+ self.class.new(encrypted_password: old_password).valid_password?(password)
44
+ end
43
45
  end
44
46
 
45
47
  def deny_old_passwords
@@ -58,20 +60,14 @@ module Devise
58
60
 
59
61
  # archive the last password before save and delete all to old passwords from archive
60
62
  def archive_password
61
- if encrypted_password_changed?
62
- if archive_count.to_i > 0
63
- old_passwords.create! old_password_params
64
- old_passwords.order(:id).reverse_order.offset(archive_count).destroy_all
65
- else
66
- old_passwords.destroy_all
67
- end
63
+ if max_old_passwords > 0
64
+ old_passwords.create!(encrypted_password: encrypted_password_was) if encrypted_password_was.present?
65
+ old_passwords.order(:id).reverse_order.offset(max_old_passwords).destroy_all
66
+ else
67
+ old_passwords.destroy_all
68
68
  end
69
69
  end
70
70
 
71
- def old_password_params
72
- { encrypted_password: encrypted_password_change.first }
73
- end
74
-
75
71
  module ClassMethods
76
72
  ::Devise::Models.config(self, :password_archiving_count, :deny_old_passwords)
77
73
  end
@@ -24,7 +24,7 @@ module Devise
24
24
  def need_change_password!
25
25
  if expired_password_after_numeric?
26
26
  need_change_password
27
- self.save(:validate => false)
27
+ self.save(validate: false)
28
28
  end
29
29
  end
30
30
 
@@ -1,3 +1,5 @@
1
+ require_relative 'compatibility'
2
+
1
3
  module Devise
2
4
  module Models
3
5
  # SecureValidatable creates better validations with more validation for security
@@ -11,6 +13,7 @@ module Devise
11
13
  # * +password_regex+: need strong password. Defaults to /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/
12
14
  #
13
15
  module SecureValidatable
16
+ include Devise::Models::Compatibility
14
17
 
15
18
  def self.included(base)
16
19
  base.extend ClassMethods
@@ -23,27 +26,27 @@ module Devise
23
26
  unless has_uniqueness_validation_of_login?
24
27
  validation_condition = "#{login_attribute}_changed?".to_sym
25
28
 
26
- validates login_attribute, :uniqueness => {
27
- :scope => authentication_keys[1..-1],
28
- :case_sensitive => !!case_insensitive_keys
29
+ validates login_attribute, uniqueness: {
30
+ scope: authentication_keys[1..-1],
31
+ case_sensitive: !!case_insensitive_keys
29
32
  },
30
- :if => validation_condition
33
+ if: validation_condition
31
34
 
32
35
  already_validated_email = login_attribute.to_s == 'email'
33
36
  end
34
37
 
35
38
  unless devise_validation_enabled?
36
- validates :email, :presence => true, :if => :email_required?
39
+ validates :email, presence: true, if: :email_required?
37
40
  unless already_validated_email
38
- validates :email, :uniqueness => true, :allow_blank => true, :if => :email_changed? # check uniq for email ever
41
+ validates :email, uniqueness: true, allow_blank: true, if: :email_changed? # check uniq for email ever
39
42
  end
40
43
 
41
- validates :password, :presence => true, :length => password_length, :confirmation => true, :if => :password_required?
44
+ validates :password, presence: true, length: password_length, confirmation: true, if: :password_required?
42
45
  end
43
46
 
44
47
  # extra validations
45
- validates :email, :email => email_validation if email_validation # use rails_email_validator or similar
46
- validates :password, :format => { :with => password_regex, :message => :password_format }, :if => :password_required?
48
+ validates :email, email: email_validation if email_validation # use rails_email_validator or similar
49
+ validates :password, format: { with: password_regex, message: :password_format }, if: :password_required?
47
50
 
48
51
  # don't allow use same password
49
52
  validate :current_equal_password_validation
@@ -55,12 +58,11 @@ module Devise
55
58
  end
56
59
 
57
60
  def current_equal_password_validation
58
- if !self.new_record? && !self.encrypted_password_change.nil?
59
- dummy = self.class.new
60
- dummy.encrypted_password = self.encrypted_password_change.first
61
- dummy.password_salt = self.password_salt_change.first if self.respond_to?(:password_salt_change) && !self.password_salt_change.nil?
62
- self.errors.add(:password, :equal_to_current_password) if dummy.valid_password?(self.password)
61
+ return if new_record? || !will_save_change_to_encrypted_password? || password.blank?
62
+ dummy = self.class.new(encrypted_password: encrypted_password_was).tap do |user|
63
+ user.password_salt = password_salt_was if respond_to?(:password_salt)
63
64
  end
65
+ self.errors.add(:password, :equal_to_current_password) if dummy.valid_password?(password)
64
66
  end
65
67
 
66
68
  protected
@@ -12,7 +12,6 @@ module Devise
12
12
  # f.text_field :security_question_answer
13
13
  module SecurityQuestionable
14
14
  extend ActiveSupport::Concern
15
-
16
15
  end
17
16
  end
18
- end
17
+ end
@@ -5,7 +5,7 @@ module Devise
5
5
  # SessionLimited ensures, that there is only one session usable per account at once.
6
6
  # If someone logs in, and some other is logging in with the same credentials,
7
7
  # the session from the first one is invalidated and not usable anymore.
8
- # The first one is redirected to the sign page with a message, telling that
8
+ # The first one is redirected to the sign page with a message, telling that
9
9
  # someone used his credentials to sign in.
10
10
  module SessionLimitable
11
11
  extend ActiveSupport::Concern
@@ -13,9 +13,9 @@ module Devise
13
13
  def update_unique_session_id!(unique_session_id)
14
14
  self.unique_session_id = unique_session_id
15
15
 
16
- save(:validate => false)
16
+ save(validate: false)
17
17
  end
18
18
 
19
19
  end
20
20
  end
21
- end
21
+ end
@@ -9,12 +9,10 @@ module DeviseSecurity
9
9
  module ActiveRecord
10
10
  module Schema
11
11
  include DeviseSecurity::Schema
12
-
13
-
14
12
  end
15
13
  end
16
14
  end
17
15
  end
18
16
 
19
17
  ActiveRecord::ConnectionAdapters::Table.send :include, DeviseSecurity::Orm::ActiveRecord::Schema
20
- ActiveRecord::ConnectionAdapters::TableDefinition.send :include, DeviseSecurity::Orm::ActiveRecord::Schema
18
+ ActiveRecord::ConnectionAdapters::TableDefinition.send :include, DeviseSecurity::Orm::ActiveRecord::Schema
@@ -7,13 +7,13 @@ module DeviseSecurity::Patches
7
7
  self.resource = resource_class.send_confirmation_instructions(params[resource_name])
8
8
 
9
9
  if successfully_sent?(resource)
10
- respond_with({}, :location => after_resending_confirmation_instructions_path_for(resource_name))
10
+ respond_with({}, location: after_resending_confirmation_instructions_path_for(resource_name))
11
11
  else
12
12
  respond_with(resource)
13
13
  end
14
14
  else
15
15
  flash[:alert] = t('devise.invalid_captcha') if is_navigational_format?
16
- respond_with({}, :location => new_confirmation_path(resource_name))
16
+ respond_with({}, location: new_confirmation_path(resource_name))
17
17
  end
18
18
  end
19
19
  end