devise-security 0.13.0 → 0.14.0.rc1

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -1
  3. data/.gitignore +1 -0
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +30 -12
  6. data/Appraisals +21 -5
  7. data/Gemfile +8 -1
  8. data/README.md +47 -7
  9. data/app/views/devise/paranoid_verification_code/show.html.erb +1 -1
  10. data/app/views/devise/password_expired/show.html.erb +1 -1
  11. data/config/locales/de.yml +11 -11
  12. data/config/locales/fr.yml +13 -13
  13. data/config/locales/ja.yml +29 -0
  14. data/devise-security.gemspec +12 -6
  15. data/gemfiles/rails_4.2_stable.gemfile +9 -1
  16. data/gemfiles/rails_5.0_stable.gemfile +8 -1
  17. data/gemfiles/rails_5.1_stable.gemfile +8 -1
  18. data/gemfiles/{rails_5.2.0.gemfile → rails_5.2_stable.gemfile} +8 -1
  19. data/gemfiles/rails_6.0_beta.gemfile +15 -0
  20. data/lib/devise-security.rb +2 -4
  21. data/lib/devise-security/models/{old_password.rb → active_record/old_password.rb} +1 -2
  22. data/lib/devise-security/models/compatibility.rb +6 -15
  23. data/lib/devise-security/models/compatibility/active_record.rb +29 -0
  24. data/lib/devise-security/models/compatibility/mongoid.rb +21 -0
  25. data/lib/devise-security/models/database_authenticatable_patch.rb +1 -1
  26. data/lib/devise-security/models/expirable.rb +6 -2
  27. data/lib/devise-security/models/mongoid/old_password.rb +21 -0
  28. data/lib/devise-security/models/password_archivable.rb +16 -7
  29. data/lib/devise-security/models/password_expirable.rb +5 -0
  30. data/lib/devise-security/models/secure_validatable.rb +2 -2
  31. data/lib/devise-security/orm/mongoid.rb +7 -0
  32. data/lib/devise-security/patches/controller_security_question.rb +1 -1
  33. data/lib/devise-security/version.rb +1 -1
  34. data/lib/generators/devise_security/install_generator.rb +1 -1
  35. data/test/dummy/app/models/application_record.rb +8 -2
  36. data/test/dummy/app/models/application_user_record.rb +11 -0
  37. data/test/dummy/app/models/captcha_user.rb +5 -2
  38. data/test/dummy/app/models/mongoid/confirmable_fields.rb +13 -0
  39. data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +17 -0
  40. data/test/dummy/app/models/mongoid/expirable_fields.rb +11 -0
  41. data/test/dummy/app/models/mongoid/lockable_fields.rb +13 -0
  42. data/test/dummy/app/models/mongoid/mappings.rb +13 -0
  43. data/test/dummy/app/models/mongoid/omniauthable_fields.rb +11 -0
  44. data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +10 -0
  45. data/test/dummy/app/models/mongoid/password_archivable_fields.rb +9 -0
  46. data/test/dummy/app/models/mongoid/password_expirable_fields.rb +10 -0
  47. data/test/dummy/app/models/mongoid/recoverable_fields.rb +11 -0
  48. data/test/dummy/app/models/mongoid/registerable_fields.rb +19 -0
  49. data/test/dummy/app/models/mongoid/rememberable_fields.rb +10 -0
  50. data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +11 -0
  51. data/test/dummy/app/models/mongoid/security_questionable_fields.rb +13 -0
  52. data/test/dummy/app/models/mongoid/session_limitable_fields.rb +10 -0
  53. data/test/dummy/app/models/mongoid/timeoutable_fields.rb +9 -0
  54. data/test/dummy/app/models/mongoid/trackable_fields.rb +14 -0
  55. data/test/dummy/app/models/mongoid/validatable_fields.rb +7 -0
  56. data/test/dummy/app/models/secure_user.rb +5 -1
  57. data/test/dummy/app/models/security_question_user.rb +7 -4
  58. data/test/dummy/app/models/user.rb +5 -0
  59. data/test/dummy/app/models/widget.rb +4 -0
  60. data/test/dummy/app/mongoid/admin.rb +31 -0
  61. data/test/dummy/app/mongoid/one_user.rb +58 -0
  62. data/test/dummy/app/mongoid/shim.rb +25 -0
  63. data/test/dummy/app/mongoid/user_on_engine.rb +41 -0
  64. data/test/dummy/app/mongoid/user_on_main_app.rb +41 -0
  65. data/test/dummy/app/mongoid/user_with_validations.rb +37 -0
  66. data/test/dummy/app/mongoid/user_without_email.rb +35 -0
  67. data/test/dummy/config/application.rb +10 -7
  68. data/test/dummy/config/environments/test.rb +2 -2
  69. data/test/dummy/config/initializers/devise.rb +3 -4
  70. data/test/dummy/config/initializers/migration_class.rb +8 -6
  71. data/test/dummy/config/mongoid.yml +6 -0
  72. data/test/dummy/lib/shared_expirable_columns.rb +14 -0
  73. data/test/dummy/lib/shared_security_questions_fields.rb +16 -0
  74. data/test/dummy/lib/shared_user.rb +32 -0
  75. data/test/dummy/lib/shared_user_with_password_verification.rb +13 -0
  76. data/test/dummy/lib/shared_user_without_email.rb +28 -0
  77. data/test/dummy/lib/shared_user_without_omniauth.rb +15 -0
  78. data/test/dummy/lib/shared_verification_fields.rb +15 -0
  79. data/test/orm/active_record.rb +12 -0
  80. data/test/orm/mongoid.rb +12 -0
  81. data/test/support/mongoid.yml +6 -0
  82. data/test/test_helper.rb +16 -9
  83. data/test/test_install_generator.rb +1 -0
  84. data/test/test_password_archivable.rb +6 -7
  85. data/test/test_password_expirable.rb +3 -1
  86. data/test/test_secure_validatable.rb +11 -10
  87. data/test/test_security_question_controller.rb +9 -11
  88. metadata +125 -16
