devise-security 0.16.0 → 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 +4 -2
- data/app/controllers/devise/paranoid_verification_code_controller.rb +13 -1
- data/app/controllers/devise/password_expired_controller.rb +14 -1
- data/config/locales/bg.yml +41 -0
- data/config/locales/de.yml +2 -0
- data/config/locales/en.yml +2 -1
- 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/secure_validatable.rb +51 -15
- data/lib/devise-security/validators/password_complexity_validator.rb +53 -26
- data/lib/devise-security/version.rb +1 -1
- data/lib/devise-security.rb +7 -2
- data/lib/generators/templates/devise_security.rb +3 -1
- data/test/controllers/test_paranoid_verification_code_controller.rb +68 -0
- data/test/controllers/test_password_expired_controller.rb +38 -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/locales/en.yml +10 -0
- data/test/dummy/config/routes.rb +2 -0
- 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/log/development.log +0 -883
- data/test/dummy/log/test.log +95414 -15570
- data/test/integration/test_session_limitable_workflow.rb +2 -0
- data/test/orm/active_record.rb +7 -7
- data/test/test_compatibility.rb +2 -0
- data/test/test_complexity_validator.rb +246 -37
- data/test/test_database_authenticatable_patch.rb +146 -0
- data/test/test_helper.rb +7 -8
- data/test/test_install_generator.rb +1 -1
- data/test/test_paranoid_verification.rb +0 -1
- data/test/test_password_archivable.rb +34 -11
- data/test/test_password_expirable.rb +26 -26
- data/test/test_secure_validatable.rb +273 -107
- data/test/test_secure_validatable_overrides.rb +185 -0
- data/test/test_session_limitable.rb +2 -2
- data/test/tmp/config/initializers/{devise-security.rb → devise_security.rb} +3 -1
- data/test/tmp/config/locales/devise.security_extension.de.yml +2 -0
- data/test/tmp/config/locales/devise.security_extension.en.yml +2 -1
- data/test/tmp/config/locales/devise.security_extension.hi.yml +20 -20
- metadata +42 -19
- data/test/dummy/app/models/secure_user.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d2d19c261f7efb929b61e3bfdb31fbe0dce4ae5ab81d829508338ec86486f09
|
4
|
+
data.tar.gz: fa6c34683e462867b85d9fefe9132dae0891e0fca4eb1fa32fc0c1f9fc9177f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99cd7b257250d09474d2de4b1aeb2348ca1ec8ca651ff0cc11121e23a71faea5bc2085293c83fcb11d4d641d402a76977185df3bd39a2862d3cab6359a1f90ec
|
7
|
+
data.tar.gz: fc55c011517dbaaab893ebb5da71d6fcd0d65de43de21bb51620cec29104a187e6fb405f159b3ad14a5f40109c9e4a9d18695ae020f0485393fc9b5d4840968d
|
data/README.md
CHANGED
@@ -37,7 +37,7 @@ automated mass creation and brute forcing of accounts harder)
|
|
37
37
|
|
38
38
|
## Getting started
|
39
39
|
|
40
|
-
Devise Security works with Devise on Rails >= 5.
|
40
|
+
Devise Security works with Devise on Rails >= 5.2. You can add it to your
|
41
41
|
Gemfile after you successfully set up Devise (see
|
42
42
|
[Devise documentation](https://github.com/heartcombo/devise)) with:
|
43
43
|
|
@@ -89,6 +89,8 @@ Devise.setup do |config|
|
|
89
89
|
# config.expire_password_after = 3.months | true | false
|
90
90
|
|
91
91
|
# Need 1 char each of: A-Z, a-z, 0-9, and a punctuation mark or symbol
|
92
|
+
# You may use "digits" in place of "digit" and "symbols" in place of
|
93
|
+
# "symbol" based on your preference
|
92
94
|
# config.password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
|
93
95
|
|
94
96
|
# Number of old passwords in archive
|
@@ -321,7 +323,7 @@ end
|
|
321
323
|
## Requirements
|
322
324
|
|
323
325
|
- Devise (<https://github.com/heartcombo/devise>)
|
324
|
-
- Rails 5.
|
326
|
+
- Rails 5.2 onwards (<http://github.com/rails/rails>)
|
325
327
|
- recommendations:
|
326
328
|
- `autocomplete-off` (<http://github.com/phatworx/autocomplete-off>)
|
327
329
|
- `easy_captcha` (<http://github.com/phatworx/easy_captcha>)
|
@@ -17,12 +17,24 @@ class Devise::ParanoidVerificationCodeController < DeviseController
|
|
17
17
|
warden.session(scope)['paranoid_verify'] = false
|
18
18
|
set_flash_message :notice, :updated
|
19
19
|
bypass_sign_in resource, scope: scope
|
20
|
-
redirect_to
|
20
|
+
redirect_to after_paranoid_verification_code_update_path_for(resource)
|
21
21
|
else
|
22
22
|
respond_with(resource, action: :show)
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
+
# Allows you to customize where the user is redirected to after the update action
|
27
|
+
# successfully completes.
|
28
|
+
#
|
29
|
+
# Defaults to the request's original path, and then `root` if that is `nil`.
|
30
|
+
#
|
31
|
+
# @param resource [ActiveModel::Model] Devise `resource` model for logged in user.
|
32
|
+
#
|
33
|
+
# @return [String, Symbol] The path that the user will be redirected to.
|
34
|
+
def after_paranoid_verification_code_update_path_for(_resource)
|
35
|
+
stored_location_for(scope) || :root
|
36
|
+
end
|
37
|
+
|
26
38
|
private
|
27
39
|
|
28
40
|
def resource_params
|
@@ -24,17 +24,30 @@ class Devise::PasswordExpiredController < DeviseController
|
|
24
24
|
warden.session(scope)['password_expired'] = false
|
25
25
|
set_flash_message :notice, :updated
|
26
26
|
bypass_sign_in resource, scope: scope
|
27
|
-
respond_with({}, location:
|
27
|
+
respond_with({}, location: after_password_expired_update_path_for(resource))
|
28
28
|
else
|
29
29
|
clean_up_passwords(resource)
|
30
30
|
respond_with(resource, action: :show)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
+
# Allows you to customize where the user is sent to after the update action
|
35
|
+
# successfully completes.
|
36
|
+
#
|
37
|
+
# Defaults to the request's original path, and then `root` if that is `nil`.
|
38
|
+
#
|
39
|
+
# @param resource [ActiveModel::Model] Devise `resource` model for logged in user.
|
40
|
+
#
|
41
|
+
# @return [String, Symbol] The path that the user will be sent to.
|
42
|
+
def after_password_expired_update_path_for(_resource)
|
43
|
+
stored_location_for(scope) || :root
|
44
|
+
end
|
45
|
+
|
34
46
|
private
|
35
47
|
|
36
48
|
def skip_password_change
|
37
49
|
return if !resource.nil? && resource.need_change_password?
|
50
|
+
|
38
51
|
redirect_to :root
|
39
52
|
end
|
40
53
|
|
@@ -0,0 +1,41 @@
|
|
1
|
+
bg:
|
2
|
+
errors:
|
3
|
+
messages:
|
4
|
+
taken_in_past: 'е използвана и преди.'
|
5
|
+
equal_to_current_password: 'трябва да е различна от настоящата парола.'
|
6
|
+
equal_to_email: 'трябва да е различна от e-mail адреса.'
|
7
|
+
password_complexity:
|
8
|
+
digit:
|
9
|
+
one: трябва да съдържа поне една цифра
|
10
|
+
other: трябва да съдържа %{count} цифри
|
11
|
+
lower:
|
12
|
+
one: трябва да съдържа поне една малка буква
|
13
|
+
other: трябва да съдържа поне %{count} малки букви
|
14
|
+
symbol:
|
15
|
+
one: трябва да съдържа поне един пунктоационен знак или символ
|
16
|
+
other: трябва да съдържа поне %{count} пунктоационни знака или символи
|
17
|
+
upper:
|
18
|
+
one: трябва да съдържа поне една главна буква
|
19
|
+
other: трябва да съдържа поне %{count} главни букви
|
20
|
+
devise:
|
21
|
+
invalid_captcha: 'Кодът е грешен.'
|
22
|
+
invalid_security_question: 'Отговора на тайния въпрос е грешен.'
|
23
|
+
paranoid_verify:
|
24
|
+
code_required: 'Моля въведете кода, който нашия екип по поддръжката Ви е предоставил'
|
25
|
+
paranoid_verification_code:
|
26
|
+
show:
|
27
|
+
submit_verification_code: Изпрати код за потвърждение
|
28
|
+
verification_code: Код за потвърждение
|
29
|
+
submit: Изпрати
|
30
|
+
password_expired:
|
31
|
+
updated: 'Вашата нова парола е запазена.'
|
32
|
+
change_required: 'Вашата парола е изтекла. Моля подновете паролата си.'
|
33
|
+
show:
|
34
|
+
renew_your_password: Подновете паролата си
|
35
|
+
current_password: Настояща парола
|
36
|
+
new_password: Нова парола
|
37
|
+
new_password_confirmation: Потвърждение на нова парола
|
38
|
+
change_my_password: Промени паролата ми
|
39
|
+
failure:
|
40
|
+
session_limited: 'Вашето потребителско име и парола са използвани в друг браузър. Моля влезте отново за да продължите в този браузър.'
|
41
|
+
expired: 'Вашия акаунт е затворен поради неактивност. Моля свържете се с администратор.'
|
data/config/locales/de.yml
CHANGED
@@ -19,8 +19,10 @@ de:
|
|
19
19
|
other: muss mindestens %{count} Großbuchstaben enthalten
|
20
20
|
devise:
|
21
21
|
invalid_captcha: 'Die Captcha-Eingabe ist nicht gültig.'
|
22
|
+
invalid_security_question: 'Die Antwort auf die Sicherheitsfrage war ungültig.'
|
22
23
|
paranoid_verify:
|
23
24
|
code_required: 'Bitte geben Sie den Code ein, den unser Support-Team zur Verfügung gestellt hat.'
|
25
|
+
paranoid_verification_code:
|
24
26
|
show:
|
25
27
|
submit_verification_code: Bestätigungscode eingeben
|
26
28
|
verification_code: Bestätigungscode
|
data/config/locales/en.yml
CHANGED
@@ -7,7 +7,7 @@ en:
|
|
7
7
|
password_complexity:
|
8
8
|
digit:
|
9
9
|
one: must contain at least one digit
|
10
|
-
other: must contain at least %{count}
|
10
|
+
other: must contain at least %{count} digits
|
11
11
|
lower:
|
12
12
|
one: must contain at least one lower-case letter
|
13
13
|
other: must contain at least %{count} lower-case letters
|
@@ -23,6 +23,7 @@ en:
|
|
23
23
|
paranoid_verify:
|
24
24
|
code_required: 'Please enter the code our support team provided'
|
25
25
|
paranoid_verification_code:
|
26
|
+
updated: Verification code accepted
|
26
27
|
show:
|
27
28
|
submit_verification_code: Submit verification code
|
28
29
|
verification_code: Verification code
|
@@ -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
|
@@ -44,20 +44,39 @@ module Devise
|
|
44
44
|
validates :email, uniqueness: true, allow_blank: true, if: :email_changed? # check uniq for email ever
|
45
45
|
end
|
46
46
|
|
47
|
-
|
47
|
+
validates_presence_of :password, if: :password_required?
|
48
|
+
validates_confirmation_of :password, if: :password_required?
|
49
|
+
|
50
|
+
validate if: :password_required? do |record|
|
51
|
+
validates_with ActiveModel::Validations::LengthValidator,
|
52
|
+
attributes: :password,
|
53
|
+
allow_blank: true,
|
54
|
+
in: record.password_length
|
55
|
+
end
|
48
56
|
end
|
49
57
|
|
50
58
|
# extra validations
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
59
|
+
# see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
|
60
|
+
validate do |record|
|
61
|
+
if email_validation
|
62
|
+
validates_with(
|
63
|
+
EmailValidator, { attributes: :email }
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
validate if: :password_required? do |record|
|
69
|
+
validates_with(
|
70
|
+
record.password_complexity_validator.is_a?(Class) ? record.password_complexity_validator : record.password_complexity_validator.classify.constantize,
|
71
|
+
{ attributes: :password }.merge(record.password_complexity)
|
72
|
+
)
|
73
|
+
end
|
55
74
|
|
56
75
|
# don't allow use same password
|
57
76
|
validate :current_equal_password_validation
|
58
77
|
|
59
78
|
# don't allow email to equal password
|
60
|
-
validate :email_not_equal_password_validation
|
79
|
+
validate :email_not_equal_password_validation
|
61
80
|
end
|
62
81
|
end
|
63
82
|
|
@@ -74,14 +93,13 @@ module Devise
|
|
74
93
|
end
|
75
94
|
|
76
95
|
def email_not_equal_password_validation
|
77
|
-
return if
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
self.errors.add(:password, :equal_to_email) if dummy.valid_password?(password.downcase.strip)
|
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)
|
85
103
|
end
|
86
104
|
|
87
105
|
protected
|
@@ -89,6 +107,8 @@ module Devise
|
|
89
107
|
# Checks whether a password is needed or not. For validations only.
|
90
108
|
# Passwords are always required if it's a new record, or if the password
|
91
109
|
# or confirmation are being set somewhere.
|
110
|
+
#
|
111
|
+
# @return [Boolean]
|
92
112
|
def password_required?
|
93
113
|
!persisted? || !password.nil? || !password_confirmation.nil?
|
94
114
|
end
|
@@ -97,8 +117,24 @@ module Devise
|
|
97
117
|
true
|
98
118
|
end
|
99
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
|
+
|
100
129
|
module ClassMethods
|
101
|
-
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
|
+
)
|
102
138
|
|
103
139
|
private
|
104
140
|
|
@@ -1,35 +1,62 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# (NIST)[https://pages.nist.gov/800-63-3/sp800-63b.html#appA] does not recommend
|
4
|
+
# the use of a password complexity checks because...
|
5
|
+
#
|
6
|
+
# > Length and complexity requirements beyond those recommended here
|
7
|
+
# > significantly increase the difficulty of memorized secrets and increase user
|
8
|
+
# > frustration. As a result, users often work around these restrictions in a
|
9
|
+
# > way that is counterproductive. Furthermore, other mitigations such as
|
10
|
+
# > blacklists, secure hashed storage, and rate limiting are more effective at
|
11
|
+
# > preventing modern brute-force attacks. Therefore, no additional complexity
|
12
|
+
# > requirements are imposed.
|
13
|
+
#
|
4
14
|
# Options:
|
5
|
-
# - digit
|
6
|
-
#
|
7
|
-
# - lower
|
8
|
-
# - symbol
|
9
|
-
#
|
10
|
-
# - upper
|
15
|
+
# - `digit | digits`: minimum number of digits in the validated string. Uses
|
16
|
+
# the `digit` localization key.
|
17
|
+
# - `lower`: minimum number of lower-case letters in the validated string
|
18
|
+
# - `symbol | symbols`: minimum number of punctuation characters or symbols in
|
19
|
+
# the validated string. Uses the `symbol` localization key.
|
20
|
+
# - `upper`: minimum number of upper-case letters in the validated string
|
11
21
|
class DeviseSecurity::PasswordComplexityValidator < ActiveModel::EachValidator
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
22
|
+
# A Hash of the possible valid patterns that can be checked against. The keys
|
23
|
+
# for this Hash are singular symbols corresponding to entries in the
|
24
|
+
# localization files. Override or redefine this method if you want to include
|
25
|
+
# custom patterns (e.g., `letter: /\p{Alpha}/` for all letters).
|
26
|
+
#
|
27
|
+
# @return [Hash<Symbol,Regexp>]
|
28
|
+
def patterns
|
29
|
+
{
|
30
|
+
digit: /\p{Digit}/,
|
31
|
+
lower: /\p{Lower}/,
|
32
|
+
symbol: /\p{Punct}|\p{S}/,
|
33
|
+
upper: /\p{Upper}/
|
34
|
+
}
|
35
|
+
end
|
20
36
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
37
|
+
# Validate the complexity of the password. This validation does not check to
|
38
|
+
# ensure the password is not blank. That is the responsibility of other
|
39
|
+
# validations. This validator will also ignore any patterns that are not
|
40
|
+
# explicitly configured to be used or whose minimum limits are less than 1.
|
41
|
+
#
|
42
|
+
# @param record [ActiveModel::Model]
|
43
|
+
# @param attribute [Symbol]
|
44
|
+
# @param password [String]
|
45
|
+
def validate_each(record, attribute, password)
|
46
|
+
return if password.blank?
|
25
47
|
|
26
|
-
|
27
|
-
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
48
|
+
options.sort.each do |pattern_name, minimum|
|
49
|
+
normalized_option = pattern_name.to_s.singularize.to_sym
|
31
50
|
|
32
|
-
|
33
|
-
|
51
|
+
next unless patterns.key?(normalized_option)
|
52
|
+
next unless minimum.positive?
|
53
|
+
next if password.scan(patterns[normalized_option]).size >= minimum
|
54
|
+
|
55
|
+
record.errors.add(
|
56
|
+
attribute,
|
57
|
+
:"password_complexity.#{normalized_option}",
|
58
|
+
count: minimum
|
59
|
+
)
|
60
|
+
end
|
34
61
|
end
|
35
62
|
end
|
data/lib/devise-security.rb
CHANGED
@@ -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
|
@@ -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
|
@@ -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
|
@@ -76,6 +76,10 @@ class Devise::PasswordExpiredControllerTest < ActionController::TestCase
|
|
76
76
|
assert_response :success
|
77
77
|
assert_template :show
|
78
78
|
assert_equal response.media_type, 'text/html'
|
79
|
+
assert_includes(
|
80
|
+
response.body,
|
81
|
+
'Password confirmation doesn't match Password'
|
82
|
+
)
|
79
83
|
end
|
80
84
|
|
81
85
|
test 'update password using JSON format' do
|
@@ -108,3 +112,37 @@ class Devise::PasswordExpiredControllerTest < ActionController::TestCase
|
|
108
112
|
assert_nil response.media_type, 'No Content-Type header should be set for No Content response'
|
109
113
|
end
|
110
114
|
end
|
115
|
+
|
116
|
+
class PasswordExpiredCustomRedirectTest < ActionController::TestCase
|
117
|
+
include Devise::Test::ControllerHelpers
|
118
|
+
tests Overrides::PasswordExpiredController
|
119
|
+
|
120
|
+
setup do
|
121
|
+
@controller.class.respond_to :json, :xml
|
122
|
+
@request.env['devise.mapping'] = Devise.mappings[:password_expired_user]
|
123
|
+
@user = PasswordExpiredUser.create!(
|
124
|
+
username: 'hello',
|
125
|
+
email: 'hello@path.travel',
|
126
|
+
password: 'Password4',
|
127
|
+
password_changed_at: 4.months.ago,
|
128
|
+
confirmed_at: 5.months.ago,
|
129
|
+
)
|
130
|
+
assert @user.valid?
|
131
|
+
assert @user.need_change_password?
|
132
|
+
|
133
|
+
sign_in(@user)
|
134
|
+
end
|
135
|
+
|
136
|
+
test 'update password with custom redirect route' do
|
137
|
+
put :update,
|
138
|
+
params: {
|
139
|
+
password_expired_user: {
|
140
|
+
current_password: 'Password4',
|
141
|
+
password: 'Password5',
|
142
|
+
password_confirmation: 'Password5',
|
143
|
+
},
|
144
|
+
}
|
145
|
+
|
146
|
+
assert_redirected_to '/cookies'
|
147
|
+
end
|
148
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DatabaseAuthenticatableFields
|
2
4
|
extend ::ActiveSupport::Concern
|
3
5
|
|
@@ -6,10 +8,9 @@ module DatabaseAuthenticatableFields
|
|
6
8
|
|
7
9
|
## Database authenticatable
|
8
10
|
field :username, type: String
|
9
|
-
field :email, type: String, default:
|
10
|
-
#validates_presence_of :email
|
11
|
+
field :email, type: String, default: ''
|
11
12
|
|
12
|
-
field :encrypted_password, type: String, default:
|
13
|
+
field :encrypted_password, type: String, default: ''
|
13
14
|
validates_presence_of :encrypted_password
|
14
15
|
|
15
16
|
include Mongoid::Timestamps
|