devise-security 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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