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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/app/controllers/devise/paranoid_verification_code_controller.rb +13 -1
  4. data/app/controllers/devise/password_expired_controller.rb +14 -1
  5. data/config/locales/bg.yml +41 -0
  6. data/config/locales/de.yml +2 -0
  7. data/config/locales/en.yml +2 -1
  8. data/lib/devise-security/models/database_authenticatable_patch.rb +15 -5
  9. data/lib/devise-security/models/password_archivable.rb +2 -2
  10. data/lib/devise-security/models/secure_validatable.rb +51 -15
  11. data/lib/devise-security/validators/password_complexity_validator.rb +53 -26
  12. data/lib/devise-security/version.rb +1 -1
  13. data/lib/devise-security.rb +7 -2
  14. data/lib/generators/templates/devise_security.rb +3 -1
  15. data/test/controllers/test_paranoid_verification_code_controller.rb +68 -0
  16. data/test/controllers/test_password_expired_controller.rb +38 -0
  17. data/test/dummy/app/controllers/overrides/paranoid_verification_code_controller.rb +7 -0
  18. data/test/dummy/app/controllers/overrides/password_expired_controller.rb +7 -0
  19. data/test/dummy/app/controllers/widgets_controller.rb +3 -0
  20. data/test/dummy/app/models/application_user_record.rb +2 -1
  21. data/test/dummy/app/models/mongoid/confirmable_fields.rb +2 -0
  22. data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +4 -3
  23. data/test/dummy/app/models/mongoid/expirable_fields.rb +2 -0
  24. data/test/dummy/app/models/mongoid/lockable_fields.rb +2 -0
  25. data/test/dummy/app/models/mongoid/mappings.rb +4 -2
  26. data/test/dummy/app/models/mongoid/omniauthable_fields.rb +2 -0
  27. data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +2 -0
  28. data/test/dummy/app/models/mongoid/password_archivable_fields.rb +2 -0
  29. data/test/dummy/app/models/mongoid/password_expirable_fields.rb +2 -0
  30. data/test/dummy/app/models/mongoid/recoverable_fields.rb +2 -0
  31. data/test/dummy/app/models/mongoid/registerable_fields.rb +4 -2
  32. data/test/dummy/app/models/mongoid/rememberable_fields.rb +2 -0
  33. data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +2 -0
  34. data/test/dummy/app/models/mongoid/security_questionable_fields.rb +2 -0
  35. data/test/dummy/app/models/mongoid/session_limitable_fields.rb +2 -0
  36. data/test/dummy/app/models/mongoid/timeoutable_fields.rb +2 -0
  37. data/test/dummy/app/models/mongoid/trackable_fields.rb +2 -0
  38. data/test/dummy/app/models/mongoid/validatable_fields.rb +2 -0
  39. data/test/dummy/app/models/paranoid_verification_user.rb +26 -0
  40. data/test/dummy/app/models/password_expired_user.rb +26 -0
  41. data/test/dummy/app/models/user.rb +1 -2
  42. data/test/dummy/app/models/widget.rb +1 -3
  43. data/test/dummy/app/mongoid/one_user.rb +5 -5
  44. data/test/dummy/app/mongoid/user_on_engine.rb +2 -2
  45. data/test/dummy/app/mongoid/user_on_main_app.rb +2 -2
  46. data/test/dummy/app/mongoid/user_with_validations.rb +3 -3
  47. data/test/dummy/app/mongoid/user_without_email.rb +3 -3
  48. data/test/dummy/config/application.rb +4 -4
  49. data/test/dummy/config/boot.rb +1 -1
  50. data/test/dummy/config/environment.rb +1 -1
  51. data/test/dummy/config/locales/en.yml +10 -0
  52. data/test/dummy/config/routes.rb +2 -0
  53. data/test/dummy/db/migrate/20120508165529_create_tables.rb +3 -3
  54. data/test/dummy/lib/shared_expirable_columns.rb +1 -0
  55. data/test/dummy/lib/shared_security_questions_fields.rb +1 -0
  56. data/test/dummy/lib/shared_user.rb +17 -6
  57. data/test/dummy/lib/shared_user_without_email.rb +2 -1
  58. data/test/dummy/lib/shared_user_without_omniauth.rb +12 -3
  59. data/test/dummy/lib/shared_verification_fields.rb +1 -0
  60. data/test/dummy/log/development.log +0 -883
  61. data/test/dummy/log/test.log +95414 -15570
  62. data/test/integration/test_session_limitable_workflow.rb +2 -0
  63. data/test/orm/active_record.rb +7 -7
  64. data/test/test_compatibility.rb +2 -0
  65. data/test/test_complexity_validator.rb +246 -37
  66. data/test/test_database_authenticatable_patch.rb +146 -0
  67. data/test/test_helper.rb +7 -8
  68. data/test/test_install_generator.rb +1 -1
  69. data/test/test_paranoid_verification.rb +0 -1
  70. data/test/test_password_archivable.rb +34 -11
  71. data/test/test_password_expirable.rb +26 -26
  72. data/test/test_secure_validatable.rb +273 -107
  73. data/test/test_secure_validatable_overrides.rb +185 -0
  74. data/test/test_session_limitable.rb +2 -2
  75. data/test/tmp/config/initializers/{devise-security.rb → devise_security.rb} +3 -1
  76. data/test/tmp/config/locales/devise.security_extension.de.yml +2 -0
  77. data/test/tmp/config/locales/devise.security_extension.en.yml +2 -1
  78. data/test/tmp/config/locales/devise.security_extension.hi.yml +20 -20
  79. metadata +42 -19
  80. 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: 1d065158ce85c823918ca0fb7ad40382ca9957f7c5e0847e1fec86e1eaed0ffb
