devise-security 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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