@@ -2,7 +2,14 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "omniauth"
6
5
  gem "rails", "~> 5.2.0"
7
6
 
7
+ group :active_record do
8
+ gem "sqlite3", "~> 1.3.0"
9
+ end
10
+
11
+ group :mongoid do
12
+ gem "mongoid", "~> 6.0"
13
+ end
14
+
8
15
  gemspec path: "../"
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 6.0.0.beta1"
6
+
7
+ group :active_record do
8
+ gem "sqlite3", "~> 1.3.0"
9
+ end
10
+
11
+ group :mongoid do
12
+ gem "mongoid", "~> 6.0"
13
+ end
14
+
15
+ gemspec path: "../"
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record'
3
+ require DEVISE_ORM.to_s if DEVISE_ORM.in? [:active_record, :mongoid]
4
4
  require 'active_support/core_ext/integer'
5
5
  require 'active_support/ordered_hash'
6
6
  require 'active_support/concern'
7
7
  require 'devise'
8
8
 
9
9
  module Devise
10
-
11
10
  # Number of seconds that passwords are valid (e.g 3.months)
12
11
  # Disable pasword expiration with +false+
13
12
  # Expire only on demand with +true+
@@ -104,7 +103,6 @@ Devise.add_module :paranoid_verification, controller: :paranoid_verification_cod
104
103
  # requires
105
104
  require 'devise-security/routes'
106
105
  require 'devise-security/rails'
107
- require 'devise-security/orm/active_record'
108
- require 'devise-security/models/old_password'
106
+ require "devise-security/orm/#{DEVISE_ORM}"
109
107
  require 'devise-security/models/database_authenticatable_patch'
