devise-security 0.14.2 → 0.17.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 +125 -59
 - data/app/controllers/devise/paranoid_verification_code_controller.rb +13 -1
 - data/app/controllers/devise/password_expired_controller.rb +24 -6
 - data/app/views/devise/paranoid_verification_code/show.html.erb +3 -3
 - data/app/views/devise/password_expired/show.html.erb +5 -5
 - data/config/locales/bg.yml +41 -0
 - data/config/locales/by.yml +49 -0
 - data/config/locales/cs.yml +41 -0
 - data/config/locales/de.yml +15 -2
 - data/config/locales/en.yml +15 -2
 - data/config/locales/es.yml +10 -9
 - data/config/locales/fa.yml +41 -0
 - data/config/locales/fr.yml +1 -0
 - data/config/locales/hi.yml +42 -0
 - data/config/locales/it.yml +35 -4
 - data/config/locales/ja.yml +2 -1
 - 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 +1 -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 +59 -50
 - data/lib/devise-security/hooks/password_expirable.rb +2 -0
 - data/lib/devise-security/hooks/session_limitable.rb +21 -11
 - data/lib/devise-security/models/database_authenticatable_patch.rb +15 -5
 - data/lib/devise-security/models/password_archivable.rb +2 -2
 - data/lib/devise-security/models/password_expirable.rb +5 -1
 - data/lib/devise-security/models/secure_validatable.rb +56 -6
 - data/lib/devise-security/models/session_limitable.rb +10 -1
 - data/lib/devise-security/validators/password_complexity_validator.rb +53 -24
 - data/lib/devise-security/version.rb +1 -1
 - data/lib/devise-security.rb +13 -5
 - data/lib/generators/devise_security/install_generator.rb +3 -3
 - data/lib/generators/templates/{devise-security.rb → devise_security.rb} +6 -1
 - data/test/controllers/test_paranoid_verification_code_controller.rb +68 -0
 - data/test/controllers/test_password_expired_controller.rb +121 -19
 - data/test/controllers/test_security_question_controller.rb +16 -40
 - data/test/dummy/app/assets/config/manifest.js +3 -0
 - data/test/dummy/app/controllers/overrides/paranoid_verification_code_controller.rb +7 -0
 - data/test/dummy/app/controllers/overrides/password_expired_controller.rb +7 -0
 - data/test/dummy/app/controllers/widgets_controller.rb +3 -0
 - data/test/dummy/app/models/application_user_record.rb +2 -1
 - data/test/dummy/app/models/mongoid/confirmable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +4 -3
 - data/test/dummy/app/models/mongoid/expirable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/lockable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/mappings.rb +4 -2
 - data/test/dummy/app/models/mongoid/omniauthable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/password_archivable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/password_expirable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/recoverable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/registerable_fields.rb +4 -2
 - data/test/dummy/app/models/mongoid/rememberable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/security_questionable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/session_limitable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/timeoutable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/trackable_fields.rb +2 -0
 - data/test/dummy/app/models/mongoid/validatable_fields.rb +2 -0
 - data/test/dummy/app/models/paranoid_verification_user.rb +26 -0
 - data/test/dummy/app/models/password_expired_user.rb +26 -0
 - data/test/dummy/app/models/user.rb +1 -2
 - data/test/dummy/app/models/widget.rb +1 -3
 - data/test/dummy/app/mongoid/one_user.rb +5 -5
 - data/test/dummy/app/mongoid/user_on_engine.rb +2 -2
 - data/test/dummy/app/mongoid/user_on_main_app.rb +2 -2
 - data/test/dummy/app/mongoid/user_with_validations.rb +3 -3
 - data/test/dummy/app/mongoid/user_without_email.rb +3 -3
 - data/test/dummy/config/application.rb +4 -4
 - data/test/dummy/config/boot.rb +1 -1
 - data/test/dummy/config/environment.rb +1 -1
 - data/test/dummy/config/environments/test.rb +3 -13
 - data/test/dummy/config/initializers/migration_class.rb +1 -8
 - data/test/dummy/config/locales/en.yml +10 -0
 - data/test/dummy/config/mongoid.yml +1 -1
 - data/test/dummy/config/routes.rb +5 -3
 - data/test/dummy/db/migrate/20120508165529_create_tables.rb +3 -3
 - data/test/dummy/lib/shared_expirable_columns.rb +1 -0
 - data/test/dummy/lib/shared_security_questions_fields.rb +1 -0
 - data/test/dummy/lib/shared_user.rb +17 -6
 - data/test/dummy/lib/shared_user_without_email.rb +2 -1
 - data/test/dummy/lib/shared_user_without_omniauth.rb +12 -3
 - data/test/dummy/lib/shared_verification_fields.rb +1 -0
 - data/test/dummy/{app/models/.gitkeep → log/development.log} +0 -0
 - data/test/dummy/log/test.log +101533 -0
 - data/test/integration/test_password_expirable_workflow.rb +53 -0
 - data/test/integration/test_session_limitable_workflow.rb +2 -0
 - data/test/orm/active_record.rb +7 -4
 - data/test/orm/mongoid.rb +2 -1
 - data/test/support/integration_helpers.rb +15 -33
 - data/test/support/mongoid.yml +1 -1
 - data/test/test_compatibility.rb +2 -0
 - data/test/test_complexity_validator.rb +250 -29
 - data/test/test_database_authenticatable_patch.rb +146 -0
 - data/test/test_helper.rb +12 -6
 - data/test/test_install_generator.rb +12 -2
 - data/test/test_paranoid_verification.rb +0 -1
 - data/test/test_password_archivable.rb +34 -11
 - data/test/test_password_expirable.rb +26 -26
 - data/test/test_secure_validatable.rb +292 -50
 - data/test/test_secure_validatable_overrides.rb +185 -0
 - data/test/test_session_limitable.rb +27 -1
 - data/test/tmp/config/initializers/devise_security.rb +49 -0
 - 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 +41 -0
 - data/test/tmp/config/locales/devise.security_extension.en.yml +42 -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 +168 -132
 - data/.codeclimate.yml +0 -63
 - data/.document +0 -5
 - data/.gitignore +0 -43
 - data/.mdlrc +0 -1
 - data/.rubocop.yml +0 -64
 - data/.ruby-version +0 -1
 - data/.travis.yml +0 -39
 - data/Appraisals +0 -35
 - data/Gemfile +0 -10
 - data/Rakefile +0 -27
 - data/devise-security.gemspec +0 -50
 - data/gemfiles/rails_4.2_stable.gemfile +0 -16
 - data/gemfiles/rails_5.0_stable.gemfile +0 -15
 - data/gemfiles/rails_5.1_stable.gemfile +0 -15
 - data/gemfiles/rails_5.2_stable.gemfile +0 -15
 - data/gemfiles/rails_6.0_beta.gemfile +0 -15
 - data/lib/devise-security/orm/active_record.rb +0 -20
 - data/lib/devise-security/schema.rb +0 -66
 - data/test/dummy/app/models/secure_user.rb +0 -9
 
