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,85 +1,319 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'test_helper'
|
2
|
-
require 'rails_email_validator'
|
3
4
|
|
4
5
|
class TestSecureValidatable < ActiveSupport::TestCase
|
5
|
-
class User <
|
6
|
-
devise :database_authenticatable, :
|
7
|
-
|
6
|
+
class User < ApplicationRecord
|
7
|
+
devise :database_authenticatable, :secure_validatable
|
8
|
+
include ::Mongoid::Mappings if DEVISE_ORM == :mongoid
|
8
9
|
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
end
|
11
|
+
class EmailNotRequiredUser < User
|
12
|
+
protected
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
user = User.create password: 'passWord1', password_confirmation: 'passWord1'
|
17
|
-
assert_equal(false, user.valid?)
|
18
|
-
assert_equal([msg], user.errors.full_messages)
|
19
|
-
assert_raises(ActiveRecord::RecordInvalid) do
|
20
|
-
user.save!
|
14
|
+
def email_required?
|
15
|
+
false
|
21
16
|
end
|
22
17
|
end
|
23
18
|
|
19
|
+
test 'email cannot be blank upon creation' do
|
20
|
+
user = User.new(
|
21
|
+
password: 'Password1!', password_confirmation: 'Password1!'
|
22
|
+
)
|
23
|
+
|
24
|
+
assert user.invalid?
|
25
|
+
assert_equal(["Email can't be blank"], user.errors.full_messages)
|
26
|
+
end
|
27
|
+
|
28
|
+
test 'email can be blank upon creation if email not required' do
|
29
|
+
user = EmailNotRequiredUser.new(
|
30
|
+
password: 'Password1!', password_confirmation: 'Password1!'
|
31
|
+
)
|
32
|
+
|
33
|
+
assert user.valid?
|
34
|
+
end
|
35
|
+
|
36
|
+
test 'email cannot be updated to be blank' do
|
37
|
+
user = User.new(
|
38
|
+
email: 'bob@microsoft.com',
|
39
|
+
password: 'Password1!',
|
40
|
+
password_confirmation: 'Password1!'
|
41
|
+
)
|
42
|
+
|
43
|
+
assert user.valid?
|
44
|
+
|
45
|
+
user.email = nil
|
46
|
+
|
47
|
+
assert user.invalid?
|
48
|
+
assert_equal(["Email can't be blank"], user.errors.full_messages)
|
49
|
+
end
|
50
|
+
|
51
|
+
test 'email can be updated to be blank if email not required' do
|
52
|
+
user = EmailNotRequiredUser.new(
|
53
|
+
email: 'bob@microsoft.com',
|
54
|
+
password: 'Password1!',
|
55
|
+
password_confirmation: 'Password1!'
|
56
|
+
)
|
57
|
+
|
58
|
+
assert user.valid?
|
59
|
+
|
60
|
+
user.email = nil
|
61
|
+
|
62
|
+
assert user.valid?
|
63
|
+
end
|
64
|
+
|
24
65
|
test 'email must be valid' do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
66
|
+
user = User.new(
|
67
|
+
email: 'bob', password: 'Password1!', password_confirmation: 'Password1!'
|
68
|
+
)
|
69
|
+
|
70
|
+
assert user.invalid?
|
71
|
+
assert_equal(['Email is invalid'], user.errors.full_messages)
|
72
|
+
end
|
73
|
+
|
74
|
+
test 'validate both email and password' do
|
75
|
+
user = User.new(
|
76
|
+
email: 'bob',
|
77
|
+
password: 'password1!',
|
78
|
+
password_confirmation: 'password1!'
|
79
|
+
)
|
80
|
+
|
81
|
+
assert user.invalid?
|
82
|
+
assert_equal(
|
83
|
+
[
|
84
|
+
'Email is invalid',
|
85
|
+
'Password must contain at least one upper-case letter'
|
86
|
+
],
|
87
|
+
user.errors.full_messages
|
88
|
+
)
|
32
89
|
end
|
33
90
|
|
34
|
-
test '
|
35
|
-
|
36
|
-
|
37
|
-
|
91
|
+
test 'password cannot be blank upon creation' do
|
92
|
+
user = User.new(email: 'bob@microsoft.com')
|
93
|
+
|
94
|
+
msgs = ["Password can't be blank"]
|
95
|
+
|
96
|
+
msgs << "Encrypted password can't be blank" if DEVISE_ORM == :mongoid
|
97
|
+
|
98
|
+
assert user.invalid?
|
38
99
|
assert_equal(msgs, user.errors.full_messages)
|
39
|
-
|
100
|
+
end
|
101
|
+
|
102
|
+
test 'password cannot be updated to be blank' do
|
103
|
+
user = User.new(
|
104
|
+
email: 'bob@microsoft.com',
|
105
|
+
password: 'Password1!',
|
106
|
+
password_confirmation: 'Password1!'
|
107
|
+
)
|
108
|
+
|
109
|
+
assert user.valid?
|
110
|
+
|
111
|
+
user.password = nil
|
112
|
+
user.password_confirmation = nil
|
113
|
+
|
114
|
+
assert user.invalid?
|
115
|
+
assert_equal(["Password can't be blank"], user.errors.full_messages)
|
116
|
+
end
|
117
|
+
|
118
|
+
test 'password_confirmation must match password' do
|
119
|
+
user = User.new(
|
120
|
+
email: 'bob@microsoft.com',
|
121
|
+
password: 'Password1!',
|
122
|
+
password_confirmation: 'not the same password'
|
123
|
+
)
|
124
|
+
|
125
|
+
assert user.invalid?
|
126
|
+
assert_equal(
|
127
|
+
["Password confirmation doesn't match Password"],
|
128
|
+
user.errors.full_messages
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
test 'password_confirmation cannot be blank' do
|
133
|
+
user = User.new(
|
134
|
+
email: 'bob@microsoft.com',
|
135
|
+
password: 'Password1!',
|
136
|
+
password_confirmation: ''
|
137
|
+
)
|
138
|
+
|
139
|
+
assert user.invalid?
|
140
|
+
assert_equal(
|
141
|
+
["Password confirmation doesn't match Password"],
|
142
|
+
user.errors.full_messages
|
143
|
+
)
|
144
|
+
end
|
145
|
+
|
146
|
+
test 'password_confirmation can be skipped' do
|
147
|
+
user = User.new(
|
148
|
+
email: 'bob@microsoft.com',
|
149
|
+
password: 'Password1!',
|
150
|
+
password_confirmation: nil
|
151
|
+
)
|
152
|
+
|
153
|
+
assert user.valid?
|
40
154
|
end
|
41
155
|
|
42
156
|
test 'password must have capital letter' do
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
157
|
+
user = User.new(
|
158
|
+
email: 'bob@microsoft.com',
|
159
|
+
password: 'password1',
|
160
|
+
password_confirmation: 'password1'
|
161
|
+
)
|
162
|
+
|
163
|
+
assert user.invalid?
|
164
|
+
assert_equal(
|
165
|
+
['Password must contain at least one upper-case letter'],
|
166
|
+
user.errors.full_messages
|
167
|
+
)
|
48
168
|
end
|
49
169
|
|
50
170
|
test 'password must have lowercase letter' do
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
171
|
+
user = User.new(
|
172
|
+
email: 'bob@microsoft.com',
|
173
|
+
password: 'PASSWORD1',
|
174
|
+
password_confirmation: 'PASSWORD1'
|
175
|
+
)
|
176
|
+
|
177
|
+
assert user.invalid?
|
178
|
+
assert_equal(
|
179
|
+
['Password must contain at least one lower-case letter'],
|
180
|
+
user.errors.full_messages
|
181
|
+
)
|
56
182
|
end
|
57
183
|
|
58
184
|
test 'password must have number' do
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
185
|
+
user = User.new(
|
186
|
+
email: 'bob@microsoft.com',
|
187
|
+
password: 'PASSword',
|
188
|
+
password_confirmation: 'PASSword'
|
189
|
+
)
|
190
|
+
|
191
|
+
assert user.invalid?
|
192
|
+
assert_equal(
|
193
|
+
['Password must contain at least one digit'],
|
194
|
+
user.errors.full_messages
|
195
|
+
)
|
196
|
+
end
|
197
|
+
|
198
|
+
test 'password must meet minimum length' do
|
199
|
+
user = User.new(
|
200
|
+
email: 'bob@microsoft.com',
|
201
|
+
password: 'Pa3zZ',
|
202
|
+
password_confirmation: 'Pa3zZ'
|
203
|
+
)
|
204
|
+
|
205
|
+
assert user.invalid?
|
206
|
+
assert_equal(
|
207
|
+
['Password is too short (minimum is 7 characters)'],
|
208
|
+
user.errors.full_messages
|
209
|
+
)
|
64
210
|
end
|
65
211
|
|
66
|
-
test '
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
212
|
+
test "new user can't use existing user's email" do
|
213
|
+
options = {
|
214
|
+
email: 'bob@microsoft.com',
|
215
|
+
password: 'Password1!',
|
216
|
+
password_confirmation: 'Password1!'
|
217
|
+
}
|
218
|
+
User.create!(options)
|
219
|
+
user = User.new(options)
|
220
|
+
|
221
|
+
assert user.invalid?
|
222
|
+
assert_equal(['Email has already been taken'], user.errors.full_messages)
|
72
223
|
end
|
73
224
|
|
74
|
-
test '
|
225
|
+
test "new user can't use existing user's email with different casing" do
|
75
226
|
options = {
|
76
|
-
email: '
|
77
|
-
password: '
|
78
|
-
password_confirmation: '
|
227
|
+
email: 'bob@microsoft.com',
|
228
|
+
password: 'Password1!',
|
229
|
+
password_confirmation: 'Password1!'
|
79
230
|
}
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
231
|
+
User.create!(options)
|
232
|
+
options[:email] = 'BOB@MICROSOFT.COM'
|
233
|
+
user = User.new(options)
|
234
|
+
|
235
|
+
assert user.invalid?
|
236
|
+
assert_equal(['Email has already been taken'], user.errors.full_messages)
|
237
|
+
end
|
238
|
+
|
239
|
+
test 'password cannot equal email for new user' do
|
240
|
+
user = User.new(
|
241
|
+
email: 'Bob1@microsoft.com',
|
242
|
+
password: 'Bob1@microsoft.com',
|
243
|
+
password_confirmation: 'Bob1@microsoft.com'
|
244
|
+
)
|
245
|
+
|
246
|
+
assert user.invalid?
|
247
|
+
assert_equal(
|
248
|
+
['Password must be different than the email.'],
|
249
|
+
user.errors.full_messages
|
250
|
+
)
|
251
|
+
end
|
252
|
+
|
253
|
+
test 'password cannot equal case sensitive version of email for new user' do
|
254
|
+
user = User.new(
|
255
|
+
email: 'bob1@microsoft.com',
|
256
|
+
password: 'BoB1@microsoft.com',
|
257
|
+
password_confirmation: 'BoB1@microsoft.com'
|
258
|
+
)
|
259
|
+
|
260
|
+
assert user.invalid?
|
261
|
+
assert_equal(
|
262
|
+
['Password must be different than the email.'],
|
263
|
+
user.errors.full_messages
|
264
|
+
)
|
265
|
+
end
|
266
|
+
|
267
|
+
test 'password cannot equal email with spaces for new user' do
|
268
|
+
user = User.new(
|
269
|
+
email: 'Bob1@microsoft.com',
|
270
|
+
password: 'Bob1@microsoft.com ',
|
271
|
+
password_confirmation: 'Bob1@microsoft.com '
|
272
|
+
)
|
273
|
+
|
274
|
+
assert user.invalid?
|
275
|
+
assert_equal(
|
276
|
+
['Password must be different than the email.'],
|
277
|
+
user.errors.full_messages
|
278
|
+
)
|
279
|
+
end
|
280
|
+
|
281
|
+
test 'password cannot equal case sensitive version of email with spaces '\
|
282
|
+
'for new user' do
|
283
|
+
user = User.new(
|
284
|
+
email: 'Bob1@microsoft.com',
|
285
|
+
password: ' boB1@microsoft.com ',
|
286
|
+
password_confirmation: ' boB1@microsoft.com '
|
287
|
+
)
|
288
|
+
|
289
|
+
assert user.invalid?
|
290
|
+
assert_equal(
|
291
|
+
['Password must be different than the email.'],
|
292
|
+
user.errors.full_messages
|
293
|
+
)
|
294
|
+
end
|
295
|
+
|
296
|
+
test 'new password cannot equal current password' do
|
297
|
+
user = User.create(
|
298
|
+
email: 'bob@microsoft.com',
|
299
|
+
password: 'Password1!',
|
300
|
+
password_confirmation: 'Password1!'
|
301
|
+
)
|
302
|
+
|
303
|
+
user.password = 'Password1!'
|
304
|
+
|
305
|
+
assert user.invalid?
|
306
|
+
assert_equal(
|
307
|
+
['Password must be different than the current password.'],
|
308
|
+
user.errors.full_messages
|
309
|
+
)
|
310
|
+
end
|
311
|
+
|
312
|
+
test 'should not be included in objects with invalid API' do
|
313
|
+
error = assert_raise RuntimeError do
|
314
|
+
class ::Dog; include Devise::Models::SecureValidatable; end
|
315
|
+
end
|
316
|
+
|
317
|
+
assert_equal('Could not use SecureValidatable on Dog', error.message)
|
84
318
|
end
|
85
319
|
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class TestSecureValidatableOverrides < ActiveSupport::TestCase
|
6
|
+
class ::CustomClassPasswordValidator < DeviseSecurity::PasswordComplexityValidator
|
7
|
+
def patterns
|
8
|
+
super.merge(letter: /\p{Alpha}/)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class ::CustomInstancePasswordValidator < DeviseSecurity::PasswordComplexityValidator
|
13
|
+
# Add a pattern for alphanumeric characters. See
|
14
|
+
# [en.yml](file:///./test/dummy/config/locales/en.yml) for translations used in
|
15
|
+
# tests.
|
16
|
+
def patterns
|
17
|
+
super.merge(alnum: /\p{Alnum}/)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class User < ApplicationRecord
|
22
|
+
devise :database_authenticatable, :secure_validatable
|
23
|
+
include ::Mongoid::Mappings if DEVISE_ORM == :mongoid
|
24
|
+
end
|
25
|
+
|
26
|
+
class ClassLevelOverrideUser < User
|
27
|
+
self.allow_passwords_equal_to_email = true
|
28
|
+
self.email_validation = false
|
29
|
+
self.password_complexity = { symbol: 1, letter: 1 }
|
30
|
+
self.password_complexity_validator = 'custom_class_password_validator'
|
31
|
+
self.password_length = 10..100
|
32
|
+
end
|
33
|
+
|
34
|
+
class InstanceLevelOverrideUser < ClassLevelOverrideUser
|
35
|
+
def allow_passwords_equal_to_email
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def email_validation
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def password_complexity
|
44
|
+
{ symbol: 2, alnum: 1 }
|
45
|
+
end
|
46
|
+
|
47
|
+
def password_length
|
48
|
+
11..100
|
49
|
+
end
|
50
|
+
|
51
|
+
def password_complexity_validator
|
52
|
+
'CustomInstancePasswordValidator'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
test 'email equal to password can be overridden at the class level' do
|
57
|
+
user = ClassLevelOverrideUser.new(
|
58
|
+
email: 'bob1!@microsoft.com',
|
59
|
+
password: 'bob1!@microsoft.com',
|
60
|
+
password_confirmation: 'bob1!@microsoft.com'
|
61
|
+
)
|
62
|
+
|
63
|
+
assert user.valid?
|
64
|
+
end
|
65
|
+
|
66
|
+
test 'email equal to password can be overridden at the instance level' do
|
67
|
+
user = InstanceLevelOverrideUser.new(
|
68
|
+
email: 'bob1!@microsoft.com',
|
69
|
+
password: 'bob1!@microsoft.com',
|
70
|
+
password_confirmation: 'bob1!@microsoft.com'
|
71
|
+
)
|
72
|
+
|
73
|
+
assert user.valid?
|
74
|
+
end
|
75
|
+
|
76
|
+
test 'email validation can be overridden at the class level' do
|
77
|
+
user = ClassLevelOverrideUser.new(
|
78
|
+
email: 'bob1!@f.com',
|
79
|
+
password: 'Pa3zZ1!!aaaaaa',
|
80
|
+
password_confirmation: 'Pa3zZ1!!aaaaaa'
|
81
|
+
)
|
82
|
+
|
83
|
+
assert user.valid?
|
84
|
+
end
|
85
|
+
|
86
|
+
test 'email validation can be overridden at the instance level' do
|
87
|
+
user = InstanceLevelOverrideUser.new(
|
88
|
+
email: 'bob1!@f.com',
|
89
|
+
password: 'Pa3zZ1!!aaaaaa',
|
90
|
+
password_confirmation: 'Pa3zZ1!!aaaaaa'
|
91
|
+
)
|
92
|
+
|
93
|
+
assert user.valid?
|
94
|
+
end
|
95
|
+
|
96
|
+
test 'password complexity can be overridden at the class level' do
|
97
|
+
user = ClassLevelOverrideUser.new(
|
98
|
+
email: 'bob@microsoft.com',
|
99
|
+
password: 'PASSwordddd',
|
100
|
+
password_confirmation: 'PASSwordddd'
|
101
|
+
)
|
102
|
+
|
103
|
+
assert user.invalid?
|
104
|
+
assert_equal(
|
105
|
+
['Password must contain at least one punctuation mark or symbol'],
|
106
|
+
user.errors.full_messages
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
test 'password complexity can be overridden at the instance level' do
|
111
|
+
user = InstanceLevelOverrideUser.new(
|
112
|
+
email: 'bob@microsoft.com',
|
113
|
+
password: 'PASSwordddd',
|
114
|
+
password_confirmation: 'PASSwordddd'
|
115
|
+
)
|
116
|
+
|
117
|
+
assert user.invalid?
|
118
|
+
assert_equal(
|
119
|
+
['Password must contain at least 2 punctuation marks or symbols'],
|
120
|
+
user.errors.full_messages
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
test 'password length can be overridden at the class level' do
|
125
|
+
user = ClassLevelOverrideUser.new(
|
126
|
+
email: 'bob@microsoft.com',
|
127
|
+
password: 'Pa3zZ1!',
|
128
|
+
password_confirmation: 'Pa3zZ1!'
|
129
|
+
)
|
130
|
+
|
131
|
+
assert user.invalid?
|
132
|
+
assert_equal(
|
133
|
+
['Password is too short (minimum is 10 characters)'],
|
134
|
+
user.errors.full_messages
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
test 'password length can be overridden at the instance level' do
|
139
|
+
user = InstanceLevelOverrideUser.new(
|
140
|
+
email: 'bob@microsoft.com',
|
141
|
+
password: 'Pa3zZ1!!',
|
142
|
+
password_confirmation: 'Pa3zZ1!!'
|
143
|
+
)
|
144
|
+
|
145
|
+
assert user.invalid?
|
146
|
+
assert_equal(
|
147
|
+
['Password is too short (minimum is 11 characters)'],
|
148
|
+
user.errors.full_messages
|
149
|
+
)
|
150
|
+
end
|
151
|
+
|
152
|
+
test 'password validator can be overridden at the instance level' do
|
153
|
+
password = '!' * 11 # 11 characters, all symbols
|
154
|
+
user = InstanceLevelOverrideUser.new(
|
155
|
+
email: 'bob@microsoft.com',
|
156
|
+
password: password,
|
157
|
+
password_confirmation: password
|
158
|
+
)
|
159
|
+
|
160
|
+
assert user.invalid?
|
161
|
+
# This validation error only occurs when the CustomInstancePasswordValidator
|
162
|
+
# is used.
|
163
|
+
assert_equal(
|
164
|
+
['Password must contain at least one letter or number'],
|
165
|
+
user.errors.full_messages
|
166
|
+
)
|
167
|
+
end
|
168
|
+
|
169
|
+
test 'password validator can be overridden at the class level' do
|
170
|
+
password = '!' * 10 # 10 characters, all symbols
|
171
|
+
user = ClassLevelOverrideUser.new(
|
172
|
+
email: 'bob@microsoft.com',
|
173
|
+
password: password,
|
174
|
+
password_confirmation: password
|
175
|
+
)
|
176
|
+
|
177
|
+
assert user.invalid?
|
178
|
+
# This validation error only occurs when the CustomClassPasswordValidator
|
179
|
+
# is used.
|
180
|
+
assert_equal(
|
181
|
+
['Password must contain at least one letter'],
|
182
|
+
user.errors.full_messages
|
183
|
+
)
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class TestSessionLimitable < ActiveSupport::TestCase
|
6
|
+
class ModifiedUser < User
|
7
|
+
def skip_session_limitable?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
test 'check is not skipped by default' do
|
13
|
+
user = User.new(email: 'bob@microsoft.com', password: 'password1', password_confirmation: 'password1')
|
14
|
+
assert_not(user.skip_session_limitable?)
|
15
|
+
end
|
16
|
+
|
17
|
+
test 'default check can be overridden by record instance' do
|
18
|
+
modified_user = ModifiedUser.new(email: 'bob2@microsoft.com', password: 'password1', password_confirmation: 'password1')
|
19
|
+
assert(modified_user.skip_session_limitable?)
|
20
|
+
end
|
21
|
+
|
22
|
+
class SessionLimitableUser < User
|
23
|
+
devise :session_limitable
|
24
|
+
include ::Mongoid::Mappings if DEVISE_ORM == :mongoid
|
25
|
+
end
|
26
|
+
|
27
|
+
test 'includes Devise::Models::Compatibility' do
|
28
|
+
assert_kind_of(Devise::Models::Compatibility, SessionLimitableUser.new)
|
29
|
+
end
|
30
|
+
|
31
|
+
test '#update_unique_session_id!(value) updates valid record' do
|
32
|
+
user = User.create! password: 'passWord1', password_confirmation: 'passWord1', email: 'bob@microsoft.com'
|
33
|
+
assert user.persisted?
|
34
|
+
assert_nil user.unique_session_id
|
35
|
+
user.update_unique_session_id!('unique_value')
|
36
|
+
user.reload
|
37
|
+
assert_equal('unique_value', user.unique_session_id)
|
38
|
+
end
|
39
|
+
|
40
|
+
test '#update_unique_session_id!(value) updates invalid record atomically' do
|
41
|
+
user = User.create! password: 'passWord1', password_confirmation: 'passWord1', email: 'bob@microsoft.com'
|
42
|
+
assert user.persisted?
|
43
|
+
user.email = ''
|
44
|
+
assert user.invalid?
|
45
|
+
assert_nil user.unique_session_id
|
46
|
+
user.update_unique_session_id!('unique_value')
|
47
|
+
user.reload
|
48
|
+
assert_equal('bob@microsoft.com', user.email)
|
49
|
+
assert_equal('unique_value', user.unique_session_id)
|
50
|
+
end
|
51
|
+
|
52
|
+
test '#update_unique_session_id!(value) raises an exception on an unpersisted record' do
|
53
|
+
user = User.create
|
54
|
+
assert_not user.persisted?
|
55
|
+
assert_raises(Devise::Models::Compatibility::NotPersistedError) { user.update_unique_session_id!('unique_value') }
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Devise.setup do |config|
|
4
|
+
# ==> Security Extension
|
5
|
+
# Configure security extension for devise
|
6
|
+
|
7
|
+
# Should the password expire (e.g 3.months)
|
8
|
+
# config.expire_password_after = false
|
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
|
13
|
+
# config.password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
|
14
|
+
|
15
|
+
# How many passwords to keep in archive
|
16
|
+
# config.password_archiving_count = 5
|
17
|
+
|
18
|
+
# Deny old passwords (true, false, number_of_old_passwords_to_check)
|
19
|
+
# Examples:
|
20
|
+
# config.deny_old_passwords = false # allow old passwords
|
21
|
+
# config.deny_old_passwords = true # will deny all the old passwords
|
22
|
+
# config.deny_old_passwords = 3 # will deny new passwords that matches with the last 3 passwords
|
23
|
+
# config.deny_old_passwords = true
|
24
|
+
|
25
|
+
# enable email validation for :secure_validatable. (true, false, validation_options)
|
26
|
+
# dependency: see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
|
27
|
+
# config.email_validation = true
|
28
|
+
|
29
|
+
# captcha integration for recover form
|
30
|
+
# config.captcha_for_recover = true
|
31
|
+
|
32
|
+
# captcha integration for sign up form
|
33
|
+
# config.captcha_for_sign_up = true
|
34
|
+
|
35
|
+
# captcha integration for sign in form
|
36
|
+
# config.captcha_for_sign_in = true
|
37
|
+
|
38
|
+
# captcha integration for unlock form
|
39
|
+
# config.captcha_for_unlock = true
|
40
|
+
|
41
|
+
# captcha integration for confirmation form
|
42
|
+
# config.captcha_for_confirmation = true
|
43
|
+
|
44
|
+
# Time period for account expiry from last_activity_at
|
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
|
52
|
+
end
|