110
108
  require 'devise-security/models/paranoid_verification'
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record'
4
- class OldPassword < ActiveRecord::Base
3
+ class OldPassword < ApplicationRecord
5
4
  belongs_to :password_archivable, polymorphic: true
6
5
  end
@@ -1,24 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "compatibility/#{DEVISE_ORM}"
4
+
3
5
  module Devise
4
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.
5
10
  module Compatibility
6
11
  extend ActiveSupport::Concern
7
-
8
- # for backwards compatibility with Rails < 5.1.x
9
- unless Devise.activerecord51?
10
- def saved_change_to_encrypted_password?
11
- encrypted_password_changed?
12
- end
13
-
14
- def encrypted_password_before_last_save
15
- previous_changes['encrypted_password'].try(:first)
16
- end
17
-
18
- def will_save_change_to_encrypted_password?
19
- changed_attributes['encrypted_password'].present?
20
- end
21
- end
12
+ include "Devise::Models::Compatibility::#{DEVISE_ORM.to_s.classify}".constantize
22
13
  end
23
14
  end
24
15
  end
@@ -0,0 +1,29 @@
1
+ module Devise
2
+ module Models
3
+ module Compatibility
4
+ module ActiveRecord
5
+ extend ActiveSupport::Concern
6
+ unless Devise.activerecord51?
7
+ # When the record was saved, was the +encrypted_password+ changed?
8
+ # @return [Boolean]
9
+ def saved_change_to_encrypted_password?
10
+ encrypted_password_changed?
11
+ end
12
+
13
+ # The encrypted password that existed before the record was saved
14
+ # @return [String]
15
+ # @return [nil] if an +encrypted_password+ had not been set
16
+ def encrypted_password_before_last_save
17
+ previous_changes['encrypted_password'].try(:first)
18
+ end
19
+
20
+ # When the record is saved, will the +encrypted_password+ be changed?
21
+ # @return [Boolean]
22
+ def will_save_change_to_encrypted_password?
23
+ changed_attributes['encrypted_password'].present?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ module Devise
2
+ module Models
3
+ module Compatibility
4
+ module Mongoid
5
+ extend ActiveSupport::Concern
6
+
7
+ # Will saving this record change the +email+ attribute?
8
+ # @return [Boolean]
9
+ def will_save_change_to_email?
10
+ changed.include? 'email'
11
+ end
12
+
13
+ # Will saving this record change the +encrypted_password+ attribute?
14
+ # @return [Boolean]
15
+ def will_save_change_to_encrypted_password?
16
+ changed.include? 'encrypted_password'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -10,7 +10,7 @@ module Devise
10
10
  new_password_confirmation = params[:password_confirmation]
11
11
 
12
12
  result = if valid_password?(current_password) && new_password.present? && new_password_confirmation.present?
13
- update_attributes(params, *options)
13
+ update(params, *options)
14
14
  else
15
15
  self.assign_attributes(params, *options)
16
16
  self.valid?
@@ -22,7 +22,11 @@ module Devise
22
22
 
23
23
  # Updates +last_activity_at+, called from a Warden::Manager.after_set_user hook.
24
24
  def update_last_activity!
25
- self.update_column(:last_activity_at, Time.now.utc)
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
26
30
  end
27
31
 
28
32
  # Tells if the account has expired
@@ -103,7 +107,7 @@ module Devise
103
107
  # @example Overwritten version to blank out the object.
104
108
  # def self.delete_all_expired_for(time = 90.days)
105
109
  # expired_for(time).each do |u|
106
- # u.update_attributes first_name: nil, last_name: nil
110
+ # u.update first_name: nil, last_name: nil
107
111
  # end
108
112
  # end
109
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,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'compatibility'
4
+ require_relative "#{DEVISE_ORM}/old_password"
4
5
 
5
6
  module Devise
6
7
  module Models
@@ -11,7 +12,7 @@ module Devise
11
12
  include Devise::Models::DatabaseAuthenticatable
12
13
 
13
14
  included do
