devise-security 0.12.0 → 0.16.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 +186 -63
- data/app/controllers/devise/paranoid_verification_code_controller.rb +2 -0
- data/app/controllers/devise/password_expired_controller.rb +13 -6
- 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/by.yml +49 -0
- data/config/locales/cs.yml +41 -0
- data/config/locales/de.yml +30 -7
- data/config/locales/en.yml +25 -1
- data/config/locales/es.yml +19 -6
- data/config/locales/fa.yml +41 -0
- data/config/locales/fr.yml +30 -0
- data/config/locales/hi.yml +42 -0
- data/config/locales/it.yml +35 -4
- data/config/locales/ja.yml +30 -0
- data/config/locales/nl.yml +41 -0
- data/config/locales/pt.yml +41 -0
- data/config/locales/ru.yml +49 -0
- data/config/locales/tr.yml +18 -0
- data/config/locales/uk.yml +49 -0
- data/config/locales/zh_CN.yml +41 -0
- data/config/locales/zh_TW.yml +41 -0
- data/lib/devise-security/controllers/helpers.rb +61 -50
- data/lib/devise-security/hooks/expirable.rb +3 -1
- data/lib/devise-security/hooks/paranoid_verification.rb +2 -0
- data/lib/devise-security/hooks/password_expirable.rb +4 -0
- 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 +40 -0
- data/lib/devise-security/models/compatibility/mongoid_patch.rb +31 -0
- data/lib/devise-security/models/compatibility.rb +8 -15
- data/lib/devise-security/models/database_authenticatable_patch.rb +3 -1
- data/lib/devise-security/models/expirable.rb +8 -2
- data/lib/devise-security/models/mongoid/old_password.rb +21 -0
- data/lib/devise-security/models/paranoid_verification.rb +2 -0
- data/lib/devise-security/models/password_archivable.rb +18 -7
- data/lib/devise-security/models/password_expirable.rb +103 -48
- data/lib/devise-security/models/secure_validatable.rb +26 -6
- 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/confirmations_controller_captcha.rb +2 -0
- data/lib/devise-security/patches/confirmations_controller_security_question.rb +2 -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/passwords_controller_captcha.rb +2 -0
- data/lib/devise-security/patches/passwords_controller_security_question.rb +2 -0
- data/lib/devise-security/patches/registrations_controller_captcha.rb +2 -0
- data/lib/devise-security/patches/sessions_controller_captcha.rb +2 -0
- data/lib/devise-security/patches/unlocks_controller_captcha.rb +2 -0
- data/lib/devise-security/patches/unlocks_controller_security_question.rb +2 -0
- data/lib/devise-security/patches.rb +2 -0
- data/lib/devise-security/rails.rb +2 -0
- data/lib/devise-security/routes.rb +2 -0
- data/lib/devise-security/validators/password_complexity_validator.rb +35 -0
- data/lib/devise-security/version.rb +3 -1
- data/lib/devise-security.rb +16 -10
- data/lib/generators/devise_security/install_generator.rb +5 -3
- data/lib/generators/templates/devise_security.rb +47 -0
- data/test/{test_captcha_controller.rb → controllers/test_captcha_controller.rb} +2 -0
- data/test/controllers/test_password_expired_controller.rb +110 -0
- data/test/controllers/test_security_question_controller.rb +60 -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/security_question/unlocks_controller.rb +2 -0
- data/test/dummy/app/controllers/widgets_controller.rb +6 -0
- data/test/dummy/app/models/application_record.rb +10 -2
- data/test/dummy/app/models/application_user_record.rb +11 -0
- data/test/dummy/app/models/captcha_user.rb +7 -2
- data/test/dummy/app/models/mongoid/confirmable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +17 -0
- data/test/dummy/app/models/mongoid/expirable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/lockable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/mappings.rb +13 -0
- data/test/dummy/app/models/mongoid/omniauthable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/password_archivable_fields.rb +9 -0
- data/test/dummy/app/models/mongoid/password_expirable_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/recoverable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/registerable_fields.rb +19 -0
- data/test/dummy/app/models/mongoid/rememberable_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/security_questionable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/session_limitable_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/timeoutable_fields.rb +9 -0
- data/test/dummy/app/models/mongoid/trackable_fields.rb +14 -0
- data/test/dummy/app/models/mongoid/validatable_fields.rb +7 -0
- data/test/dummy/app/models/secure_user.rb +7 -1
- data/test/dummy/app/models/security_question_user.rb +9 -4
- data/test/dummy/app/models/user.rb +15 -0
- data/test/dummy/app/models/widget.rb +6 -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 +35 -0
- data/test/dummy/config/application.rb +13 -7
- data/test/dummy/config/boot.rb +2 -0
- data/test/dummy/config/environment.rb +2 -0
- data/test/dummy/config/environments/test.rb +5 -13
- data/test/dummy/config/initializers/devise.rb +10 -3
- data/test/dummy/config/initializers/migration_class.rb +3 -6
- data/test/dummy/config/mongoid.yml +6 -0
- data/test/dummy/config/routes.rb +6 -3
- data/test/dummy/config.ru +3 -1
- data/test/dummy/db/migrate/20120508165529_create_tables.rb +13 -2
- 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 +14 -0
- data/test/dummy/lib/shared_security_questions_fields.rb +16 -0
- data/test/dummy/lib/shared_user.rb +32 -0
- data/test/dummy/lib/shared_user_with_password_verification.rb +13 -0
- data/test/dummy/lib/shared_user_without_email.rb +28 -0
- data/test/dummy/lib/shared_user_without_omniauth.rb +15 -0
- data/test/dummy/lib/shared_verification_fields.rb +15 -0
- data/test/dummy/log/development.log +883 -0
- data/test/dummy/log/test.log +21689 -0
- data/test/integration/test_password_expirable_workflow.rb +53 -0
- data/test/integration/test_session_limitable_workflow.rb +67 -0
- data/test/orm/active_record.rb +15 -0
- data/test/orm/mongoid.rb +13 -0
- data/test/support/integration_helpers.rb +29 -0
- data/test/support/mongoid.yml +6 -0
- data/test/test_compatibility.rb +13 -0
- data/test/test_complexity_validator.rb +72 -0
- data/test/test_helper.rb +42 -9
- data/test/test_install_generator.rb +19 -2
- data/test/test_paranoid_verification.rb +2 -0
- data/test/test_password_archivable.rb +8 -7
- data/test/test_password_expirable.rb +70 -7
- data/test/test_secure_validatable.rb +97 -21
- data/test/test_session_limitable.rb +57 -0
- data/{lib/generators/templates → test/tmp/config/initializers}/devise-security.rb +12 -3
- data/test/tmp/config/locales/devise.security_extension.by.yml +49 -0
- data/test/tmp/config/locales/devise.security_extension.cs.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.de.yml +39 -0
- data/test/tmp/config/locales/devise.security_extension.en.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.es.yml +30 -0
- data/test/tmp/config/locales/devise.security_extension.fa.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.fr.yml +30 -0
- data/test/tmp/config/locales/devise.security_extension.hi.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.it.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.ja.yml +30 -0
- data/test/tmp/config/locales/devise.security_extension.nl.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.pt.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.ru.yml +49 -0
- data/test/tmp/config/locales/devise.security_extension.tr.yml +18 -0
- data/test/tmp/config/locales/devise.security_extension.uk.yml +49 -0
- data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +41 -0
- data/test/tmp/config/locales/devise.security_extension.zh_TW.yml +41 -0
- metadata +235 -110
- 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/schema.rb +0 -64
- data/test/dummy/app/models/.gitkeep +0 -0
- data/test/test_password_expired_controller.rb +0 -44
- data/test/test_security_question_controller.rb +0 -84
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DeviseSecurity
|
2
4
|
module Controllers
|
3
5
|
module Helpers
|
@@ -38,71 +40,80 @@ module DeviseSecurity
|
|
38
40
|
|
39
41
|
# controller instance methods
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
43
|
+
private
|
44
|
+
|
45
|
+
# Called as a `before_action` on all actions on any controller that uses
|
46
|
+
# this helper. If the user's session is marked as having an expired
|
47
|
+
# password we double check in case it has been changed by another process,
|
48
|
+
# then redirect to the password change url.
|
49
|
+
#
|
50
|
+
# @note `Warden::Manager.after_authentication` is run AFTER this method
|
51
|
+
#
|
52
|
+
# @note Once the warden session has `'password_expired'` set to `false`,
|
53
|
+
# it will **never** be checked again until the user re-logs in.
|
54
|
+
def handle_password_change
|
55
|
+
return if warden.nil?
|
56
|
+
|
57
|
+
if !devise_controller? &&
|
58
|
+
!ignore_password_expire? &&
|
59
|
+
!request.format.nil? &&
|
60
|
+
request.format.html?
|
61
|
+
Devise.mappings.keys.flatten.any? do |scope|
|
62
|
+
if signed_in?(scope) && warden.session(scope)['password_expired'] == true
|
63
|
+
if send(:"current_#{scope}").try(:need_change_password?)
|
64
|
+
store_location_for(scope, request.original_fullpath) if request.get?
|
65
|
+
redirect_for_password_change(scope)
|
66
|
+
else
|
67
|
+
warden.session(scope)['password_expired'] = false
|
58
68
|
end
|
59
69
|
end
|
60
70
|
end
|
61
71
|
end
|
72
|
+
end
|
62
73
|
|
63
|
-
|
64
|
-
|
65
|
-
|
74
|
+
# lookup if extra (paranoid) code verification is needed
|
75
|
+
def handle_paranoid_verification
|
76
|
+
return if warden.nil?
|
66
77
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
end
|
78
|
+
if !devise_controller? && !request.format.nil? && request.format.html?
|
79
|
+
Devise.mappings.keys.flatten.any? do |scope|
|
80
|
+
if signed_in?(scope) && warden.session(scope)['paranoid_verify']
|
81
|
+
store_location_for(scope, request.original_fullpath) if request.get?
|
82
|
+
redirect_for_paranoid_verification scope
|
83
|
+
return
|
74
84
|
end
|
75
85
|
end
|
76
86
|
end
|
87
|
+
end
|
77
88
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
89
|
+
# redirect for password update with alert message
|
90
|
+
def redirect_for_password_change(scope)
|
91
|
+
redirect_to change_password_required_path_for(scope), alert: I18n.t('change_required', scope: 'devise.password_expired')
|
92
|
+
end
|
82
93
|
|
83
|
-
|
84
|
-
|
85
|
-
|
94
|
+
def redirect_for_paranoid_verification(scope)
|
95
|
+
redirect_to paranoid_verification_code_path_for(scope), alert: I18n.t('code_required', scope: 'devise.paranoid_verify')
|
96
|
+
end
|
86
97
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
98
|
+
# path for change password
|
99
|
+
def change_password_required_path_for(resource_or_scope = nil)
|
100
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
101
|
+
change_path = "#{scope}_password_expired_path"
|
102
|
+
send(change_path)
|
103
|
+
end
|
93
104
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
105
|
+
def paranoid_verification_code_path_for(resource_or_scope = nil)
|
106
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
107
|
+
change_path = "#{scope}_paranoid_verification_code_path"
|
108
|
+
send(change_path)
|
109
|
+
end
|
99
110
|
|
100
|
-
|
111
|
+
protected
|
101
112
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
113
|
+
# allow to overwrite for some special handlings
|
114
|
+
def ignore_password_expire?
|
115
|
+
false
|
116
|
+
end
|
106
117
|
end
|
107
118
|
end
|
108
119
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Updates the last_activity_at fields from the record. Only when the user is active
|
2
4
|
# for authentication and authenticated.
|
3
5
|
# An expiry of the account is only checked on sign in OR on manually setting the
|
@@ -7,4 +9,4 @@ Warden::Manager.after_set_user do |record, warden, options|
|
|
7
9
|
warden.authenticated?(options[:scope]) && record.respond_to?(:update_last_activity!)
|
8
10
|
record.update_last_activity!
|
9
11
|
end
|
10
|
-
end
|
12
|
+
end
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @note This happens after
|
4
|
+
# {DeviseSecurity::Controller::Helpers#handle_password_change}
|
1
5
|
Warden::Manager.after_authentication do |record, warden, options|
|
2
6
|
if record.respond_to?(:need_change_password?)
|
3
7
|
warden.session(options[:scope])['password_expired'] = record.need_change_password?
|
@@ -1,24 +1,41 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
#
|
4
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# After each sign in, update unique_session_id. This is only triggered when the
|
4
|
+
# user is explicitly set (with set_user) and on authentication. Retrieving the
|
5
|
+
# user from session (:fetch) does not trigger it.
|
5
6
|
Warden::Manager.after_set_user except: :fetch do |record, warden, options|
|
6
|
-
if record.
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
if record.devise_modules.include?(:session_limitable) &&
|
8
|
+
warden.authenticated?(options[:scope]) &&
|
9
|
+
!record.skip_session_limitable?
|
10
|
+
|
11
|
+
if !options[:skip_session_limitable]
|
12
|
+
unique_session_id = Devise.friendly_token
|
13
|
+
warden.session(options[:scope])['unique_session_id'] = unique_session_id
|
14
|
+
record.update_unique_session_id!(unique_session_id)
|
15
|
+
else
|
16
|
+
warden.session(options[:scope])['devise.skip_session_limitable'] = true
|
17
|
+
end
|
10
18
|
end
|
11
19
|
end
|
12
20
|
|
13
|
-
# Each time a record is fetched from session we check if a new session from
|
14
|
-
# browser was opened for the record or not, based on a unique session
|
15
|
-
# If so, the old account is logged out and redirected to the sign in
|
21
|
+
# Each time a record is fetched from session we check if a new session from
|
22
|
+
# another browser was opened for the record or not, based on a unique session
|
23
|
+
# identifier. If so, the old account is logged out and redirected to the sign in
|
24
|
+
# page on the next request.
|
16
25
|
Warden::Manager.after_set_user only: :fetch do |record, warden, options|
|
17
26
|
scope = options[:scope]
|
18
|
-
env = warden.request.env
|
19
27
|
|
20
|
-
if record.
|
21
|
-
|
28
|
+
if record.devise_modules.include?(:session_limitable) &&
|
29
|
+
warden.authenticated?(scope) &&
|
30
|
+
options[:store] != false
|
31
|
+
if record.unique_session_id != warden.session(scope)['unique_session_id'] &&
|
32
|
+
!record.skip_session_limitable? &&
|
33
|
+
!warden.session(scope)['devise.skip_session_limitable']
|
34
|
+
Rails.logger.warn do
|
35
|
+
'[devise-security][session_limitable] session id mismatch: '\
|
36
|
+
"expected=#{record.unique_session_id.inspect} "\
|
37
|
+
"actual=#{warden.session(scope)['unique_session_id'].inspect}"
|
38
|
+
end
|
22
39
|
warden.raw_session.clear
|
23
40
|
warden.logout(scope)
|
24
41
|
throw :warden, scope: scope, message: :session_limited
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Devise
|
2
|
+
module Models
|
3
|
+
module Compatibility
|
4
|
+
|
5
|
+
class NotPersistedError < ActiveRecord::ActiveRecordError; end
|
6
|
+
|
7
|
+
module ActiveRecordPatch
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
unless Devise.activerecord51?
|
10
|
+
# When the record was saved, was the +encrypted_password+ changed?
|
11
|
+
# @return [Boolean]
|
12
|
+
def saved_change_to_encrypted_password?
|
13
|
+
encrypted_password_changed?
|
14
|
+
end
|
15
|
+
|
16
|
+
# The encrypted password that existed before the record was saved
|
17
|
+
# @return [String]
|
18
|
+
# @return [nil] if an +encrypted_password+ had not been set
|
19
|
+
def encrypted_password_before_last_save
|
20
|
+
previous_changes['encrypted_password'].try(:first)
|
21
|
+
end
|
22
|
+
|
23
|
+
# When the record is saved, will the +encrypted_password+ be changed?
|
24
|
+
# @return [Boolean]
|
25
|
+
def will_save_change_to_encrypted_password?
|
26
|
+
changed_attributes['encrypted_password'].present?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Updates the record with the value and does not trigger validations or callbacks
|
31
|
+
# @param name [Symbol] attribute to update
|
32
|
+
# @param value [String] value to set
|
33
|
+
def update_attribute_without_validatons_or_callbacks(name, value)
|
34
|
+
update_column(name, value)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Devise
|
2
|
+
module Models
|
3
|
+
module Compatibility
|
4
|
+
|
5
|
+
class NotPersistedError < Mongoid::Errors::MongoidError; end
|
6
|
+
|
7
|
+
module MongoidPatch
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
# Will saving this record change the +email+ attribute?
|
11
|
+
# @return [Boolean]
|
12
|
+
def will_save_change_to_email?
|
13
|
+
changed.include? 'email'
|
14
|
+
end
|
15
|
+
|
16
|
+
# Will saving this record change the +encrypted_password+ attribute?
|
17
|
+
# @return [Boolean]
|
18
|
+
def will_save_change_to_encrypted_password?
|
19
|
+
changed.include? 'encrypted_password'
|
20
|
+
end
|
21
|
+
|
22
|
+
# Updates the document with the value and does not trigger validations or callbacks
|
23
|
+
# @param name [Symbol] attribute to update
|
24
|
+
# @param value [String] value to set
|
25
|
+
def update_attribute_without_validatons_or_callbacks(name, value)
|
26
|
+
set(Hash[*[name, value]])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,22 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "compatibility/#{DEVISE_ORM}_patch"
|
4
|
+
|
1
5
|
module Devise
|
2
6
|
module Models
|
7
|
+
# These compatibility modules define methods used by devise-security
|
8
|
+
# that may need to be defined or re-defined for compatibility between ORMs
|
9
|
+
# and/or older versions of ORMs.
|
3
10
|
module Compatibility
|
4
11
|
extend ActiveSupport::Concern
|
5
|
-
|
6
|
-
# for backwards compatibility with Rails < 5.1.x
|
7
|
-
unless Devise.activerecord51?
|
8
|
-
def saved_change_to_encrypted_password?
|
9
|
-
encrypted_password_changed?
|
10
|
-
end
|
11
|
-
|
12
|
-
def encrypted_password_before_last_save
|
13
|
-
previous_changes['encrypted_password'].try(:first)
|
14
|
-
end
|
15
|
-
|
16
|
-
def will_save_change_to_encrypted_password?
|
17
|
-
changed_attributes['encrypted_password'].present?
|
18
|
-
end
|
19
|
-
end
|
12
|
+
include "Devise::Models::Compatibility::#{DEVISE_ORM.to_s.classify}Patch".constantize
|
20
13
|
end
|
21
14
|
end
|
22
15
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Devise
|
2
4
|
module Models
|
3
5
|
module DatabaseAuthenticatablePatch
|
@@ -8,7 +10,7 @@ module Devise
|
|
8
10
|
new_password_confirmation = params[:password_confirmation]
|
9
11
|
|
10
12
|
result = if valid_password?(current_password) && new_password.present? && new_password_confirmation.present?
|
11
|
-
|
13
|
+
update(params, *options)
|
12
14
|
else
|
13
15
|
self.assign_attributes(params, *options)
|
14
16
|
self.valid?
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'devise-security/hooks/expirable'
|
2
4
|
|
3
5
|
module Devise
|
@@ -20,7 +22,11 @@ module Devise
|
|
20
22
|
|
21
23
|
# Updates +last_activity_at+, called from a Warden::Manager.after_set_user hook.
|
22
24
|
def update_last_activity!
|
23
|
-
|
25
|
+
if respond_to?(:update_column)
|
26
|
+
self.update_column(:last_activity_at, Time.now.utc)
|
27
|
+
elsif defined? Mongoid
|
28
|
+
self.update_attribute(:last_activity_at, Time.now.utc)
|
29
|
+
end
|
24
30
|
end
|
25
31
|
|
26
32
|
# Tells if the account has expired
|
@@ -101,7 +107,7 @@ module Devise
|
|
101
107
|
# @example Overwritten version to blank out the object.
|
102
108
|
# def self.delete_all_expired_for(time = 90.days)
|
103
109
|
# expired_for(time).each do |u|
|
104
|
-
# u.
|
110
|
+
# u.update first_name: nil, last_name: nil
|
105
111
|
# end
|
106
112
|
# end
|
107
113
|
def delete_all_expired_for(time)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class OldPassword
|
4
|
+
include Mongoid::Document
|
5
|
+
|
6
|
+
## Database authenticatable
|
7
|
+
field :encrypted_password, type: String
|
8
|
+
validates_presence_of :encrypted_password
|
9
|
+
field :password_salt, type: String
|
10
|
+
|
11
|
+
field :password_archivable_type, type: String
|
12
|
+
validates_presence_of :password_archivable_type
|
13
|
+
|
14
|
+
field :password_archivable_id, type: String
|
15
|
+
validates_presence_of :password_archivable_id
|
16
|
+
index({ password_archivable_type: 1, password_archivable_id: 1 }, name: 'index_password_archivable')
|
17
|
+
|
18
|
+
include Mongoid::Timestamps
|
19
|
+
|
20
|
+
belongs_to :password_archivable, polymorphic: true
|
21
|
+
end
|
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'compatibility'
|
4
|
+
require_relative "#{DEVISE_ORM}/old_password"
|
2
5
|
|
3
6
|
module Devise
|
4
7
|
module Models
|
@@ -9,7 +12,7 @@ module Devise
|
|
9
12
|
include Devise::Models::DatabaseAuthenticatable
|
10
13
|
|
11
14
|
included do
|
12
|
-
has_many :old_passwords, as: :password_archivable, dependent: :destroy
|
15
|
+
has_many :old_passwords, class_name: 'OldPassword', as: :password_archivable, dependent: :destroy
|
13
16
|
before_update :archive_password, if: :will_save_change_to_encrypted_password?
|
14
17
|
validate :validate_password_archive, if: :password_present?
|
15
18
|
end
|
@@ -36,11 +39,15 @@ module Devise
|
|
36
39
|
# @return [true] if current password was used previously
|
37
40
|
# @return [false] if disabled or not previously used
|
38
41
|
def password_archive_included?
|
39
|
-
return false unless max_old_passwords
|
40
|
-
|
42
|
+
return false unless max_old_passwords.positive?
|
43
|
+
|
44
|
+
old_passwords_including_cur_change = old_passwords.order(created_at: :desc).limit(max_old_passwords).pluck(:encrypted_password)
|
41
45
|
old_passwords_including_cur_change << encrypted_password_was # include most recent change in list, but don't save it yet!
|
42
46
|
old_passwords_including_cur_change.any? do |old_password|
|
43
|
-
|
47
|
+
# NOTE: we deliberately do not do mass assignment here so that users that
|
48
|
+
# rely on `protected_attributes_continued` gem can still use this extension.
|
49
|
+
# See issue #68
|
50
|
+
self.class.new.tap { |object| object.encrypted_password = old_password }.valid_password?(password)
|
44
51
|
end
|
45
52
|
end
|
46
53
|
|
@@ -58,11 +65,15 @@ module Devise
|
|
58
65
|
|
59
66
|
private
|
60
67
|
|
61
|
-
#
|
68
|
+
# Archive the last password before save and delete all to old passwords from archive
|
69
|
+
# @note we check to see if an old password has already been archived because
|
70
|
+
# mongoid will keep re-triggering this callback when we add an old password
|
62
71
|
def archive_password
|
63
|
-
if max_old_passwords
|
72
|
+
if max_old_passwords.positive?
|
73
|
+
return true if old_passwords.where(encrypted_password: encrypted_password_was).exists?
|
74
|
+
|
64
75
|
old_passwords.create!(encrypted_password: encrypted_password_was) if encrypted_password_was.present?
|
65
|
-
old_passwords.order(:
|
76
|
+
old_passwords.order(created_at: :desc).offset(max_old_passwords).destroy_all
|
66
77
|
else
|
67
78
|
old_passwords.destroy_all
|
68
79
|
end
|
@@ -1,67 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'devise-security/hooks/password_expirable'
|
2
4
|
|
3
|
-
module Devise
|
4
|
-
|
5
|
+
module Devise::Models
|
6
|
+
# PasswordExpirable makes passwords expire after a configurable amount of
|
7
|
+
# time, or on demand.
|
8
|
+
#
|
9
|
+
# == Configuration
|
10
|
+
# Set +expire_password_after+ to the number of seconds a password is valid for
|
11
|
+
# (example: +3.months+). Setting it to +true+ will allow passwords to be expired
|
12
|
+
# on-demand only, and +false+ disables this feature.
|
13
|
+
#
|
14
|
+
# == Expire On-Demand
|
15
|
+
# This is useful to force users to change passwords for complex business reasons.
|
16
|
+
# Call +need_change_password+ to indicate a record needs a new password.
|
17
|
+
module PasswordExpirable
|
18
|
+
extend ActiveSupport::Concern
|
5
19
|
|
6
|
-
|
7
|
-
|
8
|
-
|
20
|
+
included do
|
21
|
+
scope :with_password_change_requested, -> { where(password_changed_at: nil) }
|
22
|
+
scope :without_password_change_requested, -> { where.not(password_changed_at: nil) }
|
23
|
+
scope :with_expired_password, -> { where('password_changed_at is NULL OR password_changed_at < ?', expire_password_after.seconds.ago) }
|
24
|
+
scope :without_expired_password, -> { without_password_change_requested.where('password_changed_at >= ?', expire_password_after.seconds.ago) }
|
25
|
+
before_save :update_password_changed
|
26
|
+
end
|
9
27
|
|
10
|
-
|
11
|
-
|
12
|
-
|
28
|
+
# Is a password change required?
|
29
|
+
# @return [Boolean]
|
30
|
+
# @return [true] if +password_changed_at+ has not been set or if it is old
|
31
|
+
# enough based on +expire_password_after+ configuration.
|
32
|
+
def need_change_password?
|
33
|
+
password_change_requested? || password_too_old?
|
34
|
+
end
|
13
35
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
end
|
36
|
+
# Clear the +password_changed_at+ field so that the user will be required to
|
37
|
+
# update their password.
|
38
|
+
# @note Saves the record (without validations)
|
39
|
+
# @return [Boolean]
|
40
|
+
def need_change_password!
|
41
|
+
return unless password_expiration_enabled?
|
22
42
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
29
|
-
end
|
43
|
+
need_change_password
|
44
|
+
save(validate: false)
|
45
|
+
end
|
46
|
+
alias expire_password! need_change_password!
|
47
|
+
alias request_password_change! need_change_password!
|
30
48
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
49
|
+
# Clear the +password_changed_at+ field so that the user will be required to
|
50
|
+
# update their password.
|
51
|
+
# @note Does not save the record
|
52
|
+
# @return [void]
|
53
|
+
def need_change_password
|
54
|
+
return unless password_expiration_enabled?
|
36
55
|
|
37
|
-
|
38
|
-
|
56
|
+
self.password_changed_at = nil
|
57
|
+
end
|
58
|
+
alias expire_password need_change_password
|
59
|
+
alias request_password_change need_change_password
|
39
60
|
|
40
|
-
|
41
|
-
|
61
|
+
# @return [Integer] number of seconds passwords are valid for
|
62
|
+
# @return [true] passwords are expired 'on demand' only.
|
63
|
+
# @return [false] passwords never expire (this feature is disabled)
|
64
|
+
def expire_password_after
|
65
|
+
self.class.expire_password_after
|
66
|
+
end
|
42
67
|
|
43
|
-
|
44
|
-
|
45
|
-
|
68
|
+
# When +password_changed_at+ is set to +NULL+ in the database
|
69
|
+
# the user is required to change their password. This only happens
|
70
|
+
# on demand or when the column is first added to the table.
|
71
|
+
# @return [Boolean]
|
72
|
+
def password_change_requested?
|
73
|
+
return false unless password_expiration_enabled?
|
74
|
+
return false if new_record?
|
46
75
|
|
47
|
-
|
76
|
+
password_changed_at.nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
# Is this password older than the configured expiration timeout?
|
80
|
+
# @return [Boolean]
|
81
|
+
def password_too_old?
|
82
|
+
return false if new_record?
|
83
|
+
return false unless password_expiration_enabled?
|
84
|
+
return false if expire_password_on_demand?
|
48
85
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
86
|
+
password_changed_at < expire_password_after.seconds.ago
|
87
|
+
end
|
88
|
+
alias password_expired? password_too_old?
|
53
89
|
|
54
|
-
|
55
|
-
return @_numeric if defined?(@_numeric)
|
56
|
-
@_numeric ||= self.expire_password_after.is_a?(1.class) ||
|
57
|
-
self.expire_password_after.is_a?(Float)
|
58
|
-
end
|
90
|
+
private
|
59
91
|
|
60
|
-
|
61
|
-
|
92
|
+
# Update +password_changed_at+ for new records and changed passwords.
|
93
|
+
# @note called as a +before_save+ hook
|
94
|
+
def update_password_changed
|
95
|
+
if defined?(will_save_change_to_attribute?)
|
96
|
+
return unless (new_record? || will_save_change_to_encrypted_password?) && !will_save_change_to_password_changed_at?
|
97
|
+
else
|
98
|
+
return unless (new_record? || encrypted_password_changed?) && !password_changed_at_changed?
|
62
99
|
end
|
100
|
+
|
101
|
+
self.password_changed_at = Time.zone.now
|
63
102
|
end
|
64
103
|
|
65
|
-
|
104
|
+
# Enabled if configuration +expire_password_after+ is set to an {Integer},
|
105
|
+
# {Float}, or {true}
|
106
|
+
def password_expiration_enabled?
|
107
|
+
expire_password_after.is_a?(1.class) ||
|
108
|
+
expire_password_after.is_a?(Float) ||
|
109
|
+
expire_password_on_demand?
|
110
|
+
end
|
111
|
+
|
112
|
+
# When +expire_password_after+ is set to +true+ then only expire passwords
|
113
|
+
# on demand.
|
114
|
+
def expire_password_on_demand?
|
115
|
+
expire_password_after.present? && expire_password_after == true
|
116
|
+
end
|
66
117
|
|
118
|
+
module ClassMethods
|
119
|
+
::Devise::Models.config(self, :expire_password_after)
|
120
|
+
end
|
121
|
+
end
|
67
122
|
end
|