| 
         @@ -40,71 +40,80 @@ module DeviseSecurity 
     | 
|
| 
       40 
40 
     | 
    
         | 
| 
       41 
41 
     | 
    
         
             
                  # controller instance methods
         
     | 
| 
       42 
42 
     | 
    
         | 
| 
       43 
     | 
    
         
            -
             
     | 
| 
       44 
     | 
    
         
            -
             
     | 
| 
       45 
     | 
    
         
            -
             
     | 
| 
       46 
     | 
    
         
            -
             
     | 
| 
       47 
     | 
    
         
            -
             
     | 
| 
       48 
     | 
    
         
            -
             
     | 
| 
       49 
     | 
    
         
            -
             
     | 
| 
       50 
     | 
    
         
            -
             
     | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
       55 
     | 
    
         
            -
             
     | 
| 
       56 
     | 
    
         
            -
             
     | 
| 
       57 
     | 
    
         
            -
             
     | 
| 
       58 
     | 
    
         
            -
             
     | 
| 
       59 
     | 
    
         
            -
             
     | 
| 
      
 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
         
     | 
| 
       60 
68 
     | 
    
         
             
                          end
         
     | 
| 
       61 
69 
     | 
    
         
             
                        end
         
     | 
| 
       62 
70 
     | 
    
         
             
                      end
         
     | 
| 
       63 
71 
     | 
    
         
             
                    end
         
     | 
| 
      
 72 
     | 
    
         
            +
                  end
         
     | 
| 
       64 
73 
     | 
    
         | 
| 
       65 
     | 
    
         
            -
             
     | 
| 
       66 
     | 
    
         
            -
             
     | 
| 
       67 
     | 
    
         
            -
             
     | 
| 
      
 74 
     | 
    
         
            +
                  # lookup if extra (paranoid) code verification is needed
         
     | 
| 
      
 75 
     | 
    
         
            +
                  def handle_paranoid_verification
         
     | 
| 
      
 76 
     | 
    
         
            +
                    return if warden.nil?
         
     | 
| 
       68 
77 
     | 
    
         | 
| 
       69 
     | 
    
         
            -
             
     | 
| 
       70 
     | 
    
         
            -
             
     | 
| 
       71 
     | 
    
         
            -
             
     | 
| 
       72 
     | 
    
         
            -
             
     | 
| 
       73 
     | 
    
         
            -
             
     | 
| 
       74 
     | 
    
         
            -
             
     | 
| 
       75 
     | 
    
         
            -
                          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
         
     | 
| 
       76 
84 
     | 
    
         
             
                        end
         
     | 
| 
       77 
85 
     | 
    
         
             
                      end
         
     | 
| 
       78 
86 
     | 
    
         
             
                    end
         
     | 
| 
      
 87 
     | 
    
         
            +
                  end
         
     | 
| 
       79 
88 
     | 
    
         | 
| 
       80 
     | 
    
         
            -
             
     | 
| 
       81 
     | 
    
         
            -
             
     | 
| 
       82 
     | 
    
         
            -
             
     | 
| 
       83 
     | 
    
         
            -
             
     | 
| 
      
 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
         
     | 
| 
       84 
93 
     | 
    
         | 
| 
       85 
     | 
    
         
            -
             
     | 
| 
       86 
     | 
    
         
            -
             
     | 
| 
       87 
     | 
    
         
            -
             
     | 
| 
      
 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
         
     | 
| 
       88 
97 
     | 
    
         | 
| 
       89 
     | 
    
         
            -
             
     | 
| 
       90 
     | 
    
         
            -
             
     | 
| 
       91 
     | 
    
         
            -
             
     | 
| 
       92 
     | 
    
         
            -
             
     | 
| 
       93 
     | 
    
         
            -
             
     | 
| 
       94 
     | 
    
         
            -
             
     | 
| 
      
 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
         
     | 
| 
       95 
104 
     | 
    
         | 
| 
       96 
     | 
    
         
            -
             
     | 
| 
       97 
     | 
    
         
            -
             
     | 
| 
       98 
     | 
    
         
            -
             
     | 
| 
       99 
     | 
    
         
            -
             
     | 
| 
       100 
     | 
    
         
            -
             
     | 
| 
      
 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
         
     | 
| 
       101 
110 
     | 
    
         | 
| 
       102 
     | 
    
         
            -
             
     | 
| 
      
 111 
     | 
    
         
            +
                  protected
         
     | 
| 
       103 
112 
     | 
    
         | 
| 
       104 
     | 
    
         
            -
             
     | 
| 
       105 
     | 
    
         
            -
             
     | 
| 
       106 
     | 
    
         
            -
             
     | 
| 
       107 
     | 
    
         
            -
             
     | 
| 
      
 113 
     | 
    
         
            +
                  # allow to overwrite for some special handlings
         
     | 
| 
      
 114 
     | 
    
         
            +
                  def ignore_password_expire?
         
     | 
| 
      
 115 
     | 
    
         
            +
                    false
         
     | 
| 
      
 116 
     | 
    
         
            +
                  end
         
     | 
| 
       108 
117 
     | 
    
         
             
                end
         
     | 
| 
       109 
118 
     | 
    
         
             
              end
         
     | 
| 
       110 