14
- has_many :old_passwords, as: :password_archivable, dependent: :destroy
15
+ has_many :old_passwords, class_name: 'OldPassword', as: :password_archivable, dependent: :destroy
15
16
  before_update :archive_password, if: :will_save_change_to_encrypted_password?
16
17
  validate :validate_password_archive, if: :password_present?
17
18
  end
@@ -38,11 +39,15 @@ module Devise
38
39
  # @return [true] if current password was used previously
39
40
  # @return [false] if disabled or not previously used
40
41
  def password_archive_included?
41
- return false unless max_old_passwords > 0
42
- old_passwords_including_cur_change = old_passwords.order(:id).reverse_order.limit(max_old_passwords).pluck(:encrypted_password)
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)
43
45
  old_passwords_including_cur_change << encrypted_password_was # include most recent change in list, but don't save it yet!
44
46
  old_passwords_including_cur_change.any? do |old_password|
45
- self.class.new(encrypted_password: old_password).valid_password?(password)
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)
46
51
  end
47
52
  end
48
53
 
@@ -60,11 +65,15 @@ module Devise
60
65
 
61
66
  private
62
67
 
63
- # archive the last password before save and delete all to old passwords from archive
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
64
71
  def archive_password
65
- if max_old_passwords > 0
72
+ if max_old_passwords.positive?
73
+ return true if old_passwords.where(encrypted_password: encrypted_password_was).exists?
74
+
66
75
  old_passwords.create!(encrypted_password: encrypted_password_was) if encrypted_password_was.present?
67
- old_passwords.order(:id).reverse_order.offset(max_old_passwords).destroy_all
76
+ old_passwords.order(created_at: :desc).offset(max_old_passwords).destroy_all
68
77
  else
69
78
  old_passwords.destroy_all
70
79
  end
@@ -39,6 +39,7 @@ module Devise::Models
39
39
  # @return [Boolean]
40
40
  def need_change_password!
41
41
  return unless password_expiration_enabled?
42
+
42
43
  need_change_password
43
44
  save(validate: false)
44
45
  end
@@ -51,6 +52,7 @@ module Devise::Models
51
52
  # @return [void]
52
53
  def need_change_password
53
54
  return unless password_expiration_enabled?
55
+
54
56
  self.password_changed_at = nil
55
57
  end
56
58
  alias expire_password need_change_password
@@ -70,6 +72,7 @@ module Devise::Models
70
72
  def password_change_requested?
71
73
  return false unless password_expiration_enabled?
72
74
  return false if new_record?
75
+
73
76
  password_changed_at.nil?
74
77
  end
75
78
 
@@ -79,6 +82,7 @@ module Devise::Models
79
82
  return false if new_record?
80
83
  return false unless password_expiration_enabled?
81
84
  return false if expire_password_on_demand?
85
+
82
86
  password_changed_at < expire_password_after.seconds.ago
83
87
  end
84
88
  alias password_expired? password_too_old?
@@ -89,6 +93,7 @@ module Devise::Models
89
93
  # @note called as a +before_save+ hook
90
94
  def update_password_changed
91
95
  return unless (new_record? || encrypted_password_changed?) && !password_changed_at_changed?
96
+
92
97
  self.password_changed_at = Time.zone.now
93
98
  end
94
99
 
@@ -90,8 +90,8 @@ module Devise
90
90
 
91
91
  def has_uniqueness_validation_of_login?
92
92
  validators.any? do |validator|
93
- validator.kind_of?(ActiveRecord::Validations::UniquenessValidator) &&
94
- validator.attributes.include?(login_attribute)
93
+ validator_orm_klass = DEVISE_ORM == :active_record ? ActiveRecord::Validations::UniquenessValidator : ::Mongoid::Validatable::UniquenessValidator
94
+ validator.kind_of?(validator_orm_klass) && validator.attributes.include?(login_attribute)
95
95
  end
96
96
  end
