devise-security 0.14.1 → 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 +136 -61
- data/app/controllers/devise/paranoid_verification_code_controller.rb +26 -12
- data/app/controllers/devise/password_expired_controller.rb +32 -10
- 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 +42 -0
- data/config/locales/by.yml +50 -0
- data/config/locales/cs.yml +46 -0
- data/config/locales/de.yml +16 -2
- data/config/locales/en.yml +15 -2
- data/config/locales/es.yml +22 -9
- data/config/locales/fa.yml +42 -0
- data/config/locales/fr.yml +15 -2
- data/config/locales/hi.yml +43 -0
- data/config/locales/it.yml +36 -4
- data/config/locales/ja.yml +14 -1
- 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 +26 -1
- 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 +72 -51
- 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 +3 -3
- data/lib/devise-security/hooks/session_limitable.rb +29 -14
- data/lib/devise-security/models/compatibility/{active_record.rb → active_record_patch.rb} +14 -2
- data/lib/devise-security/models/compatibility/{mongoid.rb → mongoid_patch.rb} +12 -1
- data/lib/devise-security/models/compatibility.rb +2 -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/password_expirable.rb +5 -1
- data/lib/devise-security/models/secure_validatable.rb +62 -11
- data/lib/devise-security/models/session_limitable.rb +17 -2
- 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 -24
- data/lib/devise-security/version.rb +1 -1
- data/lib/devise-security.rb +15 -6
- data/lib/generators/devise_security/install_generator.rb +4 -6
- data/lib/generators/templates/{devise-security.rb → devise_security.rb} +9 -1
- data/test/controllers/test_paranoid_verification_code_controller.rb +133 -0
- data/test/controllers/test_password_expired_controller.rb +164 -0
- data/test/{test_security_question_controller.rb → controllers/test_security_question_controller.rb} +19 -37
- 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 +17 -0
- data/test/dummy/app/controllers/widgets_controller.rb +9 -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 +10 -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 +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 +4 -13
- data/test/dummy/config/initializers/devise.rb +1 -5
- 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 +6 -3
- data/test/dummy/config.ru +1 -1
- data/test/dummy/db/migrate/20120508165529_create_tables.rb +15 -6
- 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 +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 +7 -4
- data/test/orm/mongoid.rb +2 -1
- data/test/support/integration_helpers.rb +35 -0
- data/test/support/mongoid.yml +1 -1
- data/test/test_compatibility.rb +15 -0
- data/test/test_complexity_validator.rb +251 -29
- data/test/test_database_authenticatable_patch.rb +146 -0
- data/test/test_helper.rb +23 -8
- data/test/test_install_generator.rb +12 -2
- 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 +284 -50
- 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 +202 -138
- 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/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/lib/devise-security/schema.rb +0 -66
- 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 -9
- data/test/dummy/lib/shared_user_without_email.rb +0 -28
- data/test/test_password_expired_controller.rb +0 -46
- /data/test/{test_captcha_controller.rb → controllers/test_captcha_controller.rb} +0 -0
@@ -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,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
|
|
@@ -64,10 +86,21 @@ module Devise
|
|
64
86
|
|
65
87
|
def current_equal_password_validation
|
66
88
|
return if new_record? || !will_save_change_to_encrypted_password? || password.blank?
|
89
|
+
|
67
90
|
dummy = self.class.new(encrypted_password: encrypted_password_was).tap do |user|
|
68
91
|
user.password_salt = password_salt_was if respond_to?(:password_salt)
|
69
92
|
end
|
70
|
-
|
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)
|
71
104
|
end
|
72
105
|
|
73
106
|
protected
|
@@ -75,6 +108,8 @@ module Devise
|
|
75
108
|
# Checks whether a password is needed or not. For validations only.
|
76
109
|
# Passwords are always required if it's a new record, or if the password
|
77
110
|
# or confirmation are being set somewhere.
|
111
|
+
#
|
112
|
+
# @return [Boolean]
|
78
113
|
def password_required?
|
79
114
|
!persisted? || !password.nil? || !password_confirmation.nil?
|
80
115
|
end
|
@@ -83,15 +118,31 @@ module Devise
|
|
83
118
|
true
|
84
119
|
end
|
85
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
|
+
|
86
130
|
module ClassMethods
|
87
|
-
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
|
+
)
|
88
139
|
|
89
140
|
private
|
90
141
|
|
91
|
-
def
|
142
|
+
def uniqueness_validation_of_login?
|
92
143
|
validators.any? do |validator|
|
93
144
|
validator_orm_klass = DEVISE_ORM == :active_record ? ActiveRecord::Validations::UniquenessValidator : ::Mongoid::Validatable::UniquenessValidator
|
94
|
-
validator.
|
145
|
+
validator.is_a?(validator_orm_klass) && validator.attributes.include?(login_attribute)
|
95
146
|
end
|
96
147
|
end
|
97
148
|
|
@@ -100,7 +151,7 @@ module Devise
|
|
100
151
|
end
|
101
152
|
|
102
153
|
def devise_validation_enabled?
|
103
|
-
|
154
|
+
ancestors.map(&:to_s).include? 'Devise::Models::Validatable'
|
104
155
|
end
|
105
156
|
end
|
106
157
|
end
|
@@ -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,13 +12,27 @@ 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
|
|
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
|
15
22
|
def update_unique_session_id!(unique_session_id)
|
16
|
-
|
23
|
+
raise Devise::Models::Compatibility::NotPersistedError, 'cannot update a new record' unless persisted?
|
17
24
|
|
18
|
-
|
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
|
19
28
|
end
|
20
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
|
21
36
|
end
|
22
37
|
end
|
23
38
|
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,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
@@ -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
|
@@ -79,11 +85,14 @@ module Devise
|
|
79
85
|
# paranoid_verification will regenerate verifacation code after faild attempt
|
80
86
|
mattr_accessor :paranoid_code_regenerate_after_attempt
|
81
87
|
@@paranoid_code_regenerate_after_attempt = 10
|
88
|
+
|
89
|
+
# Whether to allow passwords that are equal (case insensitive) to the email
|
90
|
+
mattr_accessor :allow_passwords_equal_to_email
|
91
|
+
@@allow_passwords_equal_to_email = false
|
82
92
|
end
|
83
93
|
|
84
|
-
#
|
94
|
+
# a security extension for devise
|
85
95
|
module DeviseSecurity
|
86
|
-
autoload :Schema, 'devise-security/schema'
|
87
96
|
autoload :Patches, 'devise-security/patches'
|
88
97
|
|
89
98
|
module Controllers
|
@@ -104,6 +113,6 @@ Devise.add_module :paranoid_verification, controller: :paranoid_verification_cod
|
|
104
113
|
# requires
|
105
114
|
require 'devise-security/routes'
|
106
115
|
require 'devise-security/rails'
|
107
|
-
require "devise-security/orm/#{DEVISE_ORM}"
|
116
|
+
require "devise-security/orm/#{DEVISE_ORM}" if DEVISE_ORM == :mongoid
|
108
117
|
require 'devise-security/models/database_authenticatable_patch'
|
109
118
|
require 'devise-security/models/paranoid_verification'
|
@@ -4,22 +4,20 @@ 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
|
-
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('
|
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
|
@@ -41,4 +43,10 @@ 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
|
49
|
+
|
50
|
+
# paranoid_verification will regenerate verification code after failed attempt
|
51
|
+
# config.paranoid_code_regenerate_after_attempt = 10
|
44
52
|
end
|
@@ -0,0 +1,133 @@
|
|
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
|
+
@controller.class.respond_to :json, :xml
|
10
|
+
@request.env['devise.mapping'] = Devise.mappings[:user]
|
11
|
+
@user = User.create!(
|
12
|
+
username: 'hello',
|
13
|
+
email: 'hello@path.travel',
|
14
|
+
password: 'Password4',
|
15
|
+
confirmed_at: 5.months.ago,
|
16
|
+
paranoid_verification_code: 'cookies'
|
17
|
+
)
|
18
|
+
assert @user.valid?
|
19
|
+
assert @user.need_paranoid_verification?
|
20
|
+
|
21
|
+
sign_in(@user)
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'redirects to root on show if user not logged in' do
|
25
|
+
sign_out(@user)
|
26
|
+
get :show
|
27
|
+
assert_redirected_to :root
|
28
|
+
end
|
29
|
+
|
30
|
+
test "redirects to root on show if user doesn't need paranoid verification" do
|
31
|
+
@user.update(paranoid_verification_code: nil)
|
32
|
+
get :show
|
33
|
+
assert_redirected_to :root
|
34
|
+
end
|
35
|
+
|
36
|
+
test 'renders show on show if user needs paranoid verification' do
|
37
|
+
@user.update(paranoid_verification_code: 'cookies')
|
38
|
+
get :show
|
39
|
+
assert_template :show
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'redirects on update if user not logged in' do
|
43
|
+
sign_out(@user)
|
44
|
+
patch :update
|
45
|
+
assert_redirected_to :root
|
46
|
+
end
|
47
|
+
|
48
|
+
test 'redirects on update if user does not need paranoid verification' do
|
49
|
+
@user.update(paranoid_verification_code: nil)
|
50
|
+
patch :update
|
51
|
+
assert_redirected_to :root
|
52
|
+
end
|
53
|
+
|
54
|
+
test 'update paranoid_verification_code with default format' do
|
55
|
+
patch(
|
56
|
+
:update,
|
57
|
+
params: {
|
58
|
+
user: {
|
59
|
+
paranoid_verification_code: 'cookies'
|
60
|
+
}
|
61
|
+
}
|
62
|
+
)
|
63
|
+
assert_redirected_to root_path
|
64
|
+
assert_equal 'Verification code accepted', flash[:notice]
|
65
|
+
assert_equal('text/html', response.media_type)
|
66
|
+
end
|
67
|
+
|
68
|
+
test 'update paranoid_verification_code using JSON format' do
|
69
|
+
patch(
|
70
|
+
:update,
|
71
|
+
format: :json,
|
72
|
+
params: {
|
73
|
+
user: {
|
74
|
+
paranoid_verification_code: 'cookies'
|
75
|
+
}
|
76
|
+
}
|
77
|
+
)
|
78
|
+
|
79
|
+
assert_response 204
|
80
|
+
assert_equal root_url, response.location
|
81
|
+
assert_nil response.media_type, 'No Content-Type header should be set for No Content response'
|
82
|
+
end
|
83
|
+
|
84
|
+
test 'update paranoid_verification_code using XML format' do
|
85
|
+
patch(
|
86
|
+
:update,
|
87
|
+
format: :xml,
|
88
|
+
params: {
|
89
|
+
user: {
|
90
|
+
paranoid_verification_code: 'cookies'
|
91
|
+
}
|
92
|
+
}
|
93
|
+
)
|
94
|
+
assert_response 204
|
95
|
+
assert_equal root_url, response.location
|
96
|
+
assert_nil response.media_type, 'No Content-Type header should be set for No Content response'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class ParanoidVerificationCodeCustomRedirectTest < ActionController::TestCase
|
101
|
+
include Devise::Test::ControllerHelpers
|
102
|
+
tests Overrides::ParanoidVerificationCodeController
|
103
|
+
|
104
|
+
setup do
|
105
|
+
@controller.class.respond_to :json, :xml
|
106
|
+
@request.env['devise.mapping'] = Devise.mappings[:paranoid_verification_user]
|
107
|
+
@user = ParanoidVerificationUser.create!(
|
108
|
+
username: 'hello',
|
109
|
+
email: 'hello@path.travel',
|
110
|
+
password: 'Password4',
|
111
|
+
confirmed_at: 5.months.ago,
|
112
|
+
paranoid_verification_code: 'cookies'
|
113
|
+
)
|
114
|
+
assert @user.valid?
|
115
|
+
assert @user.need_paranoid_verification?
|
116
|
+
|
117
|
+
sign_in(@user)
|
118
|
+
end
|
119
|
+
|
120
|
+
test 'redirects to custom redirect route on update' do
|
121
|
+
patch(
|
122
|
+
:update,
|
123
|
+
params: {
|
124
|
+
paranoid_verification_user: {
|
125
|
+
paranoid_verification_code: 'cookies'
|
126
|
+
}
|
127
|
+
}
|
128
|
+
)
|
129
|
+
|
130
|
+
assert_redirected_to '/cats'
|
131
|
+
assert_equal 'Verification code accepted', flash[:notice]
|
132
|
+
end
|
133
|
+
end
|