devise-security 0.14.2 → 0.17.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/README.md +125 -59
- data/app/controllers/devise/paranoid_verification_code_controller.rb +13 -1
- data/app/controllers/devise/password_expired_controller.rb +24 -6
- data/app/views/devise/paranoid_verification_code/show.html.erb +3 -3
- data/app/views/devise/password_expired/show.html.erb +5 -5
- data/config/locales/bg.yml +41 -0
- data/config/locales/by.yml +49 -0
- data/config/locales/cs.yml +41 -0
- data/config/locales/de.yml +15 -2
- data/config/locales/en.yml +15 -2
- data/config/locales/es.yml +10 -9
- data/config/locales/fa.yml +41 -0
- data/config/locales/fr.yml +1 -0
- data/config/locales/hi.yml +42 -0
- data/config/locales/it.yml +35 -4
- data/config/locales/ja.yml +2 -1
- data/config/locales/nl.yml +41 -0
- data/config/locales/pt.yml +41 -0
- data/config/locales/ru.yml +49 -0
- data/config/locales/tr.yml +1 -0
- data/config/locales/uk.yml +49 -0
- data/config/locales/zh_CN.yml +41 -0
- data/config/locales/zh_TW.yml +41 -0
- data/lib/devise-security/controllers/helpers.rb +59 -50
- data/lib/devise-security/hooks/password_expirable.rb +2 -0
- data/lib/devise-security/hooks/session_limitable.rb +21 -11
- data/lib/devise-security/models/database_authenticatable_patch.rb +15 -5
- data/lib/devise-security/models/password_archivable.rb +2 -2
- data/lib/devise-security/models/password_expirable.rb +5 -1
- data/lib/devise-security/models/secure_validatable.rb +56 -6
- data/lib/devise-security/models/session_limitable.rb +10 -1
- data/lib/devise-security/validators/password_complexity_validator.rb +53 -24
- data/lib/devise-security/version.rb +1 -1
- data/lib/devise-security.rb +13 -5
- data/lib/generators/devise_security/install_generator.rb +3 -3
- data/lib/generators/templates/{devise-security.rb → devise_security.rb} +6 -1
- data/test/controllers/test_paranoid_verification_code_controller.rb +68 -0
- data/test/controllers/test_password_expired_controller.rb +121 -19
- data/test/controllers/test_security_question_controller.rb +16 -40
- data/test/dummy/app/assets/config/manifest.js +3 -0
- data/test/dummy/app/controllers/overrides/paranoid_verification_code_controller.rb +7 -0
- data/test/dummy/app/controllers/overrides/password_expired_controller.rb +7 -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 +1 -2
- 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 +3 -3
- data/test/dummy/config/application.rb +4 -4
- data/test/dummy/config/boot.rb +1 -1
- data/test/dummy/config/environment.rb +1 -1
- data/test/dummy/config/environments/test.rb +3 -13
- data/test/dummy/config/initializers/migration_class.rb +1 -8
- data/test/dummy/config/locales/en.yml +10 -0
- data/test/dummy/config/mongoid.yml +1 -1
- data/test/dummy/config/routes.rb +5 -3
- data/test/dummy/db/migrate/20120508165529_create_tables.rb +3 -3
- 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_email.rb +2 -1
- data/test/dummy/lib/shared_user_without_omniauth.rb +12 -3
- data/test/dummy/lib/shared_verification_fields.rb +1 -0
- data/test/dummy/{app/models/.gitkeep → log/development.log} +0 -0
- data/test/dummy/log/test.log +101533 -0
- data/test/integration/test_password_expirable_workflow.rb +53 -0
- data/test/integration/test_session_limitable_workflow.rb +2 -0
- data/test/orm/active_record.rb +7 -4
- data/test/orm/mongoid.rb +2 -1
- data/test/support/integration_helpers.rb +15 -33
- data/test/support/mongoid.yml +1 -1
- data/test/test_compatibility.rb +2 -0
- data/test/test_complexity_validator.rb +250 -29
- data/test/test_database_authenticatable_patch.rb +146 -0
- data/test/test_helper.rb +12 -6
- data/test/test_install_generator.rb +12 -2
- data/test/test_paranoid_verification.rb +0 -1
- data/test/test_password_archivable.rb +34 -11
- data/test/test_password_expirable.rb +26 -26
- data/test/test_secure_validatable.rb +292 -50
- data/test/test_secure_validatable_overrides.rb +185 -0
- data/test/test_session_limitable.rb +27 -1
- data/test/tmp/config/initializers/devise_security.rb +49 -0
- data/test/tmp/config/locales/devise.security_extension.by.yml +49 -0
- data/test/tmp/config/locales/devise.security_extension.cs.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.de.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.en.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.es.yml +30 -0
- data/test/tmp/config/locales/devise.security_extension.fa.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.fr.yml +30 -0
- data/test/tmp/config/locales/devise.security_extension.hi.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.it.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.ja.yml +30 -0
- data/test/tmp/config/locales/devise.security_extension.nl.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.pt.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.ru.yml +49 -0
- data/test/tmp/config/locales/devise.security_extension.tr.yml +18 -0
- data/test/tmp/config/locales/devise.security_extension.uk.yml +49 -0
- data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.zh_TW.yml +41 -0
- metadata +168 -132
- data/.codeclimate.yml +0 -63
- data/.document +0 -5
- data/.gitignore +0 -43
- data/.mdlrc +0 -1
- data/.rubocop.yml +0 -64
- data/.ruby-version +0 -1
- data/.travis.yml +0 -39
- data/Appraisals +0 -35
- data/Gemfile +0 -10
- data/Rakefile +0 -27
- data/devise-security.gemspec +0 -50
- data/gemfiles/rails_4.2_stable.gemfile +0 -16
- data/gemfiles/rails_5.0_stable.gemfile +0 -15
- data/gemfiles/rails_5.1_stable.gemfile +0 -15
- data/gemfiles/rails_5.2_stable.gemfile +0 -15
- data/gemfiles/rails_6.0_beta.gemfile +0 -15
- data/lib/devise-security/orm/active_record.rb +0 -20
- data/lib/devise-security/schema.rb +0 -66
- data/test/dummy/app/models/secure_user.rb +0 -9
@@ -40,71 +40,80 @@ module DeviseSecurity
|
|
40
40
|
|
41
41
|
# controller instance methods
|
42
42
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
43
|
+
private
|
44
|
+
|
45
|
+
# Called as a `before_action` on all actions on any controller that uses
|
46
|
+
# this helper. If the user's session is marked as having an expired
|
47
|
+
# password we double check in case it has been changed by another process,
|
48
|
+
# then redirect to the password change url.
|
49
|
+
#
|
50
|
+
# @note `Warden::Manager.after_authentication` is run AFTER this method
|
51
|
+
#
|
52
|
+
# @note Once the warden session has `'password_expired'` set to `false`,
|
53
|
+
# it will **never** be checked again until the user re-logs in.
|
54
|
+
def handle_password_change
|
55
|
+
return if warden.nil?
|
56
|
+
|
57
|
+
if !devise_controller? &&
|
58
|
+
!ignore_password_expire? &&
|
59
|
+
!request.format.nil? &&
|
60
|
+
request.format.html?
|
61
|
+
Devise.mappings.keys.flatten.any? do |scope|
|
62
|
+
if signed_in?(scope) && warden.session(scope)['password_expired'] == true
|
63
|
+
if send(:"current_#{scope}").try(:need_change_password?)
|
64
|
+
store_location_for(scope, request.original_fullpath) if request.get?
|
65
|
+
redirect_for_password_change(scope)
|
66
|
+
else
|
67
|
+
warden.session(scope)['password_expired'] = false
|
60
68
|
end
|
61
69
|
end
|
62
70
|
end
|
63
71
|
end
|
72
|
+
end
|
64
73
|
|
65
|
-
|
66
|
-
|
67
|
-
|
74
|
+
# lookup if extra (paranoid) code verification is needed
|
75
|
+
def handle_paranoid_verification
|
76
|
+
return if warden.nil?
|
68
77
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
end
|
78
|
+
if !devise_controller? && !request.format.nil? && request.format.html?
|
79
|
+
Devise.mappings.keys.flatten.any? do |scope|
|
80
|
+
if signed_in?(scope) && warden.session(scope)['paranoid_verify']
|
81
|
+
store_location_for(scope, request.original_fullpath) if request.get?
|
82
|
+
redirect_for_paranoid_verification scope
|
83
|
+
return
|
76
84
|
end
|
77
85
|
end
|
78
86
|
end
|
87
|
+
end
|
79
88
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
89
|
+
# redirect for password update with alert message
|
90
|
+
def redirect_for_password_change(scope)
|
91
|
+
redirect_to change_password_required_path_for(scope), alert: I18n.t('change_required', scope: 'devise.password_expired')
|
92
|
+
end
|
84
93
|
|
85
|
-
|
86
|
-
|
87
|
-
|
94
|
+
def redirect_for_paranoid_verification(scope)
|
95
|
+
redirect_to paranoid_verification_code_path_for(scope), alert: I18n.t('code_required', scope: 'devise.paranoid_verify')
|
96
|
+
end
|
88
97
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
98
|
+
# path for change password
|
99
|
+
def change_password_required_path_for(resource_or_scope = nil)
|
100
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
101
|
+
change_path = "#{scope}_password_expired_path"
|
102
|
+
send(change_path)
|
103
|
+
end
|
95
104
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
105
|
+
def paranoid_verification_code_path_for(resource_or_scope = nil)
|
106
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
107
|
+
change_path = "#{scope}_paranoid_verification_code_path"
|
108
|
+
send(change_path)
|
109
|
+
end
|
101
110
|
|
102
|
-
|
111
|
+
protected
|
103
112
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
113
|
+
# allow to overwrite for some special handlings
|
114
|
+
def ignore_password_expire?
|
115
|
+
false
|
116
|
+
end
|
108
117
|
end
|
109
118
|
end
|
110
119
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# @note This happens after
|
4
|
+
# {DeviseSecurity::Controller::Helpers#handle_password_change}
|
3
5
|
Warden::Manager.after_authentication do |record, warden, options|
|
4
6
|
if record.respond_to?(:need_change_password?)
|
5
7
|
warden.session(options[:scope])['password_expired'] = record.need_change_password?
|
@@ -4,10 +4,17 @@
|
|
4
4
|
# user is explicitly set (with set_user) and on authentication. Retrieving the
|
5
5
|
# user from session (:fetch) does not trigger it.
|
6
6
|
Warden::Manager.after_set_user except: :fetch do |record, warden, options|
|
7
|
-
if record.
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
if record.devise_modules.include?(:session_limitable) &&
|
8
|
+
warden.authenticated?(options[:scope]) &&
|
9
|
+
!record.skip_session_limitable?
|
10
|
+
|
11
|
+
if !options[:skip_session_limitable]
|
12
|
+
unique_session_id = Devise.friendly_token
|
13
|
+
warden.session(options[:scope])['unique_session_id'] = unique_session_id
|
14
|
+
record.update_unique_session_id!(unique_session_id)
|
15
|
+
else
|
16
|
+
warden.session(options[:scope])['devise.skip_session_limitable'] = true
|
17
|
+
end
|
11
18
|
end
|
12
19
|
end
|
13
20
|
|
@@ -17,15 +24,18 @@ end
|
|
17
24
|
# page on the next request.
|
18
25
|
Warden::Manager.after_set_user only: :fetch do |record, warden, options|
|
19
26
|
scope = options[:scope]
|
20
|
-
env = warden.request.env
|
21
27
|
|
22
|
-
if record.
|
23
|
-
|
24
|
-
|
25
|
-
|
28
|
+
if record.devise_modules.include?(:session_limitable) &&
|
29
|
+
warden.authenticated?(scope) &&
|
30
|
+
options[:store] != false
|
31
|
+
if record.unique_session_id != warden.session(scope)['unique_session_id'] &&
|
32
|
+
!record.skip_session_limitable? &&
|
33
|
+
!warden.session(scope)['devise.skip_session_limitable']
|
34
|
+
Rails.logger.warn do
|
35
|
+
'[devise-security][session_limitable] session id mismatch: '\
|
26
36
|
"expected=#{record.unique_session_id.inspect} "\
|
27
|
-
"actual=#{warden.session(scope)['unique_session_id'].inspect}"
|
28
|
-
|
37
|
+
"actual=#{warden.session(scope)['unique_session_id'].inspect}"
|
38
|
+
end
|
29
39
|
warden.raw_session.clear
|
30
40
|
warden.logout(scope)
|
31
41
|
throw :warden, scope: scope, message: :session_limited
|
@@ -5,18 +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
|
+
result = if valid_password && new_password.present? && new_password_confirmation.present?
|
13
14
|
update(params, *options)
|
14
15
|
else
|
15
16
|
self.assign_attributes(params, *options)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
|
18
|
+
if current_password.blank?
|
19
|
+
self.errors.add(:current_password, :blank)
|
20
|
+
elsif !valid_password
|
21
|
+
self.errors.add(:current_password, :invalid)
|
22
|
+
end
|
23
|
+
|
24
|
+
self.errors.add(:password, :blank) if new_password.blank?
|
25
|
+
|
26
|
+
if new_password_confirmation.blank?
|
27
|
+
self.errors.add(:password_confirmation, :blank)
|
28
|
+
end
|
29
|
+
|
20
30
|
false
|
21
31
|
end
|
22
32
|
|
@@ -41,7 +41,7 @@ module Devise
|
|
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
|
@@ -92,7 +92,11 @@ module Devise::Models
|
|
92
92
|
# Update +password_changed_at+ for new records and changed passwords.
|
93
93
|
# @note called as a +before_save+ hook
|
94
94
|
def update_password_changed
|
95
|
-
|
95
|
+
if defined?(will_save_change_to_attribute?)
|
96
|
+
return unless (new_record? || will_save_change_to_encrypted_password?) && !will_save_change_to_password_changed_at?
|
97
|
+
else
|
98
|
+
return unless (new_record? || encrypted_password_changed?) && !password_changed_at_changed?
|
99
|
+
end
|
96
100
|
|
97
101
|
self.password_changed_at = Time.zone.now
|
98
102
|
end
|
@@ -44,17 +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
|
77
|
+
|
78
|
+
# don't allow email to equal password
|
79
|
+
validate :email_not_equal_password_validation
|
58
80
|
end
|
59
81
|
end
|
60
82
|
|
@@ -70,11 +92,23 @@ module Devise
|
|
70
92
|
self.errors.add(:password, :equal_to_current_password) if dummy.valid_password?(password)
|
71
93
|
end
|
72
94
|
|
95
|
+
def email_not_equal_password_validation
|
96
|
+
return if allow_passwords_equal_to_email
|
97
|
+
|
98
|
+
return if password.blank? || email.blank? || (!new_record? && !will_save_change_to_encrypted_password?)
|
99
|
+
|
100
|
+
return unless Devise.secure_compare(password.downcase.strip, email.downcase.strip)
|
101
|
+
|
102
|
+
errors.add(:password, :equal_to_email)
|
103
|
+
end
|
104
|
+
|
73
105
|
protected
|
74
106
|
|
75
107
|
# Checks whether a password is needed or not. For validations only.
|
76
108
|
# Passwords are always required if it's a new record, or if the password
|
77
109
|
# or confirmation are being set somewhere.
|
110
|
+
#
|
111
|
+
# @return [Boolean]
|
78
112
|
def password_required?
|
79
113
|
!persisted? || !password.nil? || !password_confirmation.nil?
|
80
114
|
end
|
@@ -83,8 +117,24 @@ module Devise
|
|
83
117
|
true
|
84
118
|
end
|
85
119
|
|
120
|
+
delegate(
|
121
|
+
:allow_passwords_equal_to_email,
|
122
|
+
:email_validation,
|
123
|
+
:password_complexity,
|
124
|
+
:password_complexity_validator,
|
125
|
+
:password_length,
|
126
|
+
to: :class
|
127
|
+
)
|
128
|
+
|
86
129
|
module ClassMethods
|
87
|
-
Devise::Models.config(
|
130
|
+
Devise::Models.config(
|
131
|
+
self,
|
132
|
+
:allow_passwords_equal_to_email,
|
133
|
+
:email_validation,
|
134
|
+
:password_complexity,
|
135
|
+
:password_complexity_validator,
|
136
|
+
:password_length
|
137
|
+
)
|
88
138
|
|
89
139
|
private
|
90
140
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'compatibility'
|
3
4
|
require 'devise-security/hooks/session_limitable'
|
4
5
|
|
5
6
|
module Devise
|
@@ -11,6 +12,7 @@ module Devise
|
|
11
12
|
# someone used his credentials to sign in.
|
12
13
|
module SessionLimitable
|
13
14
|
extend ActiveSupport::Concern
|
15
|
+
include Devise::Models::Compatibility
|
14
16
|
|
15
17
|
# Update the unique_session_id on the model. This will be checked in
|
16
18
|
# the Warden after_set_user hook in {file:devise-security/hooks/session_limitable}
|
@@ -19,11 +21,18 @@ module Devise
|
|
19
21
|
# @raise [Devise::Models::Compatibility::NotPersistedError] if record is unsaved
|
20
22
|
def update_unique_session_id!(unique_session_id)
|
21
23
|
raise Devise::Models::Compatibility::NotPersistedError, 'cannot update a new record' unless persisted?
|
24
|
+
|
22
25
|
update_attribute_without_validatons_or_callbacks(:unique_session_id, unique_session_id).tap do
|
23
|
-
Rails.logger.debug { "[devise-security][session_limitable] unique_session_id=#{unique_session_id}"}
|
26
|
+
Rails.logger.debug { "[devise-security][session_limitable] unique_session_id=#{unique_session_id}" }
|
24
27
|
end
|
25
28
|
end
|
26
29
|
|
30
|
+
# Should session_limitable be skipped for this instance?
|
31
|
+
# @return [Boolean]
|
32
|
+
# @return [false] by default. This can be overridden by application logic as necessary.
|
33
|
+
def skip_session_limitable?
|
34
|
+
false
|
35
|
+
end
|
27
36
|
end
|
28
37
|
end
|
29
38
|
end
|
@@ -1,33 +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
|
-
# -
|
8
|
-
# -
|
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
|
9
21
|
class DeviseSecurity::PasswordComplexityValidator < ActiveModel::EachValidator
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
18
36
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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?
|
23
47
|
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
48
|
+
options.sort.each do |pattern_name, minimum|
|
49
|
+
normalized_option = pattern_name.to_s.singularize.to_sym
|
29
50
|
|
30
|
-
|
31
|
-
|
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
|
32
61
|
end
|
33
62
|
end
|
data/lib/devise-security.rb
CHANGED
@@ -9,15 +9,20 @@ require 'devise'
|
|
9
9
|
|
10
10
|
module Devise
|
11
11
|
# Number of seconds that passwords are valid (e.g 3.months)
|
12
|
-
# Disable
|
12
|
+
# Disable password expiration with +false+
|
13
13
|
# Expire only on demand with +true+
|
14
14
|
mattr_accessor :expire_password_after
|
15
15
|
@@expire_password_after = 3.months
|
16
16
|
|
17
|
-
# Validate password
|
17
|
+
# Validate password complexity
|
18
18
|
mattr_accessor :password_complexity
|
19
19
|
@@password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
|
20
20
|
|
21
|
+
# Define the class used to validate password complexity. Set to a Class or a
|
22
|
+
# string which will be used to determine which class to use.
|
23
|
+
mattr_accessor :password_complexity_validator
|
24
|
+
@@password_complexity_validator = 'devise_security/password_complexity_validator'
|
25
|
+
|
21
26
|
# Number of old passwords in archive
|
22
27
|
mattr_accessor :password_archiving_count
|
23
28
|
@@password_archiving_count = 5
|
@@ -79,11 +84,14 @@ module Devise
|
|
79
84
|
# paranoid_verification will regenerate verifacation code after faild attempt
|
80
85
|
mattr_accessor :paranoid_code_regenerate_after_attempt
|
81
86
|
@@paranoid_code_regenerate_after_attempt = 10
|
87
|
+
|
88
|
+
# Whether to allow passwords that are equal (case insensitive) to the email
|
89
|
+
mattr_accessor :allow_passwords_equal_to_email
|
90
|
+
@@allow_passwords_equal_to_email = false
|
82
91
|
end
|
83
92
|
|
84
|
-
#
|
93
|
+
# a security extension for devise
|
85
94
|
module DeviseSecurity
|
86
|
-
autoload :Schema, 'devise-security/schema'
|
87
95
|
autoload :Patches, 'devise-security/patches'
|
88
96
|
|
89
97
|
module Controllers
|
@@ -104,6 +112,6 @@ Devise.add_module :paranoid_verification, controller: :paranoid_verification_cod
|
|
104
112
|
# requires
|
105
113
|
require 'devise-security/routes'
|
106
114
|
require 'devise-security/rails'
|
107
|
-
require "devise-security/orm/#{DEVISE_ORM}"
|
115
|
+
require "devise-security/orm/#{DEVISE_ORM}" if DEVISE_ORM == :mongoid
|
108
116
|
require 'devise-security/models/database_authenticatable_patch'
|
109
117
|
require 'devise-security/models/paranoid_verification'
|
@@ -4,14 +4,14 @@ module DeviseSecurity
|
|
4
4
|
module Generators
|
5
5
|
# Generator for Rails to create or append to a Devise initializer.
|
6
6
|
class InstallGenerator < Rails::Generators::Base
|
7
|
-
LOCALES = %w[en es
|
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
9
|
source_root File.expand_path('../../templates', __FILE__)
|
10
10
|
desc 'Install the devise security extension'
|
11
11
|
|
12
12
|
def copy_initializer
|
13
|
-
template('
|
14
|
-
'config/initializers/
|
13
|
+
template('devise_security.rb',
|
14
|
+
'config/initializers/devise_security.rb',
|
15
15
|
)
|
16
16
|
end
|
17
17
|
|
@@ -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
|
@@ -41,4 +43,7 @@ Devise.setup do |config|
|
|
41
43
|
|
42
44
|
# Time period for account expiry from last_activity_at
|
43
45
|
# config.expire_after = 90.days
|
46
|
+
|
47
|
+
# Allow password to equal the email
|
48
|
+
# config.allow_passwords_equal_to_email = false
|
44
49
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class Devise::ParanoidVerificationCodeControllerTest < ActionController::TestCase
|
6
|
+
include Devise::Test::ControllerHelpers
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@request.env['devise.mapping'] = Devise.mappings[:user]
|
10
|
+
|
11
|
+
@user = User.create!(
|
12
|
+
username: 'hello',
|
13
|
+
email: 'hello@path.travel',
|
14
|
+
password: 'Password4',
|
15
|
+
confirmed_at: 5.months.ago,
|
16
|
+
)
|
17
|
+
|
18
|
+
sign_in(@user)
|
19
|
+
end
|
20
|
+
|
21
|
+
test 'redirects to root on show if user not logged in' do
|
22
|
+
sign_out(@user)
|
23
|
+
get :show
|
24
|
+
assert_redirected_to :root
|
25
|
+
end
|
26
|
+
|
27
|
+
test "redirects to root on show if user doesn't need paranoid verification" do
|
28
|
+
get :show
|
29
|
+
assert_redirected_to :root
|
30
|
+
end
|
31
|
+
|
32
|
+
test 'renders show on show if user needs paranoid verification' do
|
33
|
+
@user.update(paranoid_verification_code: 'cookies')
|
34
|
+
get :show
|
35
|
+
assert_template :show
|
36
|
+
end
|
37
|
+
|
38
|
+
test "redirects to root on update" do
|
39
|
+
patch :update, params: { user: { paranoid_verification_code: 'cookies' } }
|
40
|
+
assert_redirected_to :root
|
41
|
+
assert_equal 'Verification code accepted', flash[:notice]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class ParanoidVerificationCodeCustomRedirectTest < ActionController::TestCase
|
46
|
+
include Devise::Test::ControllerHelpers
|
47
|
+
tests Overrides::ParanoidVerificationCodeController
|
48
|
+
|
49
|
+
setup do
|
50
|
+
@request.env['devise.mapping'] = Devise.mappings[:paranoid_verification_user]
|
51
|
+
|
52
|
+
@user = ParanoidVerificationUser.create!(
|
53
|
+
username: 'hello',
|
54
|
+
email: 'hello@path.travel',
|
55
|
+
password: 'Password4',
|
56
|
+
confirmed_at: 5.months.ago,
|
57
|
+
)
|
58
|
+
|
59
|
+
sign_in(@user)
|
60
|
+
end
|
61
|
+
|
62
|
+
test 'redirects to custom redirect route on update' do
|
63
|
+
patch :update, params: { paranoid_verification_user: { paranoid_verification_code: 'cookies' } }
|
64
|
+
|
65
|
+
assert_redirected_to '/cats'
|
66
|
+
assert_equal 'Verification code accepted', flash[:notice]
|
67
|
+
end
|
68
|
+
end
|