4
- data.tar.gz: fa07606b583076da6b68ceeddf70f74f80c05f3adf752a057b8f935c9af68fb6
3
+ metadata.gz: 9d2d19c261f7efb929b61e3bfdb31fbe0dce4ae5ab81d829508338ec86486f09
4
+ data.tar.gz: fa6c34683e462867b85d9fefe9132dae0891e0fca4eb1fa32fc0c1f9fc9177f3
5
5
  SHA512:
6
- metadata.gz: 7f18a70374b20c80908006811184fc4757c4f678e11ff226d60b78ff0a3c1cf2612382185911b23692c6c7ed1553914f1361ab1243948ad1e1ff3ac91fdb5ab7
7
- data.tar.gz: 68e392e9f0049659ad62977a0bb31910d4942b26ab24fab11b28e1e875286f2b48e9da20dd952f94bc4ac8350b1cd5199d9984e431d4664c865544a095b274b8
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.0. You can add it to your
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.0 onwards (<http://github.com/rails/rails>)
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 stored_location_for(scope) || :root
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: stored_location_for(scope) || :root)
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: 'Вашия акаунт е затворен поради неактивност. Моля свържете се с администратор.'
@@ -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
@@ -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} numerals
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?(current_password) && new_password.present? && new_password_confirmation.present?
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
- self.valid?
17
- self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
18
- self.errors.add(:password, new_password.blank? ? :blank : :invalid)
19
- self.errors.add(:password_confirmation, new_password_confirmation.blank? ? :blank : :invalid)
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.order(created_at: :desc).limit(max_old_passwords).pluck(:encrypted_password)
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.order(created_at: :desc).offset(max_old_passwords).destroy_all
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
- validates :password, presence: true, length: password_length, confirmation: true, if: :password_required?
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
- validates :email, email: email_validation if email_validation # see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
52
- validates :password,
53
- 'devise_security/password_complexity': password_complexity,
54
- if: :password_required?
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 unless allow_passwords_equal_to_email
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 password.blank? || (!new_record? && !will_save_change_to_encrypted_password?)
78
- dummy = self.class.new.tap do |user|
79
- user.password_salt = password_salt if respond_to?(:password_salt)
80
- # whether case_insensitive_keys or strip_whitespace_keys include email or not, any
81
- # variation of the email should not be a supported password
82
- user.password = email.downcase.strip
83
- end
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(self, :password_complexity, :password_length, :email_validation, :allow_passwords_equal_to_email)
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
- # Password complexity validator
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: minimum number of digits in the validated string
6
- # - digits: minimum number of digits in the validated string
7
- # - lower: minimum number of lower-case letters in the validated string
8
- # - symbol: minimum number of punctuation characters or symbols in the validated string
9
- # - symbols: minimum number of punctuation characters or symbols in the validated string
10
- # - upper: minimum number of upper-case letters in the validated string
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
- PATTERNS = {
13
- digit: /\p{Digit}/,
14
- digits: /\p{Digit}/,
15
- lower: /\p{Lower}/,
16
- symbol: /\p{Punct}|\p{S}/,
17
- symbols: /\p{Punct}|\p{S}/,
18
- upper: /\p{Upper}/
19
- }.freeze
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
- def validate_each(record, attribute, value)
22
- active_pattern_keys.each do |key|
23
- minimum = [0, options[key].to_i].max
24
- pattern = Regexp.new PATTERNS[key]
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
- unless (value || '').scan(pattern).size >= minimum
27
- record.errors.add attribute, :"password_complexity.#{key}", count: minimum
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
- def active_pattern_keys
33
- options.keys & PATTERNS.keys
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeviseSecurity
4
- VERSION = '0.16.0'
4
+ VERSION = '0.17.0'
5
5
  end
@@ -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 pasword expiration with +false+
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 for strongness
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 and 0-9
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&#39;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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Overrides::ParanoidVerificationCodeController < Devise::ParanoidVerificationCodeController
4
+ def after_paranoid_verification_code_update_path_for(_)
5
+ '/cats'
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Overrides::PasswordExpiredController < Devise::PasswordExpiredController
4
+ def after_password_expired_update_path_for(_)
5
+ '/cookies'
6
+ end
7
+ end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class WidgetsController < ApplicationController
2
4
  before_action :authenticate_user!
5
+
3
6
  def show
4
7
  render plain: 'success'
5
8
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  if DEVISE_ORM == :active_record
3
- class ApplicationUserRecord < ActiveRecord::Base
4
+ class ApplicationUserRecord < ApplicationRecord
4
5
  self.table_name = 'users'
5
6
  end
6
7
  else
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ConfirmableFields
2
4
  extend ::ActiveSupport::Concern
3
5
 
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExpirableFields
2
4
  extend ::ActiveSupport::Concern
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module LockableFields
2
4
  extend ::ActiveSupport::Concern
3
5