devise-security 0.16.0 → 0.18.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.
- checksums.yaml +4 -4
- data/LICENSE.txt +3 -1
- data/README.md +18 -7
- data/app/controllers/devise/paranoid_verification_code_controller.rb +26 -12
- data/app/controllers/devise/password_expired_controller.rb +22 -5
- data/config/locales/bg.yml +42 -0
- data/config/locales/by.yml +1 -0
- data/config/locales/cs.yml +5 -0
- data/config/locales/de.yml +3 -0
- data/config/locales/en.yml +2 -1
- data/config/locales/es.yml +12 -0
- data/config/locales/fa.yml +1 -0
- data/config/locales/fr.yml +14 -2
- data/config/locales/hi.yml +1 -0
- data/config/locales/it.yml +1 -0
- data/config/locales/ja.yml +12 -0
- data/config/locales/nl.yml +1 -0
- data/config/locales/pt.yml +1 -0
- data/config/locales/ru.yml +1 -0
- data/config/locales/tr.yml +25 -1
- data/config/locales/uk.yml +1 -0
- data/config/locales/zh_CN.yml +1 -0
- data/config/locales/zh_TW.yml +1 -0
- data/lib/devise-security/controllers/helpers.rb +23 -11
- data/lib/devise-security/hooks/expirable.rb +3 -3
- data/lib/devise-security/hooks/paranoid_verification.rb +1 -3
- data/lib/devise-security/hooks/password_expirable.rb +1 -3
- data/lib/devise-security/hooks/session_limitable.rb +4 -4
- data/lib/devise-security/models/compatibility/active_record_patch.rb +4 -3
- data/lib/devise-security/models/compatibility/mongoid_patch.rb +3 -2
- data/lib/devise-security/models/database_authenticatable_patch.rb +18 -10
- data/lib/devise-security/models/expirable.rb +6 -5
- data/lib/devise-security/models/paranoid_verification.rb +2 -2
- data/lib/devise-security/models/password_archivable.rb +3 -3
- data/lib/devise-security/models/secure_validatable.rb +57 -20
- data/lib/devise-security/orm/mongoid.rb +1 -1
- data/lib/devise-security/patches.rb +14 -8
- data/lib/devise-security/routes.rb +2 -3
- data/lib/devise-security/validators/password_complexity_validator.rb +53 -26
- data/lib/devise-security/version.rb +1 -1
- data/lib/devise-security.rb +9 -3
- data/lib/generators/devise_security/install_generator.rb +3 -5
- data/lib/generators/templates/devise_security.rb +6 -1
- data/test/controllers/test_paranoid_verification_code_controller.rb +133 -0
- data/test/controllers/test_password_expired_controller.rb +87 -33
- data/test/controllers/test_security_question_controller.rb +25 -19
- data/test/dummy/app/controllers/overrides/paranoid_verification_code_controller.rb +7 -0
- data/test/dummy/app/controllers/overrides/password_expired_controller.rb +17 -0
- data/test/dummy/app/controllers/widgets_controller.rb +3 -0
- data/test/dummy/app/models/application_user_record.rb +2 -1
- data/test/dummy/app/models/mongoid/confirmable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +4 -3
- data/test/dummy/app/models/mongoid/expirable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/lockable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/mappings.rb +4 -2
- data/test/dummy/app/models/mongoid/omniauthable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/password_archivable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/password_expirable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/recoverable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/registerable_fields.rb +4 -2
- data/test/dummy/app/models/mongoid/rememberable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/security_questionable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/session_limitable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/timeoutable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/trackable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/validatable_fields.rb +2 -0
- data/test/dummy/app/models/paranoid_verification_user.rb +26 -0
- data/test/dummy/app/models/password_expired_user.rb +26 -0
- data/test/dummy/app/models/user.rb +5 -5
- data/test/dummy/app/models/widget.rb +1 -3
- data/test/dummy/app/mongoid/one_user.rb +5 -5
- data/test/dummy/app/mongoid/user_on_engine.rb +2 -2
- data/test/dummy/app/mongoid/user_on_main_app.rb +2 -2
- data/test/dummy/app/mongoid/user_with_validations.rb +3 -3
- data/test/dummy/app/mongoid/user_without_email.rb +7 -4
- data/test/dummy/config/application.rb +3 -7
- data/test/dummy/config/boot.rb +1 -1
- data/test/dummy/config/environment.rb +1 -1
- data/test/dummy/config/environments/test.rb +1 -0
- data/test/dummy/config/initializers/devise.rb +1 -5
- data/test/dummy/config/locales/en.yml +10 -0
- data/test/dummy/config/routes.rb +3 -1
- data/test/dummy/config.ru +1 -1
- data/test/dummy/db/migrate/20120508165529_create_tables.rb +5 -5
- data/test/dummy/lib/shared_expirable_columns.rb +1 -0
- data/test/dummy/lib/shared_security_questions_fields.rb +1 -0
- data/test/dummy/lib/shared_user.rb +17 -6
- data/test/dummy/lib/shared_user_without_omniauth.rb +12 -3
- data/test/dummy/lib/shared_verification_fields.rb +1 -0
- data/test/dummy/log/test.log +39637 -16086
- data/test/i18n_test.rb +22 -0
- data/test/integration/test_paranoid_verification_code_workflow.rb +53 -0
- data/test/integration/test_password_expirable_workflow.rb +2 -2
- data/test/integration/test_session_limitable_workflow.rb +5 -3
- data/test/orm/active_record.rb +7 -7
- data/test/support/integration_helpers.rb +18 -12
- data/test/test_compatibility.rb +2 -0
- data/test/test_complexity_validator.rb +247 -37
- data/test/test_database_authenticatable_patch.rb +146 -0
- data/test/test_helper.rb +7 -8
- data/test/test_install_generator.rb +1 -1
- data/test/test_paranoid_verification.rb +8 -9
- data/test/test_password_archivable.rb +34 -11
- data/test/test_password_expirable.rb +27 -27
- data/test/test_secure_validatable.rb +265 -107
- data/test/test_secure_validatable_overrides.rb +185 -0
- data/test/test_session_limitable.rb +9 -9
- data/test/tmp/config/initializers/{devise-security.rb → devise_security.rb} +6 -1
- data/test/tmp/config/locales/devise.security_extension.by.yml +1 -0
- data/test/tmp/config/locales/devise.security_extension.cs.yml +5 -0
- data/test/tmp/config/locales/devise.security_extension.de.yml +3 -0
- data/test/tmp/config/locales/devise.security_extension.en.yml +2 -1
- data/test/tmp/config/locales/devise.security_extension.es.yml +12 -0
- data/test/tmp/config/locales/devise.security_extension.fa.yml +1 -0
- data/test/tmp/config/locales/devise.security_extension.fr.yml +14 -2
- data/test/tmp/config/locales/devise.security_extension.hi.yml +21 -20
- data/test/tmp/config/locales/devise.security_extension.it.yml +1 -0
- data/test/tmp/config/locales/devise.security_extension.ja.yml +12 -0
- data/test/tmp/config/locales/devise.security_extension.nl.yml +1 -0
- data/test/tmp/config/locales/devise.security_extension.pt.yml +1 -0
- data/test/tmp/config/locales/devise.security_extension.ru.yml +1 -0
- data/test/tmp/config/locales/devise.security_extension.tr.yml +25 -1
- data/test/tmp/config/locales/devise.security_extension.uk.yml +1 -0
- data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +1 -0
- data/test/tmp/config/locales/devise.security_extension.zh_TW.yml +1 -0
- metadata +82 -41
- data/lib/devise-security/patches/confirmations_controller_captcha.rb +0 -23
- data/lib/devise-security/patches/confirmations_controller_security_question.rb +0 -26
- data/lib/devise-security/patches/passwords_controller_captcha.rb +0 -22
- data/lib/devise-security/patches/passwords_controller_security_question.rb +0 -25
- data/lib/devise-security/patches/registrations_controller_captcha.rb +0 -35
- data/lib/devise-security/patches/sessions_controller_captcha.rb +0 -26
- data/lib/devise-security/patches/unlocks_controller_captcha.rb +0 -22
- data/lib/devise-security/patches/unlocks_controller_security_question.rb +0 -25
- data/test/dummy/app/controllers/foos_controller.rb +0 -0
- data/test/dummy/app/models/secure_user.rb +0 -9
- data/test/dummy/lib/shared_user_without_email.rb +0 -28
- data/test/dummy/log/development.log +0 -883
@@ -8,13 +8,13 @@ Warden::Manager.after_set_user except: :fetch do |record, warden, options|
|
|
8
8
|
warden.authenticated?(options[:scope]) &&
|
9
9
|
!record.skip_session_limitable?
|
10
10
|
|
11
|
-
|
11
|
+
if !options[:skip_session_limitable]
|
12
12
|
unique_session_id = Devise.friendly_token
|
13
13
|
warden.session(options[:scope])['unique_session_id'] = unique_session_id
|
14
14
|
record.update_unique_session_id!(unique_session_id)
|
15
|
-
|
15
|
+
else
|
16
16
|
warden.session(options[:scope])['devise.skip_session_limitable'] = true
|
17
|
-
|
17
|
+
end
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -29,7 +29,7 @@ Warden::Manager.after_set_user only: :fetch do |record, warden, options|
|
|
29
29
|
warden.authenticated?(scope) &&
|
30
30
|
options[:store] != false
|
31
31
|
if record.unique_session_id != warden.session(scope)['unique_session_id'] &&
|
32
|
-
!record.skip_session_limitable? &&
|
32
|
+
!record.skip_session_limitable? &&
|
33
33
|
!warden.session(scope)['devise.skip_session_limitable']
|
34
34
|
Rails.logger.warn do
|
35
35
|
'[devise-security][session_limitable] session id mismatch: '\
|
@@ -1,12 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Devise
|
2
4
|
module Models
|
3
5
|
module Compatibility
|
4
|
-
|
5
6
|
class NotPersistedError < ActiveRecord::ActiveRecordError; end
|
6
7
|
|
7
8
|
module ActiveRecordPatch
|
8
9
|
extend ActiveSupport::Concern
|
9
|
-
|
10
|
+
|
11
|
+
unless defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new("5.1.x")
|
10
12
|
# When the record was saved, was the +encrypted_password+ changed?
|
11
13
|
# @return [Boolean]
|
12
14
|
def saved_change_to_encrypted_password?
|
@@ -33,7 +35,6 @@ module Devise
|
|
33
35
|
def update_attribute_without_validatons_or_callbacks(name, value)
|
34
36
|
update_column(name, value)
|
35
37
|
end
|
36
|
-
|
37
38
|
end
|
38
39
|
end
|
39
40
|
end
|
@@ -1,7 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Devise
|
2
4
|
module Models
|
3
5
|
module Compatibility
|
4
|
-
|
5
6
|
class NotPersistedError < Mongoid::Errors::MongoidError; end
|
6
7
|
|
7
8
|
module MongoidPatch
|
@@ -23,7 +24,7 @@ module Devise
|
|
23
24
|
# @param name [Symbol] attribute to update
|
24
25
|
# @param value [String] value to set
|
25
26
|
def update_attribute_without_validatons_or_callbacks(name, value)
|
26
|
-
set(Hash[
|
27
|
+
set(Hash[name, value])
|
27
28
|
end
|
28
29
|
end
|
29
30
|
end
|
@@ -5,20 +5,28 @@ module Devise
|
|
5
5
|
module DatabaseAuthenticatablePatch
|
6
6
|
def update_with_password(params, *options)
|
7
7
|
current_password = params.delete(:current_password)
|
8
|
+
valid_password = valid_password?(current_password)
|
8
9
|
|
9
10
|
new_password = params[:password]
|
10
11
|
new_password_confirmation = params[:password_confirmation]
|
11
12
|
|
12
|
-
result = if valid_password
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
13
|
+
result = if valid_password && new_password.present? && new_password_confirmation.present?
|
14
|
+
update(params, *options)
|
15
|
+
else
|
16
|
+
assign_attributes(params, *options)
|
17
|
+
|
18
|
+
if current_password.blank?
|
19
|
+
errors.add(:current_password, :blank)
|
20
|
+
elsif !valid_password
|
21
|
+
errors.add(:current_password, :invalid)
|
22
|
+
end
|
23
|
+
|
24
|
+
errors.add(:password, :blank) if new_password.blank?
|
25
|
+
|
26
|
+
errors.add(:password_confirmation, :blank) if new_password_confirmation.blank?
|
27
|
+
|
28
|
+
false
|
29
|
+
end
|
22
30
|
|
23
31
|
clean_up_passwords
|
24
32
|
result
|
@@ -34,9 +34,11 @@ module Devise
|
|
34
34
|
# @return [bool]
|
35
35
|
def expired?
|
36
36
|
# expired_at set (manually, via cron, etc.)
|
37
|
-
return
|
37
|
+
return expired_at < Time.now.utc unless expired_at.nil?
|
38
|
+
|
38
39
|
# if it is not set, check the last activity against configured expire_after time range
|
39
|
-
return
|
40
|
+
return last_activity_at < self.class.expire_after.ago unless last_activity_at.nil?
|
41
|
+
|
40
42
|
# if last_activity_at is nil as well, the user has to be 'fresh' and is therefore not expired
|
41
43
|
false
|
42
44
|
end
|
@@ -58,13 +60,13 @@ module Devise
|
|
58
60
|
#
|
59
61
|
# @return [bool]
|
60
62
|
def active_for_authentication?
|
61
|
-
super && !
|
63
|
+
super && !expired?
|
62
64
|
end
|
63
65
|
|
64
66
|
# The message sym, if {#active_for_authentication?} returns +false+. E.g. needed
|
65
67
|
# for i18n.
|
66
68
|
def inactive_message
|
67
|
-
!
|
69
|
+
!expired? ? super : :expired
|
68
70
|
end
|
69
71
|
|
70
72
|
module ClassMethods
|
@@ -80,7 +82,6 @@ module Devise
|
|
80
82
|
all.each do |u|
|
81
83
|
u.expire! if u.expired? && u.expired_at.nil?
|
82
84
|
end
|
83
|
-
return
|
84
85
|
end
|
85
86
|
|
86
87
|
# Scope method to collect all expired users since +time+ ago
|
@@ -20,7 +20,7 @@ module Devise
|
|
20
20
|
elsif code == paranoid_verification_code
|
21
21
|
attempt = 0
|
22
22
|
update_without_password paranoid_verification_code: nil,
|
23
|
-
paranoid_verified_at: Time.now,
|
23
|
+
paranoid_verified_at: Time.zone.now,
|
24
24
|
paranoid_verification_attempt: attempt
|
25
25
|
else
|
26
26
|
update_without_password paranoid_verification_attempt: attempt
|
@@ -32,7 +32,7 @@ module Devise
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def generate_paranoid_code
|
35
|
-
update_without_password paranoid_verification_code: Devise.verification_code_generator.call
|
35
|
+
update_without_password paranoid_verification_code: Devise.verification_code_generator.call,
|
36
36
|
paranoid_verification_attempt: 0
|
37
37
|
end
|
38
38
|
end
|
@@ -35,13 +35,13 @@ module Devise
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
-
# validate
|
38
|
+
# validate if the password was used in the past
|
39
39
|
# @return [true] if current password was used previously
|
40
40
|
# @return [false] if disabled or not previously used
|
41
41
|
def password_archive_included?
|
42
42
|
return false unless max_old_passwords.positive?
|
43
43
|
|
44
|
-
old_passwords_including_cur_change = old_passwords.
|
44
|
+
old_passwords_including_cur_change = old_passwords.reorder(created_at: :desc).limit(max_old_passwords).pluck(:encrypted_password)
|
45
45
|
old_passwords_including_cur_change << encrypted_password_was # include most recent change in list, but don't save it yet!
|
46
46
|
old_passwords_including_cur_change.any? do |old_password|
|
47
47
|
# NOTE: we deliberately do not do mass assignment here so that users that
|
@@ -73,7 +73,7 @@ module Devise
|
|
73
73
|
return true if old_passwords.where(encrypted_password: encrypted_password_was).exists?
|
74
74
|
|
75
75
|
old_passwords.create!(encrypted_password: encrypted_password_was) if encrypted_password_was.present?
|
76
|
-
old_passwords.
|
76
|
+
old_passwords.reorder(created_at: :desc).offset(max_old_passwords).destroy_all
|
77
77
|
else
|
78
78
|
old_passwords.destroy_all
|
79
79
|
end
|
@@ -26,7 +26,7 @@ module Devise
|
|
26
26
|
already_validated_email = false
|
27
27
|
|
28
28
|
# validate login in a strict way if not yet validated
|
29
|
-
unless
|
29
|
+
unless uniqueness_validation_of_login?
|
30
30
|
validation_condition = "#{login_attribute}_changed?".to_sym
|
31
31
|
|
32
32
|
validates login_attribute, uniqueness: {
|
@@ -44,20 +44,39 @@ module Devise
|
|
44
44
|
validates :email, uniqueness: true, allow_blank: true, if: :email_changed? # check uniq for email ever
|
45
45
|
end
|
46
46
|
|
47
|
-
|
47
|
+
validates_presence_of :password, if: :password_required?
|
48
|
+
validates_confirmation_of :password, if: :password_required?
|
49
|
+
|
50
|
+
validate if: :password_required? do |record|
|
51
|
+
validates_with ActiveModel::Validations::LengthValidator,
|
52
|
+
attributes: :password,
|
53
|
+
allow_blank: true,
|
54
|
+
in: record.password_length
|
55
|
+
end
|
48
56
|
end
|
49
57
|
|
50
58
|
# extra validations
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
59
|
+
# see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
|
60
|
+
validate do |record|
|
61
|
+
if email_validation
|
62
|
+
validates_with(
|
63
|
+
EmailValidator, { attributes: :email }
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
validate if: :password_required? do |record|
|
69
|
+
validates_with(
|
70
|
+
record.password_complexity_validator.is_a?(Class) ? record.password_complexity_validator : record.password_complexity_validator.classify.constantize,
|
71
|
+
{ attributes: :password }.merge(record.password_complexity)
|
72
|
+
)
|
73
|
+
end
|
55
74
|
|
56
75
|
# don't allow use same password
|
57
76
|
validate :current_equal_password_validation
|
58
77
|
|
59
78
|
# don't allow email to equal password
|
60
|
-
validate :email_not_equal_password_validation
|
79
|
+
validate :email_not_equal_password_validation
|
61
80
|
end
|
62
81
|
end
|
63
82
|
|
@@ -67,21 +86,21 @@ module Devise
|
|
67
86
|
|
68
87
|
def current_equal_password_validation
|
69
88
|
return if new_record? || !will_save_change_to_encrypted_password? || password.blank?
|
89
|
+
|
70
90
|
dummy = self.class.new(encrypted_password: encrypted_password_was).tap do |user|
|
71
91
|
user.password_salt = password_salt_was if respond_to?(:password_salt)
|
72
92
|
end
|
73
|
-
|
93
|
+
errors.add(:password, :equal_to_current_password) if dummy.valid_password?(password)
|
74
94
|
end
|
75
95
|
|
76
96
|
def email_not_equal_password_validation
|
77
|
-
return if
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
self.errors.add(:password, :equal_to_email) if dummy.valid_password?(password.downcase.strip)
|
97
|
+
return if allow_passwords_equal_to_email
|
98
|
+
|
99
|
+
return if password.blank? || email.blank? || (!new_record? && !will_save_change_to_encrypted_password?)
|
100
|
+
|
101
|
+
return unless Devise.secure_compare(password.downcase.strip, email.downcase.strip)
|
102
|
+
|
103
|
+
errors.add(:password, :equal_to_email)
|
85
104
|
end
|
86
105
|
|
87
106
|
protected
|
@@ -89,6 +108,8 @@ module Devise
|
|
89
108
|
# Checks whether a password is needed or not. For validations only.
|
90
109
|
# Passwords are always required if it's a new record, or if the password
|
91
110
|
# or confirmation are being set somewhere.
|
111
|
+
#
|
112
|
+
# @return [Boolean]
|
92
113
|
def password_required?
|
93
114
|
!persisted? || !password.nil? || !password_confirmation.nil?
|
94
115
|
end
|
@@ -97,15 +118,31 @@ module Devise
|
|
97
118
|
true
|
98
119
|
end
|
99
120
|
|
121
|
+
delegate(
|
122
|
+
:allow_passwords_equal_to_email,
|
123
|
+
:email_validation,
|
124
|
+
:password_complexity,
|
125
|
+
:password_complexity_validator,
|
126
|
+
:password_length,
|
127
|
+
to: :class
|
128
|
+
)
|
129
|
+
|
100
130
|
module ClassMethods
|
101
|
-
Devise::Models.config(
|
131
|
+
Devise::Models.config(
|
132
|
+
self,
|
133
|
+
:allow_passwords_equal_to_email,
|
134
|
+
:email_validation,
|
135
|
+
:password_complexity,
|
136
|
+
:password_complexity_validator,
|
137
|
+
:password_length
|
138
|
+
)
|
102
139
|
|
103
140
|
private
|
104
141
|
|
105
|
-
def
|
142
|
+
def uniqueness_validation_of_login?
|
106
143
|
validators.any? do |validator|
|
107
144
|
validator_orm_klass = DEVISE_ORM == :active_record ? ActiveRecord::Validations::UniquenessValidator : ::Mongoid::Validatable::UniquenessValidator
|
108
|
-
validator.
|
145
|
+
validator.is_a?(validator_orm_klass) && validator.attributes.include?(login_attribute)
|
109
146
|
end
|
110
147
|
end
|
111
148
|
|
@@ -114,7 +151,7 @@ module Devise
|
|
114
151
|
end
|
115
152
|
|
116
153
|
def devise_validation_enabled?
|
117
|
-
|
154
|
+
ancestors.map(&:to_s).include? 'Devise::Models::Validatable'
|
118
155
|
end
|
119
156
|
end
|
120
157
|
end
|
@@ -6,18 +6,24 @@ module DeviseSecurity
|
|
6
6
|
autoload :ControllerSecurityQuestion, 'devise-security/patches/controller_security_question'
|
7
7
|
|
8
8
|
class << self
|
9
|
+
# rubocop:disable Metrics/AbcSize
|
10
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
11
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
9
12
|
def apply
|
10
|
-
Devise::PasswordsController.
|
11
|
-
Devise::UnlocksController.
|
12
|
-
Devise::ConfirmationsController.
|
13
|
+
Devise::PasswordsController.include(Patches::ControllerCaptcha) if Devise.captcha_for_recover || Devise.security_question_for_recover
|
14
|
+
Devise::UnlocksController.include(Patches::ControllerCaptcha) if Devise.captcha_for_unlock || Devise.security_question_for_unlock
|
15
|
+
Devise::ConfirmationsController.include(Patches::ControllerCaptcha) if Devise.captcha_for_confirmation
|
13
16
|
|
14
|
-
Devise::PasswordsController.
|
15
|
-
Devise::UnlocksController.
|
16
|
-
Devise::ConfirmationsController.
|
17
|
+
Devise::PasswordsController.include(Patches::ControllerSecurityQuestion) if Devise.security_question_for_recover
|
18
|
+
Devise::UnlocksController.include(Patches::ControllerSecurityQuestion) if Devise.security_question_for_unlock
|
19
|
+
Devise::ConfirmationsController.include(Patches::ControllerSecurityQuestion) if Devise.security_question_for_confirmation
|
17
20
|
|
18
|
-
Devise::RegistrationsController.
|
19
|
-
Devise::SessionsController.
|
21
|
+
Devise::RegistrationsController.include(Patches::ControllerCaptcha) if Devise.captcha_for_sign_up
|
22
|
+
Devise::SessionsController.include(Patches::ControllerCaptcha) if Devise.captcha_for_sign_in
|
20
23
|
end
|
24
|
+
# rubocop:enable Metrics/AbcSize
|
25
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
26
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
21
27
|
end
|
22
28
|
end
|
23
29
|
end
|
@@ -2,17 +2,16 @@
|
|
2
2
|
|
3
3
|
module ActionDispatch::Routing
|
4
4
|
class Mapper
|
5
|
-
|
6
5
|
protected
|
7
6
|
|
8
7
|
# route for handle expired passwords
|
9
8
|
def devise_password_expired(mapping, controllers)
|
10
|
-
resource :password_expired, only: [
|
9
|
+
resource :password_expired, only: %i[show update], path: mapping.path_names[:password_expired], controller: controllers[:password_expired]
|
11
10
|
end
|
12
11
|
|
13
12
|
# route for handle paranoid verification
|
14
13
|
def devise_verification_code(mapping, controllers)
|
15
|
-
resource :paranoid_verification_code, only: [
|
14
|
+
resource :paranoid_verification_code, only: %i[show update], path: mapping.path_names[:verification_code], controller: controllers[:paranoid_verification_code]
|
16
15
|
end
|
17
16
|
end
|
18
17
|
end
|
@@ -1,35 +1,62 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# (NIST)[https://pages.nist.gov/800-63-3/sp800-63b.html#appA] does not recommend
|
4
|
+
# the use of a password complexity checks because...
|
5
|
+
#
|
6
|
+
# > Length and complexity requirements beyond those recommended here
|
7
|
+
# > significantly increase the difficulty of memorized secrets and increase user
|
8
|
+
# > frustration. As a result, users often work around these restrictions in a
|
9
|
+
# > way that is counterproductive. Furthermore, other mitigations such as
|
10
|
+
# > blacklists, secure hashed storage, and rate limiting are more effective at
|
11
|
+
# > preventing modern brute-force attacks. Therefore, no additional complexity
|
12
|
+
# > requirements are imposed.
|
13
|
+
#
|
4
14
|
# Options:
|
5
|
-
# - digit
|
6
|
-
#
|
7
|
-
# - lower
|
8
|
-
# - symbol
|
9
|
-
#
|
10
|
-
# - upper
|
15
|
+
# - `digit | digits`: minimum number of digits in the validated string. Uses
|
16
|
+
# the `digit` localization key.
|
17
|
+
# - `lower`: minimum number of lower-case letters in the validated string
|
18
|
+
# - `symbol | symbols`: minimum number of punctuation characters or symbols in
|
19
|
+
# the validated string. Uses the `symbol` localization key.
|
20
|
+
# - `upper`: minimum number of upper-case letters in the validated string
|
11
21
|
class DeviseSecurity::PasswordComplexityValidator < ActiveModel::EachValidator
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
22
|
+
# A Hash of the possible valid patterns that can be checked against. The keys
|
23
|
+
# for this Hash are singular symbols corresponding to entries in the
|
24
|
+
# localization files. Override or redefine this method if you want to include
|
25
|
+
# custom patterns (e.g., `letter: /\p{Alpha}/` for all letters).
|
26
|
+
#
|
27
|
+
# @return [Hash<Symbol,Regexp>]
|
28
|
+
def patterns
|
29
|
+
{
|
30
|
+
digit: /\p{Digit}/,
|
31
|
+
lower: /\p{Lower}/,
|
32
|
+
symbol: /\p{Punct}|\p{S}/,
|
33
|
+
upper: /\p{Upper}/
|
34
|
+
}
|
35
|
+
end
|
20
36
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
37
|
+
# Validate the complexity of the password. This validation does not check to
|
38
|
+
# ensure the password is not blank. That is the responsibility of other
|
39
|
+
# validations. This validator will also ignore any patterns that are not
|
40
|
+
# explicitly configured to be used or whose minimum limits are less than 1.
|
41
|
+
#
|
42
|
+
# @param record [ActiveModel::Model]
|
43
|
+
# @param attribute [Symbol]
|
44
|
+
# @param password [String]
|
45
|
+
def validate_each(record, attribute, password)
|
46
|
+
return if password.blank?
|
25
47
|
|
26
|
-
|
27
|
-
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
48
|
+
options.sort.each do |pattern_name, minimum|
|
49
|
+
normalized_option = pattern_name.to_s.singularize.to_sym
|
31
50
|
|
32
|
-
|
33
|
-
|
51
|
+
next unless patterns.key?(normalized_option)
|
52
|
+
next unless minimum.positive?
|
53
|
+
next if password.scan(patterns[normalized_option]).size >= minimum
|
54
|
+
|
55
|
+
record.errors.add(
|
56
|
+
attribute,
|
57
|
+
:"password_complexity.#{normalized_option}",
|
58
|
+
count: minimum
|
59
|
+
)
|
60
|
+
end
|
34
61
|
end
|
35
62
|
end
|
data/lib/devise-security.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
DEVISE_ORM = ENV.fetch('DEVISE_ORM', 'active_record').to_sym unless defined?(DEVISE_ORM)
|
3
4
|
|
4
|
-
require DEVISE_ORM.to_s if DEVISE_ORM.in? [
|
5
|
+
require DEVISE_ORM.to_s if DEVISE_ORM.in? %i[active_record mongoid]
|
5
6
|
require 'active_support/core_ext/integer'
|
6
7
|
require 'active_support/ordered_hash'
|
7
8
|
require 'active_support/concern'
|
@@ -9,15 +10,20 @@ require 'devise'
|
|
9
10
|
|
10
11
|
module Devise
|
11
12
|
# Number of seconds that passwords are valid (e.g 3.months)
|
12
|
-
# Disable
|
13
|
+
# Disable password expiration with +false+
|
13
14
|
# Expire only on demand with +true+
|
14
15
|
mattr_accessor :expire_password_after
|
15
16
|
@@expire_password_after = 3.months
|
16
17
|
|
17
|
-
# Validate password
|
18
|
+
# Validate password complexity
|
18
19
|
mattr_accessor :password_complexity
|
19
20
|
@@password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
|
20
21
|
|
22
|
+
# Define the class used to validate password complexity. Set to a Class or a
|
23
|
+
# string which will be used to determine which class to use.
|
24
|
+
mattr_accessor :password_complexity_validator
|
25
|
+
@@password_complexity_validator = 'devise_security/password_complexity_validator'
|
26
|
+
|
21
27
|
# Number of old passwords in archive
|
22
28
|
mattr_accessor :password_archiving_count
|
23
29
|
@@password_archiving_count = 5
|
@@ -6,20 +6,18 @@ module DeviseSecurity
|
|
6
6
|
class InstallGenerator < Rails::Generators::Base
|
7
7
|
LOCALES = %w[by cs de en es fa fr hi it ja nl pt ru tr uk zh_CN zh_TW].freeze
|
8
8
|
|
9
|
-
source_root File.expand_path('
|
9
|
+
source_root File.expand_path('../templates', __dir__)
|
10
10
|
desc 'Install the devise security extension'
|
11
11
|
|
12
12
|
def copy_initializer
|
13
|
-
template('devise_security.rb',
|
14
|
-
'config/initializers/devise_security.rb',
|
15
|
-
)
|
13
|
+
template('devise_security.rb', 'config/initializers/devise_security.rb')
|
16
14
|
end
|
17
15
|
|
18
16
|
def copy_locales
|
19
17
|
LOCALES.each do |locale|
|
20
18
|
copy_file(
|
21
19
|
"../../../config/locales/#{locale}.yml",
|
22
|
-
"config/locales/devise.security_extension.#{locale}.yml"
|
20
|
+
"config/locales/devise.security_extension.#{locale}.yml"
|
23
21
|
)
|
24
22
|
end
|
25
23
|
end
|
@@ -7,7 +7,9 @@ Devise.setup do |config|
|
|
7
7
|
# Should the password expire (e.g 3.months)
|
8
8
|
# config.expire_password_after = false
|
9
9
|
|
10
|
-
# Need 1 char of A-Z, a-z
|
10
|
+
# Need 1 char each of: A-Z, a-z, 0-9, and a punctuation mark or symbol
|
11
|
+
# You may use "digits" in place of "digit" and "symbols" in place of
|
12
|
+
# "symbol" based on your preference
|
11
13
|
# config.password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
|
12
14
|
|
13
15
|
# How many passwords to keep in archive
|
@@ -44,4 +46,7 @@ Devise.setup do |config|
|
|
44
46
|
|
45
47
|
# Allow password to equal the email
|
46
48
|
# config.allow_passwords_equal_to_email = false
|
49
|
+
|
50
|
+
# paranoid_verification will regenerate verification code after failed attempt
|
51
|
+
# config.paranoid_code_regenerate_after_attempt = 10
|
47
52
|
end
|