devise-security 0.11.1 → 0.12.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 +5 -5
- data/.circleci/config.yml +41 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +22 -2
- data/.ruby-version +1 -1
- data/.travis.yml +15 -3
- data/Appraisals +19 -0
- data/Gemfile +1 -0
- data/README.md +15 -10
- data/Rakefile +3 -1
- data/app/controllers/devise/paranoid_verification_code_controller.rb +1 -1
- data/app/controllers/devise/password_expired_controller.rb +1 -1
- data/app/views/devise/paranoid_verification_code/show.html.erb +2 -2
- data/app/views/devise/password_expired/show.html.erb +5 -5
- data/config/locales/de.yml +7 -7
- data/config/locales/en.yml +8 -8
- data/config/locales/es.yml +8 -8
- data/devise-security.gemspec +12 -6
- data/gemfiles/rails_4.1_stable.gemfile +8 -0
- data/gemfiles/rails_4.2_stable.gemfile +8 -0
- data/gemfiles/rails_5.0_stable.gemfile +8 -0
- data/gemfiles/rails_5.1_stable.gemfile +8 -0
- data/gemfiles/rails_5.2_rc1.gemfile +8 -0
- data/lib/devise-security/controllers/helpers.rb +2 -2
- data/lib/devise-security/hooks/session_limitable.rb +3 -3
- data/lib/devise-security/models/compatibility.rb +22 -0
- data/lib/devise-security/models/expirable.rb +13 -13
- data/lib/devise-security/models/old_password.rb +1 -1
- data/lib/devise-security/models/paranoid_verification.rb +5 -2
- data/lib/devise-security/models/password_archivable.rb +34 -38
- data/lib/devise-security/models/password_expirable.rb +1 -1
- data/lib/devise-security/models/secure_validatable.rb +16 -14
- data/lib/devise-security/models/security_questionable.rb +1 -2
- data/lib/devise-security/models/session_limitable.rb +3 -3
- data/lib/devise-security/orm/active_record.rb +1 -3
- data/lib/devise-security/patches/confirmations_controller_captcha.rb +2 -2
- data/lib/devise-security/patches/confirmations_controller_security_question.rb +2 -2
- data/lib/devise-security/patches/passwords_controller_captcha.rb +2 -2
- data/lib/devise-security/patches/passwords_controller_security_question.rb +2 -2
- data/lib/devise-security/patches/registrations_controller_captcha.rb +2 -2
- data/lib/devise-security/patches/sessions_controller_captcha.rb +3 -3
- data/lib/devise-security/patches/unlocks_controller_captcha.rb +2 -2
- data/lib/devise-security/patches/unlocks_controller_security_question.rb +2 -2
- data/lib/devise-security/rails.rb +2 -2
- data/lib/devise-security/routes.rb +2 -3
- data/lib/devise-security/schema.rb +11 -6
- data/lib/devise-security/version.rb +1 -1
- data/test/dummy/app/models/application_record.rb +3 -0
- data/test/dummy/app/models/captcha_user.rb +1 -1
- data/test/dummy/app/models/security_question_user.rb +2 -3
- data/test/dummy/app/models/user.rb +21 -4
- data/test/dummy/app/models/widget.rb +4 -0
- data/test/dummy/config/environments/test.rb +10 -2
- data/test/dummy/config/initializers/devise.rb +1 -0
- data/test/dummy/config/secrets.yml +1 -2
- data/test/dummy/db/migrate/20120508165529_create_tables.rb +9 -3
- data/test/dummy/db/migrate/20180318103603_add_expireable_columns.rb +6 -0
- data/test/dummy/db/migrate/20180318105329_add_confirmable_columns.rb +8 -0
- data/test/dummy/db/migrate/20180318105732_add_rememberable_columns.rb +5 -0
- data/test/dummy/db/migrate/20180318111336_add_recoverable_columns.rb +6 -0
- data/test/dummy/db/migrate/20180319114023_add_widget.rb +8 -0
- data/test/test_captcha_controller.rb +13 -13
- data/test/test_helper.rb +7 -0
- data/test/test_paranoid_verification.rb +2 -2
- data/test/test_password_archivable.rb +27 -13
- data/test/test_password_expirable.rb +2 -2
- data/test/test_password_expired_controller.rb +25 -10
- data/test/test_security_question_controller.rb +45 -21
- metadata +90 -13
@@ -77,11 +77,11 @@ module DeviseSecurity
|
|
77
77
|
|
78
78
|
# redirect for password update with alert message
|
79
79
|
def redirect_for_password_change(scope)
|
80
|
-
redirect_to change_password_required_path_for(scope), :
|
80
|
+
redirect_to change_password_required_path_for(scope), alert: I18n.t('change_required', {scope: 'devise.password_expired'})
|
81
81
|
end
|
82
82
|
|
83
83
|
def redirect_for_paranoid_verification(scope)
|
84
|
-
redirect_to paranoid_verification_code_path_for(scope), :
|
84
|
+
redirect_to paranoid_verification_code_path_for(scope), alert: I18n.t('code_required', {scope: 'devise.paranoid_verify'})
|
85
85
|
end
|
86
86
|
|
87
87
|
# path for change password
|
@@ -2,7 +2,7 @@
|
|
2
2
|
# This is only triggered when the user is explicitly set (with set_user)
|
3
3
|
# and on authentication. Retrieving the user from session (:fetch) does
|
4
4
|
# not trigger it.
|
5
|
-
Warden::Manager.after_set_user :
|
5
|
+
Warden::Manager.after_set_user except: :fetch do |record, warden, options|
|
6
6
|
if record.respond_to?(:update_unique_session_id!) && warden.authenticated?(options[:scope])
|
7
7
|
unique_session_id = Devise.friendly_token
|
8
8
|
warden.session(options[:scope])['unique_session_id'] = unique_session_id
|
@@ -13,7 +13,7 @@ end
|
|
13
13
|
# Each time a record is fetched from session we check if a new session from another
|
14
14
|
# browser was opened for the record or not, based on a unique session identifier.
|
15
15
|
# If so, the old account is logged out and redirected to the sign in page on the next request.
|
16
|
-
Warden::Manager.after_set_user :
|
16
|
+
Warden::Manager.after_set_user only: :fetch do |record, warden, options|
|
17
17
|
scope = options[:scope]
|
18
18
|
env = warden.request.env
|
19
19
|
|
@@ -21,7 +21,7 @@ Warden::Manager.after_set_user :only => :fetch do |record, warden, options|
|
|
21
21
|
if record.unique_session_id != warden.session(scope)['unique_session_id'] && !env['devise.skip_session_limitable']
|
22
22
|
warden.raw_session.clear
|
23
23
|
warden.logout(scope)
|
24
|
-
throw :warden, :
|
24
|
+
throw :warden, scope: scope, message: :session_limited
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Devise
|
2
|
+
module Models
|
3
|
+
module Compatibility
|
4
|
+
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
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -2,7 +2,7 @@ require 'devise-security/hooks/expirable'
|
|
2
2
|
|
3
3
|
module Devise
|
4
4
|
module Models
|
5
|
-
# Deactivate the account after a configurable amount of time. To be able to
|
5
|
+
# Deactivate the account after a configurable amount of time. To be able to
|
6
6
|
# tell, it tracks activity about your account with the following columns:
|
7
7
|
#
|
8
8
|
# * last_activity_at - A timestamp updated when the user requests a page (only signed in)
|
@@ -11,10 +11,10 @@ module Devise
|
|
11
11
|
# +:expire_after+ - Time interval to expire accounts after
|
12
12
|
#
|
13
13
|
# == Additions
|
14
|
-
# Best used with two cron jobs. One for expiring accounts after inactivity,
|
15
|
-
# and another, that deletes accounts, which have expired for a given amount
|
14
|
+
# Best used with two cron jobs. One for expiring accounts after inactivity,
|
15
|
+
# and another, that deletes accounts, which have expired for a given amount
|
16
16
|
# of time (for example 90 days).
|
17
|
-
#
|
17
|
+
#
|
18
18
|
module Expirable
|
19
19
|
extend ActiveSupport::Concern
|
20
20
|
|
@@ -37,13 +37,13 @@ module Devise
|
|
37
37
|
|
38
38
|
# Expire an account. This is for cron jobs and manually expiring of accounts.
|
39
39
|
#
|
40
|
-
# @example
|
40
|
+
# @example
|
41
41
|
# User.expire!
|
42
42
|
# User.expire! 1.week.from_now
|
43
43
|
# @note +expired_at+ can be in the future as well
|
44
44
|
def expire!(at = Time.now.utc)
|
45
45
|
self.expired_at = at
|
46
|
-
save(:
|
46
|
+
save(validate: false)
|
47
47
|
end
|
48
48
|
|
49
49
|
# Overwrites active_for_authentication? from Devise::Models::Activatable
|
@@ -55,7 +55,7 @@ module Devise
|
|
55
55
|
super && !self.expired?
|
56
56
|
end
|
57
57
|
|
58
|
-
# The message sym, if {#active_for_authentication?} returns +false+. E.g. needed
|
58
|
+
# The message sym, if {#active_for_authentication?} returns +false+. E.g. needed
|
59
59
|
# for i18n.
|
60
60
|
def inactive_message
|
61
61
|
!self.expired? ? super : :expired
|
@@ -82,17 +82,17 @@ module Devise
|
|
82
82
|
where('expired_at < ?', time.seconds.ago)
|
83
83
|
end
|
84
84
|
|
85
|
-
# Sample method for daily cron to delete all expired entries after a
|
85
|
+
# Sample method for daily cron to delete all expired entries after a
|
86
86
|
# given amount of +time+.
|
87
87
|
#
|
88
|
-
# In your overwritten method you can "blank out" the object instead of
|
88
|
+
# In your overwritten method you can "blank out" the object instead of
|
89
89
|
# deleting it.
|
90
90
|
#
|
91
91
|
# *Word of warning*: You have to handle the dependent method
|
92
|
-
# on the +resource+ relations (+:destroy+ or +:nullify+) and catch this
|
92
|
+
# on the +resource+ relations (+:destroy+ or +:nullify+) and catch this
|
93
93
|
# behavior (see http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Deleting+from+associations).
|
94
94
|
#
|
95
|
-
# @example
|
95
|
+
# @example
|
96
96
|
# Resource.delete_all_expired_for 90.days
|
97
97
|
# @example You can overide this in your +resource+ model
|
98
98
|
# def self.delete_all_expired_for(time = 90.days)
|
@@ -108,7 +108,7 @@ module Devise
|
|
108
108
|
expired_for(time).delete_all
|
109
109
|
end
|
110
110
|
|
111
|
-
# Version of {#delete_all_expired_for} without arguments (uses
|
111
|
+
# Version of {#delete_all_expired_for} without arguments (uses
|
112
112
|
# configured +delete_expired_after+ default value).
|
113
113
|
# @see #delete_all_expired_for
|
114
114
|
def delete_all_expired
|
@@ -117,4 +117,4 @@ module Devise
|
|
117
117
|
end
|
118
118
|
end
|
119
119
|
end
|
120
|
-
end
|
120
|
+
end
|
@@ -17,7 +17,9 @@ module Devise
|
|
17
17
|
generate_paranoid_code
|
18
18
|
elsif code == paranoid_verification_code
|
19
19
|
attempt = 0
|
20
|
-
update_without_password paranoid_verification_code: nil,
|
20
|
+
update_without_password paranoid_verification_code: nil,
|
21
|
+
paranoid_verified_at: Time.now,
|
22
|
+
paranoid_verification_attempt: attempt
|
21
23
|
else
|
22
24
|
update_without_password paranoid_verification_attempt: attempt
|
23
25
|
end
|
@@ -28,7 +30,8 @@ module Devise
|
|
28
30
|
end
|
29
31
|
|
30
32
|
def generate_paranoid_code
|
31
|
-
update_without_password paranoid_verification_code: Devise.verification_code_generator.call(),
|
33
|
+
update_without_password paranoid_verification_code: Devise.verification_code_generator.call(),
|
34
|
+
paranoid_verification_attempt: 0
|
32
35
|
end
|
33
36
|
end
|
34
37
|
end
|
@@ -1,45 +1,47 @@
|
|
1
|
+
require_relative 'compatibility'
|
2
|
+
|
1
3
|
module Devise
|
2
4
|
module Models
|
3
|
-
# PasswordArchivable
|
5
|
+
# PasswordArchivable, this depends on the DatabaseAuthenticatable module from devise
|
4
6
|
module PasswordArchivable
|
5
7
|
extend ActiveSupport::Concern
|
8
|
+
include Devise::Models::Compatibility
|
9
|
+
include Devise::Models::DatabaseAuthenticatable
|
6
10
|
|
7
11
|
included do
|
8
12
|
has_many :old_passwords, as: :password_archivable, dependent: :destroy
|
9
|
-
before_update :archive_password
|
10
|
-
validate :validate_password_archive
|
13
|
+
before_update :archive_password, if: :will_save_change_to_encrypted_password?
|
14
|
+
validate :validate_password_archive, if: :password_present?
|
11
15
|
end
|
12
16
|
|
17
|
+
delegate :present?, to: :password, prefix: true
|
18
|
+
|
13
19
|
def validate_password_archive
|
14
|
-
errors.add(:password, :taken_in_past) if
|
20
|
+
errors.add(:password, :taken_in_past) if will_save_change_to_encrypted_password? && password_archive_included?
|
15
21
|
end
|
16
22
|
|
17
|
-
#
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
if self.class.deny_old_passwords > 0 && !self.password.nil?
|
28
|
-
old_passwords_including_cur_change = self.old_passwords.order(:id).reverse_order.limit(self.class.deny_old_passwords).to_a
|
29
|
-
old_passwords_including_cur_change << OldPassword.new(old_password_params) # include most recent change in list, but don't save it yet!
|
30
|
-
old_passwords_including_cur_change.each do |old_password|
|
31
|
-
dummy = self.class.new
|
32
|
-
dummy.encrypted_password = old_password.encrypted_password
|
33
|
-
return true if dummy.valid_password?(password)
|
34
|
-
end
|
23
|
+
# @return [Integer] max number of old passwords to store and check
|
24
|
+
def max_old_passwords
|
25
|
+
case deny_old_passwords
|
26
|
+
when true
|
27
|
+
[1, archive_count].max
|
28
|
+
when false
|
29
|
+
0
|
30
|
+
else
|
31
|
+
deny_old_passwords.to_i
|
35
32
|
end
|
36
|
-
|
37
|
-
false
|
38
33
|
end
|
39
34
|
|
40
|
-
|
41
|
-
|
42
|
-
|
35
|
+
# validate is the password used in the past
|
36
|
+
# @return [true] if current password was used previously
|
37
|
+
# @return [false] if disabled or not previously used
|
38
|
+
def password_archive_included?
|
39
|
+
return false unless max_old_passwords > 0
|
40
|
+
old_passwords_including_cur_change = old_passwords.order(:id).reverse_order.limit(max_old_passwords).pluck(:encrypted_password)
|
41
|
+
old_passwords_including_cur_change << encrypted_password_was # include most recent change in list, but don't save it yet!
|
42
|
+
old_passwords_including_cur_change.any? do |old_password|
|
43
|
+
self.class.new(encrypted_password: old_password).valid_password?(password)
|
44
|
+
end
|
43
45
|
end
|
44
46
|
|
45
47
|
def deny_old_passwords
|
@@ -58,20 +60,14 @@ module Devise
|
|
58
60
|
|
59
61
|
# archive the last password before save and delete all to old passwords from archive
|
60
62
|
def archive_password
|
61
|
-
if
|
62
|
-
if
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
old_passwords.destroy_all
|
67
|
-
end
|
63
|
+
if max_old_passwords > 0
|
64
|
+
old_passwords.create!(encrypted_password: encrypted_password_was) if encrypted_password_was.present?
|
65
|
+
old_passwords.order(:id).reverse_order.offset(max_old_passwords).destroy_all
|
66
|
+
else
|
67
|
+
old_passwords.destroy_all
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
-
def old_password_params
|
72
|
-
{ encrypted_password: encrypted_password_change.first }
|
73
|
-
end
|
74
|
-
|
75
71
|
module ClassMethods
|
76
72
|
::Devise::Models.config(self, :password_archiving_count, :deny_old_passwords)
|
77
73
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative 'compatibility'
|
2
|
+
|
1
3
|
module Devise
|
2
4
|
module Models
|
3
5
|
# SecureValidatable creates better validations with more validation for security
|
@@ -11,6 +13,7 @@ module Devise
|
|
11
13
|
# * +password_regex+: need strong password. Defaults to /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/
|
12
14
|
#
|
13
15
|
module SecureValidatable
|
16
|
+
include Devise::Models::Compatibility
|
14
17
|
|
15
18
|
def self.included(base)
|
16
19
|
base.extend ClassMethods
|
@@ -23,27 +26,27 @@ module Devise
|
|
23
26
|
unless has_uniqueness_validation_of_login?
|
24
27
|
validation_condition = "#{login_attribute}_changed?".to_sym
|
25
28
|
|
26
|
-
validates login_attribute, :
|
27
|
-
:
|
28
|
-
:
|
29
|
+
validates login_attribute, uniqueness: {
|
30
|
+
scope: authentication_keys[1..-1],
|
31
|
+
case_sensitive: !!case_insensitive_keys
|
29
32
|
},
|
30
|
-
:
|
33
|
+
if: validation_condition
|
31
34
|
|
32
35
|
already_validated_email = login_attribute.to_s == 'email'
|
33
36
|
end
|
34
37
|
|
35
38
|
unless devise_validation_enabled?
|
36
|
-
validates :email, :
|
39
|
+
validates :email, presence: true, if: :email_required?
|
37
40
|
unless already_validated_email
|
38
|
-
validates :email, :
|
41
|
+
validates :email, uniqueness: true, allow_blank: true, if: :email_changed? # check uniq for email ever
|
39
42
|
end
|
40
43
|
|
41
|
-
validates :password, :
|
44
|
+
validates :password, presence: true, length: password_length, confirmation: true, if: :password_required?
|
42
45
|
end
|
43
46
|
|
44
47
|
# extra validations
|
45
|
-
validates :email,
|
46
|
-
validates :password, :
|
48
|
+
validates :email, email: email_validation if email_validation # use rails_email_validator or similar
|
49
|
+
validates :password, format: { with: password_regex, message: :password_format }, if: :password_required?
|
47
50
|
|
48
51
|
# don't allow use same password
|
49
52
|
validate :current_equal_password_validation
|
@@ -55,12 +58,11 @@ module Devise
|
|
55
58
|
end
|
56
59
|
|
57
60
|
def current_equal_password_validation
|
58
|
-
if
|
59
|
-
|
60
|
-
|
61
|
-
dummy.password_salt = self.password_salt_change.first if self.respond_to?(:password_salt_change) && !self.password_salt_change.nil?
|
62
|
-
self.errors.add(:password, :equal_to_current_password) if dummy.valid_password?(self.password)
|
61
|
+
return if new_record? || !will_save_change_to_encrypted_password? || password.blank?
|
62
|
+
dummy = self.class.new(encrypted_password: encrypted_password_was).tap do |user|
|
63
|
+
user.password_salt = password_salt_was if respond_to?(:password_salt)
|
63
64
|
end
|
65
|
+
self.errors.add(:password, :equal_to_current_password) if dummy.valid_password?(password)
|
64
66
|
end
|
65
67
|
|
66
68
|
protected
|
@@ -5,7 +5,7 @@ module Devise
|
|
5
5
|
# SessionLimited ensures, that there is only one session usable per account at once.
|
6
6
|
# If someone logs in, and some other is logging in with the same credentials,
|
7
7
|
# the session from the first one is invalidated and not usable anymore.
|
8
|
-
# The first one is redirected to the sign page with a message, telling that
|
8
|
+
# The first one is redirected to the sign page with a message, telling that
|
9
9
|
# someone used his credentials to sign in.
|
10
10
|
module SessionLimitable
|
11
11
|
extend ActiveSupport::Concern
|
@@ -13,9 +13,9 @@ module Devise
|
|
13
13
|
def update_unique_session_id!(unique_session_id)
|
14
14
|
self.unique_session_id = unique_session_id
|
15
15
|
|
16
|
-
save(:
|
16
|
+
save(validate: false)
|
17
17
|
end
|
18
18
|
|
19
19
|
end
|
20
20
|
end
|
21
|
-
end
|
21
|
+
end
|
@@ -9,12 +9,10 @@ module DeviseSecurity
|
|
9
9
|
module ActiveRecord
|
10
10
|
module Schema
|
11
11
|
include DeviseSecurity::Schema
|
12
|
-
|
13
|
-
|
14
12
|
end
|
15
13
|
end
|
16
14
|
end
|
17
15
|
end
|
18
16
|
|
19
17
|
ActiveRecord::ConnectionAdapters::Table.send :include, DeviseSecurity::Orm::ActiveRecord::Schema
|
20
|
-
ActiveRecord::ConnectionAdapters::TableDefinition.send :include, DeviseSecurity::Orm::ActiveRecord::Schema
|
18
|
+
ActiveRecord::ConnectionAdapters::TableDefinition.send :include, DeviseSecurity::Orm::ActiveRecord::Schema
|
@@ -7,13 +7,13 @@ module DeviseSecurity::Patches
|
|
7
7
|
self.resource = resource_class.send_confirmation_instructions(params[resource_name])
|
8
8
|
|
9
9
|
if successfully_sent?(resource)
|
10
|
-
respond_with({}, :
|
10
|
+
respond_with({}, location: after_resending_confirmation_instructions_path_for(resource_name))
|
11
11
|
else
|
12
12
|
respond_with(resource)
|
13
13
|
end
|
14
14
|
else
|
15
15
|
flash[:alert] = t('devise.invalid_captcha') if is_navigational_format?
|
16
|
-
respond_with({}, :
|
16
|
+
respond_with({}, location: new_confirmation_path(resource_name))
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|