devise-security 0.11.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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +38 -0
  4. data/.rubocop.yml +42 -0
  5. data/.travis.yml +14 -0
  6. data/Gemfile +2 -0
  7. data/Gemfile.lock +199 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.md +263 -0
  10. data/Rakefile +26 -0
  11. data/app/controllers/devise/paranoid_verification_code_controller.rb +42 -0
  12. data/app/controllers/devise/password_expired_controller.rb +48 -0
  13. data/app/views/devise/paranoid_verification_code/show.html.erb +10 -0
  14. data/app/views/devise/password_expired/show.html.erb +16 -0
  15. data/config/locales/de.yml +16 -0
  16. data/config/locales/en.yml +17 -0
  17. data/config/locales/es.yml +17 -0
  18. data/config/locales/it.yml +10 -0
  19. data/devise-security.gemspec +34 -0
  20. data/lib/devise-security.rb +106 -0
  21. data/lib/devise-security/controllers/helpers.rb +96 -0
  22. data/lib/devise-security/hooks/expirable.rb +10 -0
  23. data/lib/devise-security/hooks/paranoid_verification.rb +5 -0
  24. data/lib/devise-security/hooks/password_expirable.rb +5 -0
  25. data/lib/devise-security/hooks/session_limitable.rb +27 -0
  26. data/lib/devise-security/models/database_authenticatable_patch.rb +26 -0
  27. data/lib/devise-security/models/expirable.rb +120 -0
  28. data/lib/devise-security/models/old_password.rb +4 -0
  29. data/lib/devise-security/models/paranoid_verification.rb +35 -0
  30. data/lib/devise-security/models/password_archivable.rb +80 -0
  31. data/lib/devise-security/models/password_expirable.rb +67 -0
  32. data/lib/devise-security/models/secure_validatable.rb +100 -0
  33. data/lib/devise-security/models/security_questionable.rb +18 -0
  34. data/lib/devise-security/models/session_limitable.rb +21 -0
  35. data/lib/devise-security/orm/active_record.rb +20 -0
  36. data/lib/devise-security/patches.rb +21 -0
  37. data/lib/devise-security/patches/confirmations_controller_captcha.rb +21 -0
  38. data/lib/devise-security/patches/confirmations_controller_security_question.rb +25 -0
  39. data/lib/devise-security/patches/controller_captcha.rb +17 -0
  40. data/lib/devise-security/patches/controller_security_question.rb +20 -0
  41. data/lib/devise-security/patches/passwords_controller_captcha.rb +20 -0
  42. data/lib/devise-security/patches/passwords_controller_security_question.rb +24 -0
  43. data/lib/devise-security/patches/registrations_controller_captcha.rb +33 -0
  44. data/lib/devise-security/patches/sessions_controller_captcha.rb +24 -0
  45. data/lib/devise-security/patches/unlocks_controller_captcha.rb +20 -0
  46. data/lib/devise-security/patches/unlocks_controller_security_question.rb +24 -0
  47. data/lib/devise-security/rails.rb +17 -0
  48. data/lib/devise-security/routes.rb +17 -0
  49. data/lib/devise-security/schema.rb +59 -0
  50. data/lib/devise-security/version.rb +3 -0
  51. data/lib/generators/devise-security/install_generator.rb +26 -0
  52. data/lib/generators/templates/devise-security.rb +38 -0
  53. data/test/dummy/Rakefile +6 -0
  54. data/test/dummy/app/controllers/application_controller.rb +2 -0
  55. data/test/dummy/app/controllers/captcha/sessions_controller.rb +3 -0
  56. data/test/dummy/app/controllers/foos_controller.rb +0 -0
  57. data/test/dummy/app/controllers/security_question/unlocks_controller.rb +3 -0
  58. data/test/dummy/app/models/.gitkeep +0 -0
  59. data/test/dummy/app/models/captcha_user.rb +5 -0
  60. data/test/dummy/app/models/secure_user.rb +3 -0
  61. data/test/dummy/app/models/security_question_user.rb +6 -0
  62. data/test/dummy/app/models/user.rb +5 -0
  63. data/test/dummy/app/views/foos/index.html.erb +0 -0
  64. data/test/dummy/config.ru +4 -0
  65. data/test/dummy/config/application.rb +24 -0
  66. data/test/dummy/config/boot.rb +6 -0
  67. data/test/dummy/config/database.yml +7 -0
  68. data/test/dummy/config/environment.rb +5 -0
  69. data/test/dummy/config/environments/test.rb +27 -0
  70. data/test/dummy/config/initializers/devise.rb +9 -0
  71. data/test/dummy/config/initializers/migration_class.rb +6 -0
  72. data/test/dummy/config/routes.rb +10 -0
  73. data/test/dummy/config/secrets.yml +3 -0
  74. data/test/dummy/db/migrate/20120508165529_create_tables.rb +33 -0
  75. data/test/dummy/db/migrate/20150402165590_add_verification_columns.rb +11 -0
  76. data/test/dummy/db/migrate/20150407162345_add_verification_attempt_column.rb +9 -0
  77. data/test/dummy/db/migrate/20160320162345_add_security_questions_fields.rb +8 -0
  78. data/test/test_captcha_controller.rb +58 -0
  79. data/test/test_helper.rb +13 -0
  80. data/test/test_install_generator.rb +16 -0
  81. data/test/test_paranoid_verification.rb +124 -0
  82. data/test/test_password_archivable.rb +61 -0
  83. data/test/test_password_expirable.rb +32 -0
  84. data/test/test_password_expired_controller.rb +29 -0
  85. data/test/test_secure_validatable.rb +85 -0
  86. data/test/test_security_question_controller.rb +60 -0
  87. metadata +315 -0