119 
     | 
    
         
             
            end
         
     | 
| 
         @@ -1,5 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
      
 3 
     | 
    
         
            +
            # @note This happens after
         
     | 
| 
      
 4 
     | 
    
         
            +
            #   {DeviseSecurity::Controller::Helpers#handle_password_change}
         
     | 
| 
       3 
5 
     | 
    
         
             
            Warden::Manager.after_authentication do |record, warden, options|
         
     | 
| 
       4 
6 
     | 
    
         
             
              if record.respond_to?(:need_change_password?)
         
     | 
| 
       5 
7 
     | 
    
         
             
                warden.session(options[:scope])['password_expired'] = record.need_change_password?
         
     | 
| 
         @@ -4,10 +4,17 @@ 
     | 
|
| 
       4 
4 
     | 
    
         
             
            # user is explicitly set (with set_user) and on authentication. Retrieving the
         
     | 
| 
       5 
5 
     | 
    
         
             
            # user from session (:fetch) does not trigger it.
         
     | 
| 
       6 
6 
     | 
    
         
             
            Warden::Manager.after_set_user except: :fetch do |record, warden, options|
         
     | 
| 
       7 
     | 
    
         
            -
              if record. 
     | 
| 
       8 
     | 
    
         
            -
             
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
             
     | 
| 
      
 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
         
     | 
| 
       11 
18 
     | 
    
         
             
              end
         
     | 
| 
       12 
19 
     | 
    
         
             
            end
         
     | 
| 
       13 
20 
     | 
    
         | 
| 
         @@ -17,15 +24,18 @@ end 
     | 
|
| 
       17 
24 
     | 
    
         
             
            # page on the next request.
         
     | 
| 
       18 
25 
     | 
    
         
             
            Warden::Manager.after_set_user only: :fetch do |record, warden, options|
         
     | 
| 
       19 
26 
     | 
    
         
             
              scope = options[:scope]
         
     | 
| 
       20 
     | 
    
         
            -
              env   = warden.request.env
         
     | 
| 
       21 
27 
     | 
    
         | 
| 
       22 
     | 
    
         
            -
              if record. 
     | 
| 
       23 
     | 
    
         
            -
             
     | 
| 
       24 
     | 
    
         
            -
             
     | 
| 
       25 
     | 
    
         
            -
             
     | 
| 
      
 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: '\
         
     | 
| 
       26 
36 
     | 
    
         
             
                    "expected=#{record.unique_session_id.inspect} "\
         
     | 
| 
       27 
     | 
    
         
            -
                    "actual=#{warden.session(scope)['unique_session_id'].inspect}" 
     | 
| 
       28 
     | 
    
         
            -
                   
     | 
| 
      
 37 
     | 
    
         
            +
                    "actual=#{warden.session(scope)['unique_session_id'].inspect}"
         
     | 
| 
      
 38 
     | 
    
         
            +
                  end
         
     | 
| 
       29 
39 
     | 
    
         
             
                  warden.raw_session.clear
         
     | 
| 
       30 
40 
     | 
    
         
             
                  warden.logout(scope)
         
     | 
| 
       31 
41 
     | 
    
         
             
                  throw :warden, scope: scope, message: :session_limited
         
     | 
| 
         @@ -5,18 +5,28 @@ module Devise 
     | 
|
| 
       5 
5 
     | 
    
         
             
                module DatabaseAuthenticatablePatch
         
     | 
| 
       6 
6 
     | 
    
         
             
                  def update_with_password(params, *options)
         
     | 
| 
       7 
7 
     | 
    
         
             
                    current_password = params.delete(:current_password)
         
     | 
| 
      
 8 
     | 
    
         
            +
                    valid_password = valid_password?(current_password)
         
     | 
| 
       8 
9 
     | 
    
         | 
| 
       9 
10 
     | 
    
         
             
                    new_password = params[:password]
         
     | 
| 
       10 
11 
     | 
    
         
             
                    new_password_confirmation = params[:password_confirmation]
         
     | 
| 
       11 
12 
     | 
    
         | 
| 
       12 
     | 
    
         
            -
                    result = if valid_password 
     | 
| 
      
 13 
     | 
    
         
            +
                    result = if valid_password && new_password.present? && new_password_confirmation.present?
         
     | 
| 
       13 
14 
     | 
    
         
             
                      update(params, *options)
         
     | 
| 
       14 
15 
     | 
    
         
             
                    else
         
     | 
| 
       15 
16 
     | 
    
         
             
                      self.assign_attributes(params, *options)
         
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
                       
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
       19 
     | 
    
         
            -
                       
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                      if current_password.blank?
         
     | 
| 
      
 19 
     | 
    
         
            +
                        self.errors.add(:current_password, :blank)
         
     | 
| 
      
 20 
     | 
    
         
            +
                      elsif !valid_password
         
     | 
| 
      
 21 
     | 
    
         
            +
                        self.errors.add(:current_password, :invalid)
         
     | 
| 
      
 22 
     | 
    
         
            +
                      end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                      self.errors.add(:password, :blank) if new_password.blank?
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                      if new_password_confirmation.blank?
         
     | 
| 
      
 27 
     | 
    
         
            +
                        self.errors.add(:password_confirmation, :blank)
         
     | 
| 
      
 28 
     | 
    
         
            +
                      end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
       20 
30 
     | 
    
         
             
                      false
         
     | 
| 
       21 
31 
     | 
    
         
             
                    end
         
     | 
| 
       22 
32 
     | 
    
         | 
| 
         @@ -41,7 +41,7 @@ module Devise 
     | 
|
| 
       41 
41 
     | 
    
         
             
                  def password_archive_included?
         
     | 
| 
       42 
42 
     | 
    
         
             
                    return false unless max_old_passwords.positive?
         
     | 
| 
       43 
43 
     | 
    
         | 
| 
       44 
     | 
    
         
            -
                    old_passwords_including_cur_change = old_passwords. 
     | 
| 
      
 44 
     | 
    
         
            +
                    old_passwords_including_cur_change = old_passwords.reorder(created_at: :desc).limit(max_old_passwords).pluck(:encrypted_password)
         
     | 