97
97
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveSupport.on_load(:mongoid) do
4
+ require 'orm_adapter/adapters/mongoid'
5
+
6
+ Mongoid::Document::ClassMethods.send :include, Devise::Models
7
+ end
@@ -9,6 +9,7 @@ module DeviseSecurity::Patches
9
9
  end
10
10
 
11
11
  private
12
+
12
13
  def check_security_question
13
14
  # only find via email, not login
14
15
  resource = resource_class.find_or_initialize_with_error_by(:email, params[resource_name][:email], :not_found)
@@ -19,4 +20,3 @@ module DeviseSecurity::Patches
19
20
  end
20
21
  end
21
22
  end
22
-
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeviseSecurity
4
- VERSION = '0.13.0'
4
+ VERSION = '0.14.0.rc1'
5
5
  end
@@ -4,7 +4,7 @@ module DeviseSecurity
4
4
  module Generators
5
5
  # Generator for Rails to create or append to a Devise initializer.
6
6
  class InstallGenerator < Rails::Generators::Base
7
- LOCALES = %w[en es de fr it tr].freeze
7
+ LOCALES = %w[en es de fr it ja tr].freeze
8
8
 
9
9
  source_root File.expand_path('../../templates', __FILE__)
10
10
  desc 'Install the devise security extension'
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ApplicationRecord < ActiveRecord::Base
4
- self.abstract_class = true
3
+ if DEVISE_ORM == :active_record
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ else
8
+ class ApplicationRecord
9
+ include Mongoid::Document
10
+ end
5
11
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ if DEVISE_ORM == :active_record
3
+ class ApplicationUserRecord < ActiveRecord::Base
4
+ self.table_name = 'users'
5
+ end
6
+ else
7
+ class ApplicationUserRecord
8
+ include Mongoid::Document
9
+ store_in collection: 'users'
10
+ end
11
+ end
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CaptchaUser < ApplicationRecord
4
- self.table_name = 'users'
3
+ class CaptchaUser < ApplicationUserRecord
5
4
  devise :database_authenticatable, :password_archivable,
6
5
  :paranoid_verification, :password_expirable
6
+ if DEVISE_ORM == :mongoid
7
+ require './test/dummy/app/models/mongoid/mappings'
8
+ include ::Mongoid::Mappings
9
+ end
7
10
  end
@@ -0,0 +1,13 @@
1
+ module ConfirmableFields
2
+ extend ::ActiveSupport::Concern
3
+
4
+ included do
5
+ include Mongoid::Document
6
+
7
+ ## Confirmable
8
+ field :confirmation_token, type: String
9
+ field :confirmed_at, type: Time
10
+ field :confirmation_sent_at, type: Time
11
+ field :unconfirmed_email, type: String # Only if using reconfirmable
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module DatabaseAuthenticatableFields
2
+ extend ::ActiveSupport::Concern
3
+
4
+ included do
5
+ include Mongoid::Document
6
+
7
+ ## Database authenticatable
8
+ field :username, type: String
9
+ field :email, type: String, default: ""
10
+ #validates_presence_of :email
11
+
12
+ field :encrypted_password, type: String, default: ""
13
+ validates_presence_of :encrypted_password
14
+
15
+ include Mongoid::Timestamps
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module ExpirableFields
2
+ extend ::ActiveSupport::Concern
3
+
4
+ included do
5
+ include Mongoid::Document
6
+
7
+ ## Expirable
8
+ field :expired_at, type: Time
9
+ field :last_activity_at, type: Time
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module LockableFields
2
+ extend ::ActiveSupport::Concern
3
+
4
+ included do
5
+ include Mongoid::Document
6
+
7
+ field :failed_attempts, type: Integer, default: 0 # Only if lock strategy is :failed_attempts
8
+ field :unlock_token, type: String # Only if unlock strategy is :email or :both
9
+ field :locked_at, type: Time
10
+ include Mongoid::Timestamps
11
+ index({ unlock_token: 1 }, { unique: true })
12
+ end
13
+ end