@@ -0,0 +1,10 @@
1
+ # Updates the last_activity_at fields from the record. Only when the user is active
2
+ # for authentication and authenticated.
3
+ # An expiry of the account is only checked on sign in OR on manually setting the
4
+ # expired_at to the past (see Devise::Models::Expirable for this)
5
+ Warden::Manager.after_set_user do |record, warden, options|
6
+ if record && record.respond_to?(:active_for_authentication?) && record.active_for_authentication? &&
7
+ warden.authenticated?(options[:scope]) && record.respond_to?(:update_last_activity!)
8
+ record.update_last_activity!
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ Warden::Manager.after_set_user do |record, warden, options|
2
+ if record.respond_to?(:need_paranoid_verification?)
3
+ warden.session(options[:scope])['paranoid_verify'] = record.need_paranoid_verification?
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ Warden::Manager.after_authentication do |record, warden, options|
2
+ if record.respond_to?(:need_change_password?)
3
+ warden.session(options[:scope])['password_expired'] = record.need_change_password?
4
+ end
5
+ end
@@ -0,0 +1,27 @@
1
+ # After each sign in, update unique_session_id.
2
+ # This is only triggered when the user is explicitly set (with set_user)
3
+ # and on authentication. Retrieving the user from session (:fetch) does
4
+ # not trigger it.
5
+ Warden::Manager.after_set_user :except => :fetch do |record, warden, options|
6
+ if record.respond_to?(:update_unique_session_id!) && warden.authenticated?(options[:scope])
7
+ unique_session_id = Devise.friendly_token
8
+ warden.session(options[:scope])['unique_session_id'] = unique_session_id
9
+ record.update_unique_session_id!(unique_session_id)
10
+ end
11
+ end
12
+
13
+ # Each time a record is fetched from session we check if a new session from another
14
+ # browser was opened for the record or not, based on a unique session identifier.
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 :only => :fetch do |record, warden, options|
17
+ scope = options[:scope]
18
+ env = warden.request.env
19
+
20
+ if record.respond_to?(:unique_session_id) && warden.authenticated?(scope) && options[:store] != false
21
+ if record.unique_session_id != warden.session(scope)['unique_session_id'] && !env['devise.skip_session_limitable']
22
+ warden.raw_session.clear
23
+ warden.logout(scope)
24
+ throw :warden, :scope => scope, :message => :session_limited
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ module Devise
2
+ module Models
3
+ module DatabaseAuthenticatablePatch
4
+ def update_with_password(params, *options)
5
+ current_password = params.delete(:current_password)
6
+
7
+ new_password = params[:password]
8
+ new_password_confirmation = params[:password_confirmation]
9
+
10
+ result = if valid_password?(current_password) && new_password.present? && new_password_confirmation.present?
11
+ update_attributes(params, *options)
12
+ else
13
+ self.assign_attributes(params, *options)
14
+ self.valid?
15
+ self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
16
+ self.errors.add(:password, new_password.blank? ? :blank : :invalid)
17
+ self.errors.add(:password_confirmation, new_password_confirmation.blank? ? :blank : :invalid)
18
+ false
19
+ end
20
+
21
+ clean_up_passwords
22
+ result
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,120 @@
1
+ require 'devise-security/hooks/expirable'
2
+
3
+ module Devise
4
+ module Models
5
+ # Deactivate the account after a configurable amount of time. To be able to
6
+ # tell, it tracks activity about your account with the following columns:
7
+ #
8
+ # * last_activity_at - A timestamp updated when the user requests a page (only signed in)
9
+ #
10
+ # == Options
11
+ # +:expire_after+ - Time interval to expire accounts after
12
+ #
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
16
+ # of time (for example 90 days).
17
+ #
18
+ module Expirable
19
+ extend ActiveSupport::Concern
20
+
21
+ # Updates +last_activity_at+, called from a Warden::Manager.after_set_user hook.
22
+ def update_last_activity!
23
+ self.update_column(:last_activity_at, Time.now.utc)
24
+ end
25
+
26
+ # Tells if the account has expired
27
+ #
28
+ # @return [bool]
29
+ def expired?
30
+ # expired_at set (manually, via cron, etc.)
31
+ return self.expired_at < Time.now.utc unless self.expired_at.nil?
32
+ # if it is not set, check the last activity against configured expire_after time range
33
+ return self.last_activity_at < self.class.expire_after.ago unless self.last_activity_at.nil?
34
+ # if last_activity_at is nil as well, the user has to be 'fresh' and is therefore not expired
35
+ false
36
+ end
37
+
38
+ # Expire an account. This is for cron jobs and manually expiring of accounts.
39
+ #
40
+ # @example
41
+ # User.expire!
42
+ # User.expire! 1.week.from_now
43
+ # @note +expired_at+ can be in the future as well
44
+ def expire!(at = Time.now.utc)
45
+ self.expired_at = at
46
+ save(:validate => false)
47
+ end
48
+
49
+ # Overwrites active_for_authentication? from Devise::Models::Activatable
50
+ # for verifying whether a user is active to sign in or not. If the account
51
+ # is expired, it should never be allowed.
52
+ #
53
+ # @return [bool]
54
+ def active_for_authentication?
55
+ super && !self.expired?
56
+ end
57
+
58
+ # The message sym, if {#active_for_authentication?} returns +false+. E.g. needed
59
+ # for i18n.
60
+ def inactive_message
61
+ !self.expired? ? super : :expired
62
+ end
63
+
64
+ module ClassMethods
65
+ ::Devise::Models.config(self, :expire_after, :delete_expired_after)
66
+
67
+ # Sample method for daily cron to mark expired entries.
68
+ #
69
+ # @example You can overide this in your +resource+ model
70
+ # def self.mark_expired
71
+ # puts 'overwritten mark_expired'
72
+ # end
73
+ def mark_expired
74
+ all.each do |u|
75
+ u.expire! if u.expired? && u.expired_at.nil?
76
+ end
77
+ return
78
+ end
79
+
80
+ # Scope method to collect all expired users since +time+ ago
81
+ def expired_for(time = delete_expired_after)
82
+ where('expired_at < ?', time.seconds.ago)
83
+ end
84
+
85
+ # Sample method for daily cron to delete all expired entries after a
86
+ # given amount of +time+.
87
+ #
88
+ # In your overwritten method you can "blank out" the object instead of
89
+ # deleting it.
90
+ #
91
+ # *Word of warning*: You have to handle the dependent method
92
+ # on the +resource+ relations (+:destroy+ or +:nullify+) and catch this
93
+ # behavior (see http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Deleting+from+associations).
94
+ #
95
+ # @example
96
+ # Resource.delete_all_expired_for 90.days
97
+ # @example You can overide this in your +resource+ model
98
+ # def self.delete_all_expired_for(time = 90.days)
99
+ # puts 'overwritten delete call'
100
+ # end
101
+ # @example Overwritten version to blank out the object.
102
+ # def self.delete_all_expired_for(time = 90.days)
103
+ # expired_for(time).each do |u|
104
+ # u.update_attributes first_name: nil, last_name: nil
105
+ # end
106
+ # end
107
+ def delete_all_expired_for(time)
108
+ expired_for(time).delete_all
109
+ end
110
+
111
+ # Version of {#delete_all_expired_for} without arguments (uses
112
+ # configured +delete_expired_after+ default value).
113
+ # @see #delete_all_expired_for
114
+ def delete_all_expired
115
+ delete_all_expired_for(delete_expired_after)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,4 @@
1
+ require 'active_record'
2
+ class OldPassword < ActiveRecord::Base
3
+ belongs_to :password_archivable, :polymorphic => true
4
+ end
@@ -0,0 +1,35 @@
1
+ require 'devise-security/hooks/paranoid_verification'
2
+
3
+ module Devise
4
+ module Models
5
+ # PasswordExpirable takes care of change password after
6
+ module ParanoidVerification
7
+ extend ActiveSupport::Concern
8
+
9
+ def need_paranoid_verification?
10
+ !!paranoid_verification_code
11
+ end
12
+
13
+ def verify_code(code)
14
+ attempt = paranoid_verification_attempt
15
+
16
+ if (attempt += 1) >= Devise.paranoid_code_regenerate_after_attempt
17
+ generate_paranoid_code
18
+ elsif code == paranoid_verification_code
19
+ attempt = 0
20
+ update_without_password paranoid_verification_code: nil, paranoid_verified_at: Time.now, paranoid_verification_attempt: attempt
21
+ else
22
+ update_without_password paranoid_verification_attempt: attempt
23
+ end
24
+ end
25
+
26
+ def paranoid_attempts_remaining
27
+ Devise.paranoid_code_regenerate_after_attempt - paranoid_verification_attempt
28
+ end
29
+
30
+ def generate_paranoid_code
31
+ update_without_password paranoid_verification_code: Devise.verification_code_generator.call(), paranoid_verification_attempt: 0
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,80 @@
1
+ module Devise
2
+ module Models
3
+ # PasswordArchivable
4
+ module PasswordArchivable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ has_many :old_passwords, as: :password_archivable, dependent: :destroy
9
+ before_update :archive_password
10
+ validate :validate_password_archive
11
+ end
12
+
13
+ def validate_password_archive
14
+ errors.add(:password, :taken_in_past) if encrypted_password_changed? and password_archive_included?
15
+ end
16
+
17
+ # validate is the password used in the past
18
+ def password_archive_included?
19
+ unless deny_old_passwords.is_a? 1.class
20
+ if deny_old_passwords.is_a? TrueClass and archive_count > 0
21
+ self.deny_old_passwords = archive_count
22
+ else
23
+ self.deny_old_passwords = 0
24
+ end
25
+ end
26
+
27
+ if self.class.deny_old_passwords > 0 and not 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
35
+ end
36
+
37
+ false
38
+ end
39
+
40
+ def password_changed_to_same?
41
+ pass_change = encrypted_password_change
42
+ pass_change && pass_change.first == pass_change.last
43
+ end
44
+
45
+ def deny_old_passwords
46
+ self.class.deny_old_passwords
47
+ end
48
+
49
+ def deny_old_passwords=(count)
50
+ self.class.deny_old_passwords = count
51
+ end
52
+
53
+ def archive_count
54
+ self.class.password_archiving_count
55
+ end
56
+
57
+ private
58
+
59
+ # archive the last password before save and delete all to old passwords from archive
60
+ def archive_password
61
+ if encrypted_password_changed?
62
+ if archive_count.to_i > 0
63
+ old_passwords.create! old_password_params
64
+ old_passwords.order(:id).reverse_order.offset(archive_count).destroy_all
65
+ else
66
+ old_passwords.destroy_all
67
+ end
68
+ end
69
+ end
70
+
71
+ def old_password_params
72
+ { encrypted_password: encrypted_password_change.first }
73
+ end
74
+
75
+ module ClassMethods
76
+ ::Devise::Models.config(self, :password_archiving_count, :deny_old_passwords)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,67 @@
1
+ require 'devise-security/hooks/password_expirable'
2
+
3
+ module Devise
4
+ module Models
5
+
6
+ # PasswordExpirable takes care of change password after
7
+ module PasswordExpirable
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ before_save :update_password_changed
12
+ end
13
+
14
+ # is an password change required?
15
+ def need_change_password?
16
+ if expired_password_after_numeric?
17
+ self.password_changed_at.nil? or self.password_changed_at < self.expire_password_after.seconds.ago
18
+ else
19
+ false
20
+ end
21
+ end
22
+
23
+ # set a fake datetime so a password change is needed and save the record
24
+ def need_change_password!
25
+ if expired_password_after_numeric?
26
+ need_change_password
27
+ self.save(:validate => false)
28
+ end
29
+ end
30
+
31
+ # set a fake datetime so a password change is needed
32
+ def need_change_password
33
+ if expired_password_after_numeric?
34
+ self.password_changed_at = self.expire_password_after.seconds.ago
35
+ end
36
+
37
+ # is date not set it will set default to need set new password next login
38
+ need_change_password if self.password_changed_at.nil?
39
+
40
+ self.password_changed_at
41
+ end
42
+
43
+ def expire_password_after
44
+ self.class.expire_password_after
45
+ end
46
+
47
+ private
48
+
49
+ # is password changed then update password_cahanged_at
50
+ def update_password_changed
51
+ self.password_changed_at = Time.now if (self.new_record? or self.encrypted_password_changed?) and not self.password_changed_at_changed?
52
+ end
53
+
54
+ def expired_password_after_numeric?
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
59
+
60
+ module ClassMethods
61
+ ::Devise::Models.config(self, :expire_password_after)
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,100 @@
1
+ module Devise
2
+ module Models
3
+ # SecureValidatable creates better validations with more validation for security
4
+ #
5
+ # == Options
6
+ #
7
+ # SecureValidatable adds the following options to devise_for:
8
+ #
9
+ # * +email_regexp+: the regular expression used to validate e-mails;
10
+ # * +password_length+: a range expressing password length. Defaults from devise
11
+ # * +password_regex+: need strong password. Defaults to /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/
12
+ #
13
+ module SecureValidatable
14
+
15
+ def self.included(base)
16
+ base.extend ClassMethods
17
+ assert_secure_validations_api!(base)
18
+
19
+ base.class_eval do
20
+ already_validated_email = false
21
+
22
+ # validate login in a strict way if not yet validated
23
+ unless has_uniqueness_validation_of_login?
24
+ validation_condition = "#{login_attribute}_changed?".to_sym
25
+
26
+ validates login_attribute, :uniqueness => {
27
+ :scope => authentication_keys[1..-1],
28
+ :case_sensitive => !!case_insensitive_keys
29
+ },
30
+ :if => validation_condition
31
+
32
+ already_validated_email = login_attribute.to_s == 'email'
33
+ end
34
+
35
+ unless devise_validation_enabled?
36
+ validates :email, :presence => true, :if => :email_required?
37
+ unless already_validated_email
38
+ validates :email, :uniqueness => true, :allow_blank => true, :if => :email_changed? # check uniq for email ever
39
+ end
40
+
41
+ validates :password, :presence => true, :length => password_length, :confirmation => true, :if => :password_required?
42
+ end
43
+
44
+ # extra validations
45
+ validates :email, :email => email_validation if email_validation # use rails_email_validator or similar
46
+ validates :password, :format => { :with => password_regex, :message => :password_format }, :if => :password_required?
47
+
48
+ # don't allow use same password
49
+ validate :current_equal_password_validation
50
+ end
51
+ end
52
+
53
+ def self.assert_secure_validations_api!(base)
54
+ raise "Could not use SecureValidatable on #{base}" unless base.respond_to?(:validates)
55
+ end
56
+
57
+ def current_equal_password_validation
58
+ if not self.new_record? and not self.encrypted_password_change.nil?
59
+ dummy = self.class.new
60
+ dummy.encrypted_password = self.encrypted_password_change.first
61
+ dummy.password_salt = self.password_salt_change.first if self.respond_to? :password_salt_change and not self.password_salt_change.nil?
62
+ self.errors.add(:password, :equal_to_current_password) if dummy.valid_password?(self.password)
63
+ end
64
+ end
65
+
66
+ protected
67
+
68
+ # Checks whether a password is needed or not. For validations only.
69
+ # Passwords are always required if it's a new record, or if the password
70
+ # or confirmation are being set somewhere.
71
+ def password_required?
72
+ !persisted? || !password.nil? || !password_confirmation.nil?
73
+ end
74
+
75
+ def email_required?
76
+ true
77
+ end
78
+
79
+ module ClassMethods
80
+ Devise::Models.config(self, :password_regex, :password_length, :email_validation)
81
+
82
+ private
83
+ def has_uniqueness_validation_of_login?
84
+ validators.any? do |validator|
85
+ validator.kind_of?(ActiveRecord::Validations::UniquenessValidator) &&
86
+ validator.attributes.include?(login_attribute)
87
+ end
88
+ end
89
+
90
+ def login_attribute
91
+ authentication_keys[0]
92
+ end
93
+
94
+ def devise_validation_enabled?
95
+ self.ancestors.map(&:to_s).include? 'Devise::Models::Validatable'
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end