| 
       45 
45 
     | 
    
         
             
                    old_passwords_including_cur_change << encrypted_password_was # include most recent change in list, but don't save it yet!
         
     | 
| 
       46 
46 
     | 
    
         
             
                    old_passwords_including_cur_change.any? do |old_password|
         
     | 
| 
       47 
47 
     | 
    
         
             
                      # NOTE: we deliberately do not do mass assignment here so that users that
         
     | 
| 
         @@ -73,7 +73,7 @@ module Devise 
     | 
|
| 
       73 
73 
     | 
    
         
             
                      return true if old_passwords.where(encrypted_password: encrypted_password_was).exists?
         
     | 
| 
       74 
74 
     | 
    
         | 
| 
       75 
75 
     | 
    
         
             
                      old_passwords.create!(encrypted_password: encrypted_password_was) if encrypted_password_was.present?
         
     | 
| 
       76 
     | 
    
         
            -
                      old_passwords. 
     | 
| 
      
 76 
     | 
    
         
            +
                      old_passwords.reorder(created_at: :desc).offset(max_old_passwords).destroy_all
         
     | 
| 
       77 
77 
     | 
    
         
             
                    else
         
     | 
| 
       78 
78 
     | 
    
         
             
                      old_passwords.destroy_all
         
     | 
| 
       79 
79 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -92,7 +92,11 @@ module Devise::Models 
     | 
|
| 
       92 
92 
     | 
    
         
             
                # Update +password_changed_at+ for new records and changed passwords.
         
     | 
| 
       93 
93 
     | 
    
         
             
                # @note called as a +before_save+ hook
         
     | 
| 
       94 
94 
     | 
    
         
             
                def update_password_changed
         
     | 
| 
       95 
     | 
    
         
            -
                   
     | 
| 
      
 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?
         
     | 
| 
      
 99 
     | 
    
         
            +
                  end
         
     | 
| 
       96 
100 
     | 
    
         | 
| 
       97 
101 
     | 
    
         
             
                  self.password_changed_at = Time.zone.now
         
     | 
| 
       98 
102 
     | 
    
         
             
                end
         
     | 
| 
         @@ -44,17 +44,39 @@ module Devise 
     | 
|
| 
       44 
44 
     | 
    
         
             
                          validates :email, uniqueness: true, allow_blank: true, if: :email_changed? # check uniq for email ever
         
     | 
| 
       45 
45 
     | 
    
         
             
                        end
         
     | 
| 
       46 
46 
     | 
    
         | 
| 
       47 
     | 
    
         
            -
                         
     | 
| 
      
 47 
     | 
    
         
            +
                        validates_presence_of :password, if: :password_required?
         
     | 
| 
      
 48 
     | 
    
         
            +
                        validates_confirmation_of :password, if: :password_required?
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                        validate if: :password_required? do |record|
         
     | 
| 
      
 51 
     | 
    
         
            +
                          validates_with ActiveModel::Validations::LengthValidator,
         
     | 
| 
      
 52 
     | 
    
         
            +
                                         attributes: :password,
         
     | 
| 
      
 53 
     | 
    
         
            +
                                         allow_blank: true,
         
     | 
| 
      
 54 
     | 
    
         
            +
                                         in: record.password_length
         
     | 
| 
      
 55 
     | 
    
         
            +
                        end
         
     | 
| 
       48 
56 
     | 
    
         
             
                      end
         
     | 
| 
       49 
57 
     | 
    
         | 
| 
       50 
58 
     | 
    
         
             
                      # extra validations
         
     | 
| 
       51 
     | 
    
         
            -
                       
     | 
| 
       52 
     | 
    
         
            -
                       
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
      
 59 
     | 
    
         
            +
                      # see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
         
     | 
| 
      
 60 
     | 
    
         
            +
                      validate do |record|
         
     | 
| 
      
 61 
     | 
    
         
            +
                        if email_validation
         
     | 
| 
      
 62 
     | 
    
         
            +
                          validates_with(
         
     | 
| 
      
 63 
     | 
    
         
            +
                            EmailValidator, { attributes: :email }
         
     | 
| 
      
 64 
     | 
    
         
            +
                          )
         
     | 
| 
      
 65 
     | 
    
         
            +
                        end
         
     | 
| 
      
 66 
     | 
    
         
            +
                      end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                      validate if: :password_required? do |record|
         
     | 
| 
      
 69 
     | 
    
         
            +
                        validates_with(
         
     | 
| 
      
 70 
     | 
    
         
            +
                          record.password_complexity_validator.is_a?(Class) ? record.password_complexity_validator : record.password_complexity_validator.classify.constantize,
         
     | 
| 
      
 71 
     | 
    
         
            +
                          { attributes: :password }.merge(record.password_complexity)
         
     | 
| 
      
 72 
     | 
    
         
            +
                        )
         
     | 
| 
      
 73 
     | 
    
         
            +
                      end
         
     | 
| 
       55 
74 
     | 
    
         | 
| 
       56 
75 
     | 
    
         
             
                      # don't allow use same password
         
     | 
| 
       57 
76 
     | 
    
         
             
                      validate :current_equal_password_validation
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                      # don't allow email to equal password
         
     | 
| 
      
 79 
     | 
    
         
            +
                      validate :email_not_equal_password_validation
         
     | 
| 
       58 
80 
     | 
    
         
             
                    end
         
     | 
| 
       59 
81 
     | 
    
         
             
                  end
         
     | 
| 
       60 
82 
     | 
    
         | 
| 
         @@ -70,11 +92,23 @@ module Devise 
     | 
|
| 
       70 
92 
     | 
    
         
             
                    self.errors.add(:password, :equal_to_current_password) if dummy.valid_password?(password)
         
     | 
| 
       71 
93 
     | 
    
         
             
                  end
         
     | 
| 
       72 
94 
     | 
    
         | 
| 
      
 95 
     | 
    
         
            +
                  def email_not_equal_password_validation
         
     | 
| 
      
 96 
     | 
    
         
            +
                    return if allow_passwords_equal_to_email
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                    return if password.blank? || email.blank? || (!new_record? && !will_save_change_to_encrypted_password?)
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                    return unless Devise.secure_compare(password.downcase.strip, email.downcase.strip)
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    errors.add(:password, :equal_to_email)
         
     | 
