devise-security 0.12.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 +199 -65
- data/app/controllers/devise/paranoid_verification_code_controller.rb +28 -12
- data/app/controllers/devise/password_expired_controller.rb +34 -10
- data/app/views/devise/paranoid_verification_code/show.html.erb +4 -4
- data/app/views/devise/password_expired/show.html.erb +6 -6
- data/config/locales/bg.yml +42 -0
- data/config/locales/by.yml +50 -0
- data/config/locales/cs.yml +46 -0
- data/config/locales/de.yml +33 -7
- data/config/locales/en.yml +26 -1
- data/config/locales/es.yml +31 -6
- data/config/locales/fa.yml +42 -0
- data/config/locales/fr.yml +42 -0
- data/config/locales/hi.yml +43 -0
- data/config/locales/it.yml +36 -4
- data/config/locales/ja.yml +42 -0
- data/config/locales/nl.yml +42 -0
- data/config/locales/pt.yml +42 -0
- data/config/locales/ru.yml +50 -0
- data/config/locales/tr.yml +42 -0
- data/config/locales/uk.yml +50 -0
- data/config/locales/zh_CN.yml +42 -0
- data/config/locales/zh_TW.yml +42 -0
- data/lib/devise-security/controllers/helpers.rb +74 -51
- data/lib/devise-security/hooks/expirable.rb +6 -4
- data/lib/devise-security/hooks/paranoid_verification.rb +3 -3
- data/lib/devise-security/hooks/password_expirable.rb +5 -3
- data/lib/devise-security/hooks/session_limitable.rb +31 -14
- data/lib/devise-security/models/active_record/old_password.rb +5 -0
- data/lib/devise-security/models/compatibility/active_record_patch.rb +41 -0
- data/lib/devise-security/models/compatibility/mongoid_patch.rb +32 -0
- data/lib/devise-security/models/compatibility.rb +8 -15
- data/lib/devise-security/models/database_authenticatable_patch.rb +20 -10
- data/lib/devise-security/models/expirable.rb +14 -7
- data/lib/devise-security/models/mongoid/old_password.rb +21 -0
- data/lib/devise-security/models/paranoid_verification.rb +4 -2
- data/lib/devise-security/models/password_archivable.rb +19 -8
- data/lib/devise-security/models/password_expirable.rb +103 -48
- data/lib/devise-security/models/secure_validatable.rb +69 -12
- data/lib/devise-security/models/security_questionable.rb +2 -0
- data/lib/devise-security/models/session_limitable.rb +19 -2
- data/lib/devise-security/orm/mongoid.rb +7 -0
- data/lib/devise-security/patches/controller_captcha.rb +2 -0
- data/lib/devise-security/patches/controller_security_question.rb +3 -1
- data/lib/devise-security/patches.rb +16 -8
- data/lib/devise-security/rails.rb +2 -0
- data/lib/devise-security/routes.rb +4 -3
- data/lib/devise-security/validators/password_complexity_validator.rb +62 -0
- data/lib/devise-security/version.rb +3 -1
- data/lib/devise-security.rb +23 -11
- data/lib/generators/devise_security/install_generator.rb +6 -6
- data/lib/generators/templates/devise_security.rb +52 -0
- data/test/{test_captcha_controller.rb → controllers/test_captcha_controller.rb} +2 -0
- data/test/controllers/test_paranoid_verification_code_controller.rb +133 -0
- data/test/controllers/test_password_expired_controller.rb +164 -0
- data/test/controllers/test_security_question_controller.rb +66 -0
- data/test/dummy/Rakefile +3 -1
- data/test/dummy/app/assets/config/manifest.js +3 -0
- data/test/dummy/app/controllers/application_controller.rb +2 -0
- data/test/dummy/app/controllers/captcha/sessions_controller.rb +2 -0
- 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/security_question/unlocks_controller.rb +2 -0
- data/test/dummy/app/controllers/widgets_controller.rb +9 -0
- data/test/dummy/app/models/application_record.rb +10 -2
- data/test/dummy/app/models/application_user_record.rb +12 -0
- data/test/dummy/app/models/captcha_user.rb +7 -2
- data/test/dummy/app/models/mongoid/confirmable_fields.rb +15 -0
- data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +18 -0
- data/test/dummy/app/models/mongoid/expirable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/lockable_fields.rb +15 -0
- data/test/dummy/app/models/mongoid/mappings.rb +15 -0
- data/test/dummy/app/models/mongoid/omniauthable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +12 -0
- data/test/dummy/app/models/mongoid/password_archivable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/password_expirable_fields.rb +12 -0
- data/test/dummy/app/models/mongoid/recoverable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/registerable_fields.rb +21 -0
- data/test/dummy/app/models/mongoid/rememberable_fields.rb +12 -0
- data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/security_questionable_fields.rb +15 -0
- data/test/dummy/app/models/mongoid/session_limitable_fields.rb +12 -0
- data/test/dummy/app/models/mongoid/timeoutable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/trackable_fields.rb +16 -0
- data/test/dummy/app/models/mongoid/validatable_fields.rb +9 -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/security_question_user.rb +9 -4
- data/test/dummy/app/models/user.rb +16 -1
- data/test/dummy/app/models/widget.rb +4 -0
- data/test/dummy/app/mongoid/admin.rb +31 -0
- data/test/dummy/app/mongoid/one_user.rb +58 -0
- data/test/dummy/app/mongoid/shim.rb +25 -0
- data/test/dummy/app/mongoid/user_on_engine.rb +41 -0
- data/test/dummy/app/mongoid/user_on_main_app.rb +41 -0
- data/test/dummy/app/mongoid/user_with_validations.rb +37 -0
- data/test/dummy/app/mongoid/user_without_email.rb +38 -0
- data/test/dummy/config/application.rb +13 -11
- data/test/dummy/config/boot.rb +3 -1
- data/test/dummy/config/environment.rb +3 -1
- data/test/dummy/config/environments/test.rb +6 -13
- data/test/dummy/config/initializers/devise.rb +6 -3
- data/test/dummy/config/initializers/migration_class.rb +3 -6
- data/test/dummy/config/locales/en.yml +10 -0
- data/test/dummy/config/mongoid.yml +6 -0
- data/test/dummy/config/routes.rb +8 -3
- data/test/dummy/config.ru +3 -1
- data/test/dummy/db/migrate/20120508165529_create_tables.rb +17 -6
- data/test/dummy/db/migrate/20150402165590_add_verification_columns.rb +2 -0
- data/test/dummy/db/migrate/20150407162345_add_verification_attempt_column.rb +2 -0
- data/test/dummy/db/migrate/20160320162345_add_security_questions_fields.rb +2 -0
- data/test/dummy/db/migrate/20180318103603_add_expireable_columns.rb +2 -0
- data/test/dummy/db/migrate/20180318105329_add_confirmable_columns.rb +2 -0
- data/test/dummy/db/migrate/20180318105732_add_rememberable_columns.rb +2 -0
- data/test/dummy/db/migrate/20180318111336_add_recoverable_columns.rb +2 -0
- data/test/dummy/db/migrate/20180319114023_add_widget.rb +2 -0
- data/test/dummy/lib/shared_expirable_columns.rb +15 -0
- data/test/dummy/lib/shared_security_questions_fields.rb +17 -0
- data/test/dummy/lib/shared_user.rb +43 -0
- data/test/dummy/lib/shared_user_with_password_verification.rb +13 -0
- data/test/dummy/lib/shared_user_without_omniauth.rb +24 -0
- data/test/dummy/lib/shared_verification_fields.rb +16 -0
- data/test/dummy/log/test.log +45240 -0
- 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 +53 -0
- data/test/integration/test_session_limitable_workflow.rb +69 -0
- data/test/orm/active_record.rb +15 -0
- data/test/orm/mongoid.rb +13 -0
- data/test/support/integration_helpers.rb +35 -0
- data/test/support/mongoid.yml +6 -0
- data/test/test_compatibility.rb +15 -0
- data/test/test_complexity_validator.rb +282 -0
- data/test/test_database_authenticatable_patch.rb +146 -0
- data/test/test_helper.rb +41 -9
- data/test/test_install_generator.rb +20 -3
- data/test/test_paranoid_verification.rb +10 -9
- data/test/test_password_archivable.rb +37 -13
- data/test/test_password_expirable.rb +72 -9
- data/test/test_secure_validatable.rb +289 -55
- data/test/test_secure_validatable_overrides.rb +185 -0
- data/test/test_session_limitable.rb +57 -0
- data/test/tmp/config/initializers/devise_security.rb +52 -0
- data/test/tmp/config/locales/devise.security_extension.by.yml +50 -0
- data/test/tmp/config/locales/devise.security_extension.cs.yml +46 -0
- data/test/tmp/config/locales/devise.security_extension.de.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.en.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.es.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.fa.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.fr.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.hi.yml +43 -0
- data/test/tmp/config/locales/devise.security_extension.it.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.ja.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.nl.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.pt.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.ru.yml +50 -0
- data/test/tmp/config/locales/devise.security_extension.tr.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.uk.yml +50 -0
- data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.zh_TW.yml +42 -0
- metadata +290 -124
- data/.circleci/config.yml +0 -41
- data/.document +0 -5
- data/.gitignore +0 -40
- data/.rubocop.yml +0 -63
- data/.ruby-version +0 -1
- data/.travis.yml +0 -25
- data/Appraisals +0 -19
- data/Gemfile +0 -3
- data/Rakefile +0 -28
- data/devise-security.gemspec +0 -44
- data/gemfiles/rails_4.1_stable.gemfile +0 -8
- data/gemfiles/rails_4.2_stable.gemfile +0 -8
- data/gemfiles/rails_5.0_stable.gemfile +0 -8
- data/gemfiles/rails_5.1_stable.gemfile +0 -8
- data/gemfiles/rails_5.2_rc1.gemfile +0 -8
- data/lib/devise-security/models/old_password.rb +0 -4
- data/lib/devise-security/orm/active_record.rb +0 -18
- data/lib/devise-security/patches/confirmations_controller_captcha.rb +0 -21
- data/lib/devise-security/patches/confirmations_controller_security_question.rb +0 -24
- data/lib/devise-security/patches/passwords_controller_captcha.rb +0 -20
- data/lib/devise-security/patches/passwords_controller_security_question.rb +0 -23
- data/lib/devise-security/patches/registrations_controller_captcha.rb +0 -33
- data/lib/devise-security/patches/sessions_controller_captcha.rb +0 -24
- data/lib/devise-security/patches/unlocks_controller_captcha.rb +0 -20
- data/lib/devise-security/patches/unlocks_controller_security_question.rb +0 -23
- data/lib/devise-security/schema.rb +0 -64
- data/lib/generators/templates/devise-security.rb +0 -38
- data/test/dummy/app/controllers/foos_controller.rb +0 -0
- data/test/dummy/app/models/.gitkeep +0 -0
- data/test/dummy/app/models/secure_user.rb +0 -3
- data/test/test_password_expired_controller.rb +0 -44
- data/test/test_security_question_controller.rb +0 -84
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'devise-security/hooks/paranoid_verification'
|
2
4
|
|
3
5
|
module Devise
|
@@ -18,7 +20,7 @@ module Devise
|
|
18
20
|
elsif code == paranoid_verification_code
|
19
21
|
attempt = 0
|
20
22
|
update_without_password paranoid_verification_code: nil,
|
21
|
-
paranoid_verified_at: Time.now,
|
23
|
+
paranoid_verified_at: Time.zone.now,
|
22
24
|
paranoid_verification_attempt: attempt
|
23
25
|
else
|
24
26
|
update_without_password paranoid_verification_attempt: attempt
|
@@ -30,7 +32,7 @@ module Devise
|
|
30
32
|
end
|
31
33
|
|
32
34
|
def generate_paranoid_code
|
33
|
-
update_without_password paranoid_verification_code: Devise.verification_code_generator.call
|
35
|
+
update_without_password paranoid_verification_code: Devise.verification_code_generator.call,
|
34
36
|
paranoid_verification_attempt: 0
|
35
37
|
end
|
36
38
|
end
|
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'compatibility'
|
4
|
+
require_relative "#{DEVISE_ORM}/old_password"
|
2
5
|
|
3
6
|
module Devise
|
4
7
|
module Models
|
@@ -9,7 +12,7 @@ module Devise
|
|
9
12
|
include Devise::Models::DatabaseAuthenticatable
|
10
13
|
|
11
14
|
included do
|
12
|
-
has_many :old_passwords, as: :password_archivable, dependent: :destroy
|
15
|
+
has_many :old_passwords, class_name: 'OldPassword', as: :password_archivable, dependent: :destroy
|
13
16
|
before_update :archive_password, if: :will_save_change_to_encrypted_password?
|
14
17
|
validate :validate_password_archive, if: :password_present?
|
15
18
|
end
|
@@ -32,15 +35,19 @@ module Devise
|
|
32
35
|
end
|
33
36
|
end
|
34
37
|
|
35
|
-
# validate
|
38
|
+
# validate if the password was used in the past
|
36
39
|
# @return [true] if current password was used previously
|
37
40
|
# @return [false] if disabled or not previously used
|
38
41
|
def password_archive_included?
|
39
|
-
return false unless max_old_passwords
|
40
|
-
|
42
|
+
return false unless max_old_passwords.positive?
|
43
|
+
|
44
|
+
old_passwords_including_cur_change = old_passwords.reorder(created_at: :desc).limit(max_old_passwords).pluck(:encrypted_password)
|
41
45
|
old_passwords_including_cur_change << encrypted_password_was # include most recent change in list, but don't save it yet!
|
42
46
|
old_passwords_including_cur_change.any? do |old_password|
|
43
|
-
|
47
|
+
# NOTE: we deliberately do not do mass assignment here so that users that
|
48
|
+
# rely on `protected_attributes_continued` gem can still use this extension.
|
49
|
+
# See issue #68
|
50
|
+
self.class.new.tap { |object| object.encrypted_password = old_password }.valid_password?(password)
|
44
51
|
end
|
45
52
|
end
|
46
53
|
|
@@ -58,11 +65,15 @@ module Devise
|
|
58
65
|
|
59
66
|
private
|
60
67
|
|
61
|
-
#
|
68
|
+
# Archive the last password before save and delete all to old passwords from archive
|
69
|
+
# @note we check to see if an old password has already been archived because
|
70
|
+
# mongoid will keep re-triggering this callback when we add an old password
|
62
71
|
def archive_password
|
63
|
-
if max_old_passwords
|
72
|
+
if max_old_passwords.positive?
|
73
|
+
return true if old_passwords.where(encrypted_password: encrypted_password_was).exists?
|
74
|
+
|
64
75
|
old_passwords.create!(encrypted_password: encrypted_password_was) if encrypted_password_was.present?
|
65
|
-
old_passwords.
|
76
|
+
old_passwords.reorder(created_at: :desc).offset(max_old_passwords).destroy_all
|
66
77
|
else
|
67
78
|
old_passwords.destroy_all
|
68
79
|
end
|
@@ -1,67 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'devise-security/hooks/password_expirable'
|
2
4
|
|
3
|
-
module Devise
|
4
|
-
|
5
|
+
module Devise::Models
|
6
|
+
# PasswordExpirable makes passwords expire after a configurable amount of
|
7
|
+
# time, or on demand.
|
8
|
+
#
|
9
|
+
# == Configuration
|
10
|
+
# Set +expire_password_after+ to the number of seconds a password is valid for
|
11
|
+
# (example: +3.months+). Setting it to +true+ will allow passwords to be expired
|
12
|
+
# on-demand only, and +false+ disables this feature.
|
13
|
+
#
|
14
|
+
# == Expire On-Demand
|
15
|
+
# This is useful to force users to change passwords for complex business reasons.
|
16
|
+
# Call +need_change_password+ to indicate a record needs a new password.
|
17
|
+
module PasswordExpirable
|
18
|
+
extend ActiveSupport::Concern
|
5
19
|
|
6
|
-
|
7
|
-
|
8
|
-
|
20
|
+
included do
|
21
|
+
scope :with_password_change_requested, -> { where(password_changed_at: nil) }
|
22
|
+
scope :without_password_change_requested, -> { where.not(password_changed_at: nil) }
|
23
|
+
scope :with_expired_password, -> { where('password_changed_at is NULL OR password_changed_at < ?', expire_password_after.seconds.ago) }
|
24
|
+
scope :without_expired_password, -> { without_password_change_requested.where('password_changed_at >= ?', expire_password_after.seconds.ago) }
|
25
|
+
before_save :update_password_changed
|
26
|
+
end
|
9
27
|
|
10
|
-
|
11
|
-
|
12
|
-
|
28
|
+
# Is a password change required?
|
29
|
+
# @return [Boolean]
|
30
|
+
# @return [true] if +password_changed_at+ has not been set or if it is old
|
31
|
+
# enough based on +expire_password_after+ configuration.
|
32
|
+
def need_change_password?
|
33
|
+
password_change_requested? || password_too_old?
|
34
|
+
end
|
13
35
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
end
|
36
|
+
# Clear the +password_changed_at+ field so that the user will be required to
|
37
|
+
# update their password.
|
38
|
+
# @note Saves the record (without validations)
|
39
|
+
# @return [Boolean]
|
40
|
+
def need_change_password!
|
41
|
+
return unless password_expiration_enabled?
|
22
42
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
29
|
-
end
|
43
|
+
need_change_password
|
44
|
+
save(validate: false)
|
45
|
+
end
|
46
|
+
alias expire_password! need_change_password!
|
47
|
+
alias request_password_change! need_change_password!
|
30
48
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
49
|
+
# Clear the +password_changed_at+ field so that the user will be required to
|
50
|
+
# update their password.
|
51
|
+
# @note Does not save the record
|
52
|
+
# @return [void]
|
53
|
+
def need_change_password
|
54
|
+
return unless password_expiration_enabled?
|
36
55
|
|
37
|
-
|
38
|
-
|
56
|
+
self.password_changed_at = nil
|
57
|
+
end
|
58
|
+
alias expire_password need_change_password
|
59
|
+
alias request_password_change need_change_password
|
39
60
|
|
40
|
-
|
41
|
-
|
61
|
+
# @return [Integer] number of seconds passwords are valid for
|
62
|
+
# @return [true] passwords are expired 'on demand' only.
|
63
|
+
# @return [false] passwords never expire (this feature is disabled)
|
64
|
+
def expire_password_after
|
65
|
+
self.class.expire_password_after
|
66
|
+
end
|
42
67
|
|
43
|
-
|
44
|
-
|
45
|
-
|
68
|
+
# When +password_changed_at+ is set to +NULL+ in the database
|
69
|
+
# the user is required to change their password. This only happens
|
70
|
+
# on demand or when the column is first added to the table.
|
71
|
+
# @return [Boolean]
|
72
|
+
def password_change_requested?
|
73
|
+
return false unless password_expiration_enabled?
|
74
|
+
return false if new_record?
|
46
75
|
|
47
|
-
|
76
|
+
password_changed_at.nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
# Is this password older than the configured expiration timeout?
|
80
|
+
# @return [Boolean]
|
81
|
+
def password_too_old?
|
82
|
+
return false if new_record?
|
83
|
+
return false unless password_expiration_enabled?
|
84
|
+
return false if expire_password_on_demand?
|
48
85
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
86
|
+
password_changed_at < expire_password_after.seconds.ago
|
87
|
+
end
|
88
|
+
alias password_expired? password_too_old?
|
53
89
|
|
54
|
-
|
55
|
-
return @_numeric if defined?(@_numeric)
|
56
|
-
@_numeric ||= self.expire_password_after.is_a?(1.class) ||
|
57
|
-
self.expire_password_after.is_a?(Float)
|
58
|
-
end
|
90
|
+
private
|
59
91
|
|
60
|
-
|
61
|
-
|
92
|
+
# Update +password_changed_at+ for new records and changed passwords.
|
93
|
+
# @note called as a +before_save+ hook
|
94
|
+
def update_password_changed
|
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?
|
62
99
|
end
|
100
|
+
|
101
|
+
self.password_changed_at = Time.zone.now
|
63
102
|
end
|
64
103
|
|
65
|
-
|
104
|
+
# Enabled if configuration +expire_password_after+ is set to an {Integer},
|
105
|
+
# {Float}, or {true}
|
106
|
+
def password_expiration_enabled?
|
107
|
+
expire_password_after.is_a?(1.class) ||
|
108
|
+
expire_password_after.is_a?(Float) ||
|
109
|
+
expire_password_on_demand?
|
110
|
+
end
|
111
|
+
|
112
|
+
# When +expire_password_after+ is set to +true+ then only expire passwords
|
113
|
+
# on demand.
|
114
|
+
def expire_password_on_demand?
|
115
|
+
expire_password_after.present? && expire_password_after == true
|
116
|
+
end
|
66
117
|
|
118
|
+
module ClassMethods
|
119
|
+
::Devise::Models.config(self, :expire_password_after)
|
120
|
+
end
|
121
|
+
end
|
67
122
|
end
|
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'compatibility'
|
4
|
+
require_relative '../validators/password_complexity_validator'
|
2
5
|
|
3
6
|
module Devise
|
4
7
|
module Models
|
@@ -23,7 +26,7 @@ module Devise
|
|
23
26
|
already_validated_email = false
|
24
27
|
|
25
28
|
# validate login in a strict way if not yet validated
|
26
|
-
unless
|
29
|
+
unless uniqueness_validation_of_login?
|
27
30
|
validation_condition = "#{login_attribute}_changed?".to_sym
|
28
31
|
|
29
32
|
validates login_attribute, uniqueness: {
|
@@ -41,15 +44,39 @@ module Devise
|
|
41
44
|
validates :email, uniqueness: true, allow_blank: true, if: :email_changed? # check uniq for email ever
|
42
45
|
end
|
43
46
|
|
44
|
-
|
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
|
45
56
|
end
|
46
57
|
|
47
58
|
# extra validations
|
48
|
-
|
49
|
-
|
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
|
50
74
|
|
51
75
|
# don't allow use same password
|
52
76
|
validate :current_equal_password_validation
|
77
|
+
|
78
|
+
# don't allow email to equal password
|
79
|
+
validate :email_not_equal_password_validation
|
53
80
|
end
|
54
81
|
end
|
55
82
|
|
@@ -59,10 +86,21 @@ module Devise
|
|
59
86
|
|
60
87
|
def current_equal_password_validation
|
61
88
|
return if new_record? || !will_save_change_to_encrypted_password? || password.blank?
|
89
|
+
|
62
90
|
dummy = self.class.new(encrypted_password: encrypted_password_was).tap do |user|
|
63
91
|
user.password_salt = password_salt_was if respond_to?(:password_salt)
|
64
92
|
end
|
65
|
-
|
93
|
+
errors.add(:password, :equal_to_current_password) if dummy.valid_password?(password)
|
94
|
+
end
|
95
|
+
|
96
|
+
def email_not_equal_password_validation
|
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)
|
66
104
|
end
|
67
105
|
|
68
106
|
protected
|
@@ -70,6 +108,8 @@ module Devise
|
|
70
108
|
# Checks whether a password is needed or not. For validations only.
|
71
109
|
# Passwords are always required if it's a new record, or if the password
|
72
110
|
# or confirmation are being set somewhere.
|
111
|
+
#
|
112
|
+
# @return [Boolean]
|
73
113
|
def password_required?
|
74
114
|
!persisted? || !password.nil? || !password_confirmation.nil?
|
75
115
|
end
|
@@ -78,14 +118,31 @@ module Devise
|
|
78
118
|
true
|
79
119
|
end
|
80
120
|
|
81
|
-
|
82
|
-
|
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
|
+
)
|
83
129
|
|
84
|
-
|
85
|
-
|
130
|
+
module ClassMethods
|
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
|
+
)
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def uniqueness_validation_of_login?
|
86
143
|
validators.any? do |validator|
|
87
|
-
|
88
|
-
|
144
|
+
validator_orm_klass = DEVISE_ORM == :active_record ? ActiveRecord::Validations::UniquenessValidator : ::Mongoid::Validatable::UniquenessValidator
|
145
|
+
validator.is_a?(validator_orm_klass) && validator.attributes.include?(login_attribute)
|
89
146
|
end
|
90
147
|
end
|
91
148
|
|
@@ -94,7 +151,7 @@ module Devise
|
|
94
151
|
end
|
95
152
|
|
96
153
|
def devise_validation_enabled?
|
97
|
-
|
154
|
+
ancestors.map(&:to_s).include? 'Devise::Models::Validatable'
|
98
155
|
end
|
99
156
|
end
|
100
157
|
end
|
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'compatibility'
|
1
4
|
require 'devise-security/hooks/session_limitable'
|
2
5
|
|
3
6
|
module Devise
|
@@ -9,13 +12,27 @@ module Devise
|
|
9
12
|
# someone used his credentials to sign in.
|
10
13
|
module SessionLimitable
|
11
14
|
extend ActiveSupport::Concern
|
15
|
+
include Devise::Models::Compatibility
|
12
16
|
|
17
|
+
# Update the unique_session_id on the model. This will be checked in
|
18
|
+
# the Warden after_set_user hook in {file:devise-security/hooks/session_limitable}
|
19
|
+
# @param unique_session_id [String]
|
20
|
+
# @return [void]
|
21
|
+
# @raise [Devise::Models::Compatibility::NotPersistedError] if record is unsaved
|
13
22
|
def update_unique_session_id!(unique_session_id)
|
14
|
-
|
23
|
+
raise Devise::Models::Compatibility::NotPersistedError, 'cannot update a new record' unless persisted?
|
15
24
|
|
16
|
-
|
25
|
+
update_attribute_without_validatons_or_callbacks(:unique_session_id, unique_session_id).tap do
|
26
|
+
Rails.logger.debug { "[devise-security][session_limitable] unique_session_id=#{unique_session_id}" }
|
27
|
+
end
|
17
28
|
end
|
18
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
|
19
36
|
end
|
20
37
|
end
|
21
38
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DeviseSecurity::Patches
|
2
4
|
module ControllerSecurityQuestion
|
3
5
|
extend ActiveSupport::Concern
|
@@ -7,6 +9,7 @@ module DeviseSecurity::Patches
|
|
7
9
|
end
|
8
10
|
|
9
11
|
private
|
12
|
+
|
10
13
|
def check_security_question
|
11
14
|
# only find via email, not login
|
12
15
|
resource = resource_class.find_or_initialize_with_error_by(:email, params[resource_name][:email], :not_found)
|
@@ -17,4 +20,3 @@ module DeviseSecurity::Patches
|
|
17
20
|
end
|
18
21
|
end
|
19
22
|
end
|
20
|
-
|
@@ -1,21 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DeviseSecurity
|
2
4
|
module Patches
|
3
5
|
autoload :ControllerCaptcha, 'devise-security/patches/controller_captcha'
|
4
6
|
autoload :ControllerSecurityQuestion, 'devise-security/patches/controller_security_question'
|
5
7
|
|
6
8
|
class << self
|
9
|
+
# rubocop:disable Metrics/AbcSize
|
10
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
11
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
7
12
|
def apply
|
8
|
-
Devise::PasswordsController.
|
9
|
-
Devise::UnlocksController.
|
10
|
-
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
|
11
16
|
|
12
|
-
Devise::PasswordsController.
|
13
|
-
Devise::UnlocksController.
|
14
|
-
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
|
15
20
|
|
16
|
-
Devise::RegistrationsController.
|
17
|
-
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
|
18
23
|
end
|
24
|
+
# rubocop:enable Metrics/AbcSize
|
25
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
26
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
19
27
|
end
|
20
28
|
end
|
21
29
|
end
|
@@ -1,16 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionDispatch::Routing
|
2
4
|
class Mapper
|
3
|
-
|
4
5
|
protected
|
5
6
|
|
6
7
|
# route for handle expired passwords
|
7
8
|
def devise_password_expired(mapping, controllers)
|
8
|
-
resource :password_expired, only: [
|
9
|
+
resource :password_expired, only: %i[show update], path: mapping.path_names[:password_expired], controller: controllers[:password_expired]
|
9
10
|
end
|
10
11
|
|
11
12
|
# route for handle paranoid verification
|
12
13
|
def devise_verification_code(mapping, controllers)
|
13
|
-
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]
|
14
15
|
end
|
15
16
|
end
|
16
17
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
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
|
+
#
|
14
|
+
# Options:
|
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
|
21
|
+
class DeviseSecurity::PasswordComplexityValidator < ActiveModel::EachValidator
|
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
|
36
|
+
|
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?
|
47
|
+
|
48
|
+
options.sort.each do |pattern_name, minimum|
|
49
|
+
normalized_option = pattern_name.to_s.singularize.to_sym
|
50
|
+
|
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
|
61
|
+
end
|
62
|
+
end
|