| 
      
 103 
     | 
    
         
            +
                  end
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
       73 
105 
     | 
    
         
             
                  protected
         
     | 
| 
       74 
106 
     | 
    
         | 
| 
       75 
107 
     | 
    
         
             
                  # Checks whether a password is needed or not. For validations only.
         
     | 
| 
       76 
108 
     | 
    
         
             
                  # Passwords are always required if it's a new record, or if the password
         
     | 
| 
       77 
109 
     | 
    
         
             
                  # or confirmation are being set somewhere.
         
     | 
| 
      
 110 
     | 
    
         
            +
                  #
         
     | 
| 
      
 111 
     | 
    
         
            +
                  # @return [Boolean]
         
     | 
| 
       78 
112 
     | 
    
         
             
                  def password_required?
         
     | 
| 
       79 
113 
     | 
    
         
             
                    !persisted? || !password.nil? || !password_confirmation.nil?
         
     | 
| 
       80 
114 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -83,8 +117,24 @@ module Devise 
     | 
|
| 
       83 
117 
     | 
    
         
             
                    true
         
     | 
| 
       84 
118 
     | 
    
         
             
                  end
         
     | 
| 
       85 
119 
     | 
    
         | 
| 
      
 120 
     | 
    
         
            +
                  delegate(
         
     | 
| 
      
 121 
     | 
    
         
            +
                    :allow_passwords_equal_to_email,
         
     | 
| 
      
 122 
     | 
    
         
            +
                    :email_validation,
         
     | 
| 
      
 123 
     | 
    
         
            +
                    :password_complexity,
         
     | 
| 
      
 124 
     | 
    
         
            +
                    :password_complexity_validator,
         
     | 
| 
      
 125 
     | 
    
         
            +
                    :password_length,
         
     | 
| 
      
 126 
     | 
    
         
            +
                    to: :class
         
     | 
| 
      
 127 
     | 
    
         
            +
                  )
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
       86 
129 
     | 
    
         
             
                  module ClassMethods
         
     | 
| 
       87 
     | 
    
         
            -
                    Devise::Models.config( 
     | 
| 
      
 130 
     | 
    
         
            +
                    Devise::Models.config(
         
     | 
| 
      
 131 
     | 
    
         
            +
                      self,
         
     | 
| 
      
 132 
     | 
    
         
            +
                      :allow_passwords_equal_to_email,
         
     | 
| 
      
 133 
     | 
    
         
            +
                      :email_validation,
         
     | 
| 
      
 134 
     | 
    
         
            +
                      :password_complexity,
         
     | 
| 
      
 135 
     | 
    
         
            +
                      :password_complexity_validator,
         
     | 
| 
      
 136 
     | 
    
         
            +
                      :password_length
         
     | 
| 
      
 137 
     | 
    
         
            +
                    )
         
     | 
| 
       88 
138 
     | 
    
         | 
| 
       89 
139 
     | 
    
         
             
                    private
         
     | 
| 
       90 
140 
     | 
    
         | 
| 
         @@ -1,5 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
      
 3 
     | 
    
         
            +
            require_relative 'compatibility'
         
     | 
| 
       3 
4 
     | 
    
         
             
            require 'devise-security/hooks/session_limitable'
         
     | 
| 
       4 
5 
     | 
    
         | 
| 
       5 
6 
     | 
    
         
             
            module Devise
         
     | 
| 
         @@ -11,6 +12,7 @@ module Devise 
     | 
|
| 
       11 
12 
     | 
    
         
             
                # someone used his credentials to sign in.
         
     | 
| 
       12 
13 
     | 
    
         
             
                module SessionLimitable
         
     | 
| 
       13 
14 
     | 
    
         
             
                  extend ActiveSupport::Concern
         
     | 
| 
      
 15 
     | 
    
         
            +
                  include Devise::Models::Compatibility
         
     | 
| 
       14 
16 
     | 
    
         | 
| 
       15 
17 
     | 
    
         
             
                  # Update the unique_session_id on the model.  This will be checked in
         
     | 
| 
       16 
18 
     | 
    
         
             
                  # the Warden after_set_user hook in {file:devise-security/hooks/session_limitable}
         
     | 
| 
         @@ -19,11 +21,18 @@ module Devise 
     | 
|
| 
       19 
21 
     | 
    
         
             
                  # @raise [Devise::Models::Compatibility::NotPersistedError] if record is unsaved
         
     | 
| 
       20 
22 
     | 
    
         
             
                  def update_unique_session_id!(unique_session_id)
         
     | 
| 
       21 
23 
     | 
    
         
             
                    raise Devise::Models::Compatibility::NotPersistedError, 'cannot update a new record' unless persisted?
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
       22 
25 
     | 
    
         
             
                    update_attribute_without_validatons_or_callbacks(:unique_session_id, unique_session_id).tap do
         
     | 
| 
       23 
     | 
    
         
            -
                      Rails.logger.debug { "[devise-security][session_limitable] unique_session_id=#{unique_session_id}"}
         
     | 
| 
      
 26 
     | 
    
         
            +
                      Rails.logger.debug { "[devise-security][session_limitable] unique_session_id=#{unique_session_id}" }
         
     | 
| 
       24 
27 
     | 
    
         
             
                    end
         
     | 
| 
       25 
28 
     | 
    
         
             
                  end
         
     | 
| 
       26 
29 
     | 
    
         | 
| 
      
 30 
     | 
    
         
            +
                  # Should session_limitable be skipped for this instance?
         
     | 
| 
      
 31 
     | 
    
         
            +
                  # @return [Boolean]
         
     | 
| 
      
 32 
     | 
    
         
            +
                  # @return [false] by default. This can be overridden by application logic as necessary.
         
     | 
| 
      
 33 
     | 
    
         
            +
                  def skip_session_limitable?
         
     | 
| 
      
 34 
     | 
    
         
            +
                    false
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
       27 
36 
     | 
    
         
             
                end
         
     | 
| 
       28 
37 
     | 
    
         
             
              end
         
     | 
| 
       29 
38 
     | 
    
         
             
            end
         
     | 
| 
         @@ -1,33 +1,62 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            #  
     | 
| 
      
 3 
     | 
    
         
            +
            # (NIST)[https://pages.nist.gov/800-63-3/sp800-63b.html#appA] does not recommend
         
     | 
| 
      
 4 
     | 
    
         
            +
            # the use of a password complexity checks because...
         
     | 
| 
      
 5 
     | 
    
         
            +
            #
         
     | 
| 
      
 6 
     | 
    
         
            +
            # > Length and complexity requirements beyond those recommended here
         
     | 
| 
      
 7 
     | 
    
         
            +
            # > significantly increase the difficulty of memorized secrets and increase user
         
     | 
| 
      
 8 
     | 
    
         
            +
            # > frustration. As a result, users often work around these restrictions in a
         
     | 
| 
      
 9 
     | 
    
         
            +
            # > way that is counterproductive. Furthermore, other mitigations such as
         
     | 
| 
      
 10 
     | 
    
         
            +
            # > blacklists, secure hashed storage, and rate limiting are more effective at
         
     | 
| 
      
 11 
     | 
    
         
            +
            # > preventing modern brute-force attacks. Therefore, no additional complexity
         
     | 
| 
      
 12 
     | 
    
         
            +
            # > requirements are imposed.
         
     | 
| 
      
 13 
     | 
    
         
            +
            #
         
     | 
| 
       4 
14 
     | 
    
         
             
            # Options:
         
     | 
| 
       5 
     | 
    
         
            -
            # - digit 
     | 
| 
       6 
     | 
    
         
            -
            #  
     | 
| 
       7 
     | 
    
         
            -
            # -  
     | 
| 
       8 
     | 
    
         
            -
            # -  
     | 
| 
      
 15 
     | 
    
         
            +
            # - `digit | digits`:  minimum number of digits in the validated string. Uses
         
     | 
| 
      
 16 
     | 
    
         
            +
            #   the `digit` localization key.
         
     | 
| 
      
 17 
     | 
    
         
            +
            # - `lower`:  minimum number of lower-case letters in the validated string
         
     | 
| 
      
 18 
     | 
    
         
            +
            # - `symbol | symbols`: minimum number of punctuation characters or symbols in
         
     | 
| 
      
 19 
     | 
    
         
            +
            #   the validated string. Uses the `symbol` localization key.
         
     | 
| 
      
 20 
     | 
    
         
            +
            # - `upper`:  minimum number of upper-case letters in the validated string
         
     | 
| 
       9 
21 
     | 
    
         
             
            class DeviseSecurity::PasswordComplexityValidator < ActiveModel::EachValidator
         
     | 
| 
       10 
     | 
    
         
            -
               
     | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
      
 22 
     | 
    
         
            +
              # A Hash of the possible valid patterns that can be checked against. The keys
         
     | 
| 
      
 23 
     | 
    
         
            +
              # for this Hash are singular symbols corresponding to entries in the
         
     | 
| 
      
 24 
     | 
    
         
            +
              # localization files. Override or redefine this method if you want to include
         
     | 
| 
      
 25 
     | 
    
         
            +
              # custom patterns (e.g., `letter: /\p{Alpha}/` for all letters).
         
     | 
| 
      
 26 
     | 
    
         
            +
              #
         
     | 
| 
      
 27 
     | 
    
         
            +
              # @return [Hash<Symbol,Regexp>]
         
     | 
| 
      
 28 
     | 
    
         
            +
              def patterns
         
     | 
| 
      
 29 
     | 
    
         
            +
                {
         
     | 
| 
      
 30 
     | 
    
         
            +
                  digit: /\p{Digit}/,
         
     | 
| 
      
 31 
     | 
    
         
            +
                  lower: /\p{Lower}/,
         
     | 
| 
      
 32 
     | 
    
         
            +
                  symbol: /\p{Punct}|\p{S}/,
         
     | 
| 
      
 33 
     | 
    
         
            +
                  upper: /\p{Upper}/
         
     | 
| 
      
 34 
     | 
    
         
            +
                }
         
     | 
| 
      
 35 
     | 
    
         
            +
              end
         
     | 
| 
       18 
36 
     | 
    
         | 
| 
       19 
     | 
    
         
            -
               
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
      
 37 
     | 
    
         
            +
              # Validate the complexity of the password. This validation does not check to
         
     | 
| 
      
 38 
     | 
    
         
            +
              # ensure the password is not blank. That is the responsibility of other
         
     | 
| 
      
 39 
     | 
    
         
            +
              # validations. This validator will also ignore any patterns that are not
         
     | 
| 
      
 40 
     | 
    
         
            +
              # explicitly configured to be used or whose minimum limits are less than 1.
         
     | 
| 
      
 41 
     | 
    
         
            +
              #
         
     | 
| 
      
 42 
     | 
    
         
            +
              # @param record [ActiveModel::Model]
         
     | 
| 
      
 43 
     | 
    
         
            +
              # @param attribute [Symbol]
         
     | 
| 
      
 44 
     | 
    
         
            +
              # @param password [String]
         
     | 
| 
      
 45 
     | 
    
         
            +
              def validate_each(record, attribute, password)
         
     | 
| 
      
 46 
     | 
    
         
            +
                return if password.blank?
         
     | 
| 
       23 
47 
     | 
    
         | 
| 
       24 
     | 
    
         
            -
             
     | 
| 
       25 
     | 
    
         
            -
             
     | 
| 
       26 
     | 
    
         
            -
                  end
         
     | 
| 
       27 
     | 
    
         
            -
                end
         
     | 
| 
       28 
     | 
    
         
            -
              end
         
     | 
| 
      
 48 
     | 
    
         
            +
                options.sort.each do |pattern_name, minimum|
         
     | 
| 
      
 49 
     | 
    
         
            +
                  normalized_option = pattern_name.to_s.singularize.to_sym
         
     | 
| 
       29 
50 
     | 
    
         | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
       31 
     | 
    
         
            -
             
     | 
| 
      
 51 
     | 
    
         
            +
                  next unless patterns.key?(normalized_option)
         
     | 
| 
      
 52 
     | 
    
         
            +
                  next unless minimum.positive?
         
     | 
| 
      
 53 
     | 
    
         
            +
                  next if password.scan(patterns[normalized_option]).size >= minimum
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                  record.errors.add(
         
     | 
| 
      
 56 
     | 
    
         
            +
                    attribute,
         
     | 
| 
      
 57 
     | 
    
         
            +
                    :"password_complexity.#{normalized_option}",
         
     | 
| 
      
 58 
     | 
    
         
            +
                    count: minimum
         
     | 
| 
      
 59 
     | 
    
         
            +
                  )
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
       32 
61 
     | 
    
         
             
              end
         
     | 
| 
       33 
62 
     | 
    
         
             
            end
         
     | 
    
        data/lib/devise-security.rb
    CHANGED
    
    | 
         @@ -9,15 +9,20 @@ require 'devise' 
     | 
|
| 
       9 
9 
     | 
    
         | 
| 
       10 
10 
     | 
    
         
             
            module Devise
         
     | 
| 
       11 
11 
     | 
    
         
             
              # Number of seconds that passwords are valid (e.g 3.months)
         
     | 
| 
       12 
     | 
    
         
            -
              # Disable  
     | 
| 
      
 12 
     | 
    
         
            +
              # Disable password expiration with +false+
         
     | 
| 
       13 
13 
     | 
    
         
             
              # Expire only on demand with +true+
         
     | 
| 
       14 
14 
     | 
    
         
             
              mattr_accessor :expire_password_after
         
     | 
| 
       15 
15 
     | 
    
         
             
              @@expire_password_after = 3.months
         
     | 
| 
       16 
16 
     | 
    
         | 
| 
       17 
     | 
    
         
            -
              # Validate password  
     | 
| 
      
 17 
     | 
    
         
            +
              # Validate password complexity
         
     | 
| 
       18 
18 
     | 
    
         
             
              mattr_accessor :password_complexity
         
     | 
| 
       19 
19 
     | 
    
         
             
              @@password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
         
     | 
| 
       20 
20 
     | 
    
         | 
| 
      
 21 
     | 
    
         
            +
              # Define the class used to validate password complexity. Set to a Class or a
         
     | 
| 
      
 22 
     | 
    
         
            +
              # string which will be used to determine which class to use.
         
     | 
| 
      
 23 
     | 
    
         
            +
              mattr_accessor :password_complexity_validator
         
     | 
| 
      
 24 
     | 
    
         
            +
              @@password_complexity_validator = 'devise_security/password_complexity_validator'
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
       21 
26 
     | 
    
         
             
              # Number of old passwords in archive
         
     | 
| 
       22 
27 
     | 
    
         
             
              mattr_accessor :password_archiving_count
         
     | 
| 
       23 
28 
     | 
    
         
             
              @@password_archiving_count = 5
         
     | 
| 
         @@ -79,11 +84,14 @@ module Devise 
     | 
|
| 
       79 
84 
     | 
    
         
             
              # paranoid_verification will regenerate verifacation code after faild attempt
         
     | 
| 
       80 
85 
     | 
    
         
             
              mattr_accessor :paranoid_code_regenerate_after_attempt
         
     | 
| 
       81 
86 
     | 
    
         
             
              @@paranoid_code_regenerate_after_attempt = 10
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
              # Whether to allow passwords that are equal (case insensitive) to the email
         
     | 
| 
      
 89 
     | 
    
         
            +
              mattr_accessor :allow_passwords_equal_to_email
         
     | 
| 
      
 90 
     | 
    
         
            +
              @@allow_passwords_equal_to_email = false
         
     | 
| 
       82 
91 
     | 
    
         
             
            end
         
     | 
| 
       83 
92 
     | 
    
         | 
| 
       84 
     | 
    
         
            -
            #  
     | 
| 
      
 93 
     | 
    
         
            +
            # a security extension for devise
         
     | 
| 
       85 
94 
     | 
    
         
             
            module DeviseSecurity
         
     | 
| 
       86 
     | 
    
         
            -
              autoload :Schema, 'devise-security/schema'
         
     | 
| 
       87 
95 
     | 
    
         
             
              autoload :Patches, 'devise-security/patches'
         
     | 
| 
       88 
96 
     | 
    
         | 
| 
       89 
97 
     | 
    
         
             
              module Controllers
         
     | 
| 
         @@ -104,6 +112,6 @@ Devise.add_module :paranoid_verification, controller: :paranoid_verification_cod 
     | 
|
| 
       104 
112 
     | 
    
         
             
            # requires
         
     | 
| 
       105 
113 
     | 
    
         
             
            require 'devise-security/routes'
         
     | 
| 
       106 
114 
     | 
    
         
             
            require 'devise-security/rails'
         
     | 
| 
       107 
     | 
    
         
            -
            require "devise-security/orm/#{DEVISE_ORM}"
         
     | 
| 
      
 115 
     | 
    
         
            +
            require "devise-security/orm/#{DEVISE_ORM}" if DEVISE_ORM == :mongoid
         
     | 
| 
       108 
116 
     | 
    
         
             
            require 'devise-security/models/database_authenticatable_patch'
         
     | 
| 
       109 
117 
     | 
    
         
             
            require 'devise-security/models/paranoid_verification'
         
     | 
| 
         @@ -4,14 +4,14 @@ 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  
     | 
| 
      
 7 
     | 
    
         
            +
                  LOCALES = %w[by cs de en es fa fr hi it ja nl pt ru tr uk zh_CN zh_TW].freeze
         
     | 
| 
       8 
8 
     | 
    
         | 
| 
       9 
9 
     | 
    
         
             
                  source_root File.expand_path('../../templates', __FILE__)
         
     | 
| 
       10 
10 
     | 
    
         
             
                  desc 'Install the devise security extension'
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
       12 
12 
     | 
    
         
             
                  def copy_initializer
         
     | 
| 
       13 
     | 
    
         
            -
                    template(' 
     | 
| 
       14 
     | 
    
         
            -
                             'config/initializers/ 
     | 
| 
      
 13 
     | 
    
         
            +
                    template('devise_security.rb',
         
     | 
| 
      
 14 
     | 
    
         
            +
                             'config/initializers/devise_security.rb',
         
     | 
| 
       15 
15 
     | 
    
         
             
                    )
         
     | 
| 
       16 
16 
     | 
    
         
             
                  end
         
     | 
| 
       17 
17 
     | 
    
         | 
| 
         @@ -7,7 +7,9 @@ Devise.setup do |config| 
     | 
|
| 
       7 
7 
     | 
    
         
             
              # Should the password expire (e.g 3.months)
         
     | 
| 
       8 
8 
     | 
    
         
             
              # config.expire_password_after = false
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
       10 
     | 
    
         
            -
              # Need 1 char of A-Z, a-z  
     | 
| 
      
 10 
     | 
    
         
            +
              # Need 1 char each of: A-Z, a-z, 0-9, and a punctuation mark or symbol
         
     | 
| 
      
 11 
     | 
    
         
            +
              # You may use "digits" in place of "digit" and "symbols" in place of
         
     | 
| 
      
 12 
     | 
    
         
            +
              # "symbol" based on your preference
         
     | 
| 
       11 
13 
     | 
    
         
             
              # config.password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
         
     | 
| 
       12 
14 
     | 
    
         | 
| 
       13 
15 
     | 
    
         
             
              # How many passwords to keep in archive
         
     | 
| 
         @@ -41,4 +43,7 @@ Devise.setup do |config| 
     | 
|
| 
       41 
43 
     | 
    
         | 
| 
       42 
44 
     | 
    
         
             
              # Time period for account expiry from last_activity_at
         
     | 
| 
       43 
45 
     | 
    
         
             
              # config.expire_after = 90.days
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
              # Allow password to equal the email
         
     | 
| 
      
 48 
     | 
    
         
            +
              # config.allow_passwords_equal_to_email = false
         
     | 
| 
       44 
49 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,68 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'test_helper'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            class Devise::ParanoidVerificationCodeControllerTest < ActionController::TestCase
         
     | 
| 
      
 6 
     | 
    
         
            +
              include Devise::Test::ControllerHelpers
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
              setup do
         
     | 
| 
      
 9 
     | 
    
         
            +
                @request.env['devise.mapping'] = Devise.mappings[:user]
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                @user = User.create!(
         
     | 
| 
      
 12 
     | 
    
         
            +
                  username: 'hello',
         
     | 
| 
      
 13 
     | 
    
         
            +
                  email: 'hello@path.travel',
         
     | 
| 
      
 14 
     | 
    
         
            +
                  password: 'Password4',
         
     | 
| 
      
 15 
     | 
    
         
            +
                  confirmed_at: 5.months.ago,
         
     | 
| 
      
 16 
     | 
    
         
            +
                )
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                sign_in(@user)
         
     | 
| 
      
 19 
     | 
    
         
            +
              end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              test 'redirects to root on show if user not logged in' do
         
     | 
| 
      
 22 
     | 
    
         
            +
                sign_out(@user)
         
     | 
| 
      
 23 
     | 
    
         
            +
                get :show
         
     | 
| 
      
 24 
     | 
    
         
            +
                assert_redirected_to :root
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
              test "redirects to root on show if user doesn't need paranoid verification" do
         
     | 
| 
      
 28 
     | 
    
         
            +
                get :show
         
     | 
| 
      
 29 
     | 
    
         
            +
                assert_redirected_to :root
         
     | 
| 
      
 30 
     | 
    
         
            +
              end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
              test 'renders show on show if user needs paranoid verification' do
         
     | 
| 
      
 33 
     | 
    
         
            +
                @user.update(paranoid_verification_code: 'cookies')
         
     | 
| 
      
 34 
     | 
    
         
            +
                get :show
         
     | 
| 
      
 35 
     | 
    
         
            +
                assert_template :show
         
     | 
| 
      
 36 
     | 
    
         
            +
              end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
              test "redirects to root on update" do
         
     | 
| 
      
 39 
     | 
    
         
            +
                patch :update, params: { user: { paranoid_verification_code: 'cookies' } }
         
     | 
| 
      
 40 
     | 
    
         
            +
                assert_redirected_to :root
         
     | 
| 
      
 41 
     | 
    
         
            +
                assert_equal 'Verification code accepted', flash[:notice]
         
     | 
| 
      
 42 
     | 
    
         
            +
              end
         
     | 
| 
      
 43 
     | 
    
         
            +
            end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
            class ParanoidVerificationCodeCustomRedirectTest < ActionController::TestCase
         
     | 
| 
      
 46 
     | 
    
         
            +
              include Devise::Test::ControllerHelpers
         
     | 
| 
      
 47 
     | 
    
         
            +
              tests Overrides::ParanoidVerificationCodeController
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
              setup do
         
     | 
| 
      
 50 
     | 
    
         
            +
                @request.env['devise.mapping'] = Devise.mappings[:paranoid_verification_user]
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                @user = ParanoidVerificationUser.create!(
         
     | 
| 
      
 53 
     | 
    
         
            +
                  username: 'hello',
         
     | 
| 
      
 54 
     | 
    
         
            +
                  email: 'hello@path.travel',
         
     | 
| 
      
 55 
     | 
    
         
            +
                  password: 'Password4',
         
     | 
| 
      
 56 
     | 
    
         
            +
                  confirmed_at: 5.months.ago,
         
     | 
| 
      
 57 
     | 
    
         
            +
                )
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                sign_in(@user)
         
     | 
| 
      
 60 
     | 
    
         
            +
              end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
              test 'redirects to custom redirect route on update' do
         
     | 
| 
      
 63 
     | 
    
         
            +
                patch :update, params: { paranoid_verification_user: { paranoid_verification_code: 'cookies' } }
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                assert_redirected_to '/cats'
         
     | 
| 
      
 66 
     | 
    
         
            +
                assert_equal 'Verification code accepted', flash[:notice]
         
     | 
| 
      
 67 
     | 
    
         
            +
              end
         
     | 
| 
      
 68 
     | 
    
         
            +
            end
         
     |