devise-security 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -60
  3. data/app/controllers/devise/password_expired_controller.rb +11 -6
  4. data/app/views/devise/paranoid_verification_code/show.html.erb +3 -3
  5. data/app/views/devise/password_expired/show.html.erb +5 -5
  6. data/config/locales/by.yml +49 -0
  7. data/config/locales/cs.yml +41 -0
  8. data/config/locales/de.yml +13 -2
  9. data/config/locales/en.yml +13 -1
  10. data/config/locales/es.yml +10 -9
  11. data/config/locales/fa.yml +41 -0
  12. data/config/locales/fr.yml +1 -0
  13. data/config/locales/hi.yml +42 -0
  14. data/config/locales/it.yml +35 -4
  15. data/config/locales/ja.yml +2 -1
  16. data/config/locales/nl.yml +41 -0
  17. data/config/locales/pt.yml +41 -0
  18. data/config/locales/ru.yml +49 -0
  19. data/config/locales/tr.yml +1 -0
  20. data/config/locales/uk.yml +49 -0
  21. data/config/locales/zh_CN.yml +41 -0
  22. data/config/locales/zh_TW.yml +41 -0
  23. data/lib/devise-security.rb +7 -3
  24. data/lib/devise-security/controllers/helpers.rb +59 -50
  25. data/lib/devise-security/hooks/password_expirable.rb +2 -0
  26. data/lib/devise-security/hooks/session_limitable.rb +29 -14
  27. data/lib/devise-security/models/compatibility.rb +2 -2
  28. data/lib/devise-security/models/compatibility/{active_record.rb → active_record_patch.rb} +12 -1
  29. data/lib/devise-security/models/compatibility/{mongoid.rb → mongoid_patch.rb} +11 -1
  30. data/lib/devise-security/models/password_expirable.rb +5 -1
  31. data/lib/devise-security/models/secure_validatable.rb +15 -1
  32. data/lib/devise-security/models/session_limitable.rb +17 -2
  33. data/lib/devise-security/validators/password_complexity_validator.rb +4 -2
  34. data/lib/devise-security/version.rb +1 -1
  35. data/lib/generators/devise_security/install_generator.rb +3 -3
  36. data/lib/generators/templates/devise_security.rb +47 -0
  37. data/test/{test_captcha_controller.rb → controllers/test_captcha_controller.rb} +0 -0
  38. data/test/controllers/test_password_expired_controller.rb +110 -0
  39. data/test/{test_security_question_controller.rb → controllers/test_security_question_controller.rb} +16 -40
  40. data/test/dummy/app/assets/config/manifest.js +3 -0
  41. data/test/dummy/app/controllers/widgets_controller.rb +6 -0
  42. data/test/dummy/app/models/user.rb +8 -0
  43. data/test/dummy/config/application.rb +1 -0
  44. data/test/dummy/config/environments/test.rb +3 -13
  45. data/test/dummy/config/initializers/migration_class.rb +1 -8
  46. data/test/dummy/config/mongoid.yml +1 -1
  47. data/test/dummy/config/routes.rb +4 -3
  48. data/test/dummy/db/migrate/20120508165529_create_tables.rb +10 -1
  49. data/test/dummy/log/development.log +883 -0
  50. data/test/dummy/log/test.log +21689 -0
  51. data/test/integration/test_password_expirable_workflow.rb +53 -0
  52. data/test/integration/test_session_limitable_workflow.rb +67 -0
  53. data/test/orm/active_record.rb +4 -1
  54. data/test/orm/mongoid.rb +2 -1
  55. data/test/support/integration_helpers.rb +29 -0
  56. data/test/support/mongoid.yml +1 -1
  57. data/test/test_compatibility.rb +13 -0
  58. data/test/test_complexity_validator.rb +12 -0
  59. data/test/test_helper.rb +21 -6
  60. data/test/test_install_generator.rb +11 -1
  61. data/test/test_secure_validatable.rb +76 -0
  62. data/test/test_session_limitable.rb +57 -0
  63. data/{lib/generators/templates → test/tmp/config/initializers}/devise-security.rb +3 -0
  64. data/test/tmp/config/locales/devise.security_extension.by.yml +49 -0
  65. data/test/tmp/config/locales/devise.security_extension.cs.yml +41 -0
  66. data/test/tmp/config/locales/devise.security_extension.de.yml +39 -0
  67. data/test/tmp/config/locales/devise.security_extension.en.yml +41 -0
  68. data/test/tmp/config/locales/devise.security_extension.es.yml +30 -0
  69. data/test/tmp/config/locales/devise.security_extension.fa.yml +41 -0
  70. data/test/tmp/config/locales/devise.security_extension.fr.yml +30 -0
  71. data/test/tmp/config/locales/devise.security_extension.hi.yml +42 -0
  72. data/test/tmp/config/locales/devise.security_extension.it.yml +41 -0
  73. data/test/tmp/config/locales/devise.security_extension.ja.yml +30 -0
  74. data/test/tmp/config/locales/devise.security_extension.nl.yml +41 -0
  75. data/test/tmp/config/locales/devise.security_extension.pt.yml +41 -0
  76. data/test/tmp/config/locales/devise.security_extension.ru.yml +49 -0
  77. data/test/tmp/config/locales/devise.security_extension.tr.yml +18 -0
  78. data/test/tmp/config/locales/devise.security_extension.uk.yml +49 -0
  79. data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +41 -0
  80. data/test/tmp/config/locales/devise.security_extension.zh_TW.yml +41 -0
  81. metadata +156 -133
  82. data/.codeclimate.yml +0 -63
  83. data/.document +0 -5
  84. data/.gitignore +0 -43
  85. data/.mdlrc +0 -1
  86. data/.rubocop.yml +0 -64
  87. data/.ruby-version +0 -1
  88. data/.travis.yml +0 -41
  89. data/Appraisals +0 -35
  90. data/Gemfile +0 -10
  91. data/Rakefile +0 -27
  92. data/devise-security.gemspec +0 -50
  93. data/gemfiles/rails_4.2_stable.gemfile +0 -16
  94. data/gemfiles/rails_5.0_stable.gemfile +0 -15
  95. data/gemfiles/rails_5.1_stable.gemfile +0 -15
  96. data/gemfiles/rails_5.2_stable.gemfile +0 -15
  97. data/gemfiles/rails_6.0_beta.gemfile +0 -15
  98. data/lib/devise-security/orm/active_record.rb +0 -20
  99. data/lib/devise-security/schema.rb +0 -66
  100. data/test/dummy/app/models/.gitkeep +0 -0
  101. data/test/test_password_expired_controller.rb +0 -46
@@ -55,6 +55,9 @@ module Devise
55
55
 
56
56
  # don't allow use same password
57
57
  validate :current_equal_password_validation
58
+
59
+ # don't allow email to equal password
60
+ validate :email_not_equal_password_validation unless allow_passwords_equal_to_email
58
61
  end
59
62
  end
60
63
 
@@ -70,6 +73,17 @@ module Devise
70
73
  self.errors.add(:password, :equal_to_current_password) if dummy.valid_password?(password)
71
74
  end
72
75
 
76
+ def email_not_equal_password_validation
77
+ return if password.blank? || (!new_record? && !will_save_change_to_encrypted_password?)
78
+ dummy = self.class.new.tap do |user|
79
+ user.password_salt = password_salt if respond_to?(:password_salt)
80
+ # whether case_insensitive_keys or strip_whitespace_keys include email or not, any
81
+ # variation of the email should not be a supported password
82
+ user.password = email.downcase.strip
83
+ end
84
+ self.errors.add(:password, :equal_to_email) if dummy.valid_password?(password.downcase.strip)
85
+ end
86
+
73
87
  protected
74
88
 
75
89
  # Checks whether a password is needed or not. For validations only.
@@ -84,7 +98,7 @@ module Devise
84
98
  end
85
99
 
86
100
  module ClassMethods
87
- Devise::Models.config(self, :password_complexity, :password_length, :email_validation)
101
+ Devise::Models.config(self, :password_complexity, :password_length, :email_validation, :allow_passwords_equal_to_email)
88
102
 
89
103
  private
90
104
 
@@ -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,13 +12,27 @@ 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
 
17
+ # Update the unique_session_id on the model. This will be checked in
18
+ # the Warden after_set_user hook in {file:devise-security/hooks/session_limitable}
19
+ # @param unique_session_id [String]
20
+ # @return [void]
21
+ # @raise [Devise::Models::Compatibility::NotPersistedError] if record is unsaved
15
22
  def update_unique_session_id!(unique_session_id)
16
- self.unique_session_id = unique_session_id
23
+ raise Devise::Models::Compatibility::NotPersistedError, 'cannot update a new record' unless persisted?
17
24
 
18
- save(validate: false)
25
+ update_attribute_without_validatons_or_callbacks(:unique_session_id, unique_session_id).tap do
26
+ Rails.logger.debug { "[devise-security][session_limitable] unique_session_id=#{unique_session_id}" }
27
+ end
19
28
  end
20
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
21
36
  end
22
37
  end
23
38
  end
@@ -3,17 +3,19 @@
3
3
  # Password complexity validator
4
4
  # Options:
5
5
  # - digit: minimum number of digits in the validated string
6
+ # - digits: minimum number of digits in the validated string
6
7
  # - lower: minimum number of lower-case letters in the validated string
7
8
  # - symbol: minimum number of punctuation characters or symbols in the validated string
9
+ # - symbols: minimum number of punctuation characters or symbols in the validated string
8
10
  # - upper: minimum number of upper-case letters in the validated string
9
11
  class DeviseSecurity::PasswordComplexityValidator < ActiveModel::EachValidator
10
12
  PATTERNS = {
11
13
  digit: /\p{Digit}/,
12
14
  digits: /\p{Digit}/,
13
15
  lower: /\p{Lower}/,
14
- upper: /\p{Upper}/,
15
16
  symbol: /\p{Punct}|\p{S}/,
16
- symbols: /\p{Punct}|\p{S}/
17
+ symbols: /\p{Punct}|\p{S}/,
18
+ upper: /\p{Upper}/
17
19
  }.freeze
18
20
 
19
21
  def validate_each(record, attribute, value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeviseSecurity
4
- VERSION = '0.14.0'
4
+ VERSION = '0.16.0'
5
5
  end
@@ -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 de fr it ja tr].freeze
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('devise-security.rb',
14
- 'config/initializers/devise-security.rb',
13
+ template('devise_security.rb',
14
+ 'config/initializers/devise_security.rb',
15
15
  )
16
16
  end
17
17
 
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ Devise.setup do |config|
4
+ # ==> Security Extension
5
+ # Configure security extension for devise
6
+
7
+ # Should the password expire (e.g 3.months)
8
+ # config.expire_password_after = false
9
+
10
+ # Need 1 char of A-Z, a-z and 0-9
11
+ # config.password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
12
+
13
+ # How many passwords to keep in archive
14
+ # config.password_archiving_count = 5
15
+
16
+ # Deny old passwords (true, false, number_of_old_passwords_to_check)
17
+ # Examples:
18
+ # config.deny_old_passwords = false # allow old passwords
19
+ # config.deny_old_passwords = true # will deny all the old passwords
20
+ # config.deny_old_passwords = 3 # will deny new passwords that matches with the last 3 passwords
21
+ # config.deny_old_passwords = true
22
+
23
+ # enable email validation for :secure_validatable. (true, false, validation_options)
24
+ # dependency: see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
25
+ # config.email_validation = true
26
+
27
+ # captcha integration for recover form
28
+ # config.captcha_for_recover = true
29
+
30
+ # captcha integration for sign up form
31
+ # config.captcha_for_sign_up = true
32
+
33
+ # captcha integration for sign in form
34
+ # config.captcha_for_sign_in = true
35
+
36
+ # captcha integration for unlock form
37
+ # config.captcha_for_unlock = true
38
+
39
+ # captcha integration for confirmation form
40
+ # config.captcha_for_confirmation = true
41
+
42
+ # Time period for account expiry from last_activity_at
43
+ # config.expire_after = 90.days
44
+
45
+ # Allow password to equal the email
46
+ # config.allow_passwords_equal_to_email = false
47
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class Devise::PasswordExpiredControllerTest < ActionController::TestCase
6
+ include Devise::Test::ControllerHelpers
7
+
8
+ setup do
9
+ @controller.class.respond_to :json, :xml
10
+ @request.env['devise.mapping'] = Devise.mappings[:user]
11
+ @user = User.create!(
12
+ username: 'hello',
13
+ email: 'hello@path.travel',
14
+ password: 'Password4',
15
+ password_changed_at: 4.months.ago,
16
+ confirmed_at: 5.months.ago,
17
+ )
18
+ assert @user.valid?
19
+ assert @user.need_change_password?
20
+
21
+ sign_in(@user)
22
+ end
23
+
24
+ test 'redirects on show if user not logged in' do
25
+ sign_out(@user)
26
+ get :show
27
+ assert_redirected_to :root
28
+ end
29
+
30
+ test 'redirects on show if user does not need password change' do
31
+ @user.update(password_changed_at: Time.zone.now)
32
+ get :show
33
+ assert_redirected_to :root
34
+ end
35
+
36
+ test 'should render show' do
37
+ get :show
38
+ assert_includes @response.body, 'Renew your password'
39
+ end
40
+
41
+ test 'redirects on update if user not logged in' do
42
+ sign_out(@user)
43
+ put :update
44
+ assert_redirected_to :root
45
+ end
46
+
47
+ test 'redirects on update if user does not need password change' do
48
+ @user.update(password_changed_at: Time.zone.now)
49
+ put :update
50
+ assert_redirected_to :root
51
+ end
52
+
53
+ test 'update password with default format' do
54
+ put :update,
55
+ params: {
56
+ user: {
57
+ current_password: 'Password4',
58
+ password: 'Password5',
59
+ password_confirmation: 'Password5',
60
+ },
61
+ }
62
+ assert_redirected_to root_path
63
+ assert_equal response.media_type, 'text/html'
64
+ end
65
+
66
+ test 'password confirmation does not match' do
67
+ put :update,
68
+ params: {
69
+ user: {
70
+ current_password: 'Password4',
71
+ password: 'Password5',
72
+ password_confirmation: 'Password6',
73
+ },
74
+ }
75
+
76
+ assert_response :success
77
+ assert_template :show
78
+ assert_equal response.media_type, 'text/html'
79
+ end
80
+
81
+ test 'update password using JSON format' do
82
+ put :update,
83
+ format: :json,
84
+ params: {
85
+ user: {
86
+ current_password: 'Password4',
87
+ password: 'Password5',
88
+ password_confirmation: 'Password5',
89
+ },
90
+ }
91
+ assert_response 204
92
+ assert_equal root_url, response.location
93
+ assert_nil response.media_type, 'No Content-Type header should be set for No Content response'
94
+ end
95
+
96
+ test 'update password using XML format' do
97
+ put :update,
98
+ format: :xml,
99
+ params: {
100
+ user: {
101
+ current_password: 'Password4',
102
+ password: 'Password5',
103
+ password_confirmation: 'Password5',
104
+ },
105
+ }
106
+ assert_response 204
107
+ assert_equal root_url, response.location
108
+ assert_nil response.media_type, 'No Content-Type header should be set for No Content response'
109
+ end
110
+ end
@@ -8,44 +8,28 @@ class TestWithSecurityQuestion < ActionController::TestCase
8
8
 
9
9
  setup do
10
10
  @user = SecurityQuestionUser.create!(username: 'hello', email: 'hello@microsoft.com',
11
- password: 'A1234567z!', security_question_answer: 'Right Answer')
11
+ password: 'A1234567z!', security_question_answer: 'Right Answer')
12
12
  @user.lock_access!
13
13
  assert @user.locked_at.present?
14
14
  @request.env['devise.mapping'] = Devise.mappings[:security_question_user]
15
15
  end
16
16
 
17
17
  test 'When security question is enabled, it is inserted correctly' do
18
- if Rails.gem_version.release <= Gem::Version.new('5.0')
19
- post :create, {
20
- security_question_user: {
21
- email: @user.email
22
- }, security_question_answer: "wrong answer"
23
- }
24
- else
25
- post :create, params: {
26
- security_question_user: {
27
- email: @user.email
28
- }, security_question_answer: "wrong answer"
29
- }
30
- end
18
+ post :create, params: {
19
+ security_question_user: {
20
+ email: @user.email,
21
+ }, security_question_answer: 'wrong answer'
22
+ }
31
23
  assert_equal I18n.t('devise.invalid_security_question'), flash[:alert]
32
24
  assert_redirected_to new_security_question_user_unlock_path
33
25
  end
34
26
 
35
27
  test 'When security_question is valid, it runs as normal' do
36
- if Rails.gem_version.release <= Gem::Version.new('5.0')
37
- post :create, {
38
- security_question_user: {
39
- email: @user.email
40
- }, security_question_answer: @user.security_question_answer
41
- }
42
- else
43
- post :create, params: {
44
- security_question_user: {
45
- email: @user.email
46
- }, security_question_answer: @user.security_question_answer
47
- }
48
- end
28
+ post :create, params: {
29
+ security_question_user: {
30
+ email: @user.email,
31
+ }, security_question_answer: @user.security_question_answer
32
+ }
49
33
 
50
34
  assert_equal I18n.t('devise.unlocks.send_instructions'), flash[:notice]
51
35
  assert_redirected_to new_security_question_user_session_path
@@ -64,19 +48,11 @@ class TestWithoutSecurityQuestion < ActionController::TestCase
64
48
  end
65
49
 
66
50
  test 'When security question is not enabled it is not inserted' do
67
- if Rails.gem_version.release <= Gem::Version.new('5.0')
68
- post :create, {
69
- user: {
70
- email: @user.email
71
- }
72
- }
73
- else
74
- post :create, params: {
75
- user: {
76
- email: @user.email
77
- }
78
- }
79
- end
51
+ post :create, params: {
52
+ user: {
53
+ email: @user.email,
54
+ },
55
+ }
80
56
 
81
57
  assert_equal I18n.t('devise.unlocks.send_instructions'), flash[:notice]
82
58
  assert_redirected_to new_user_session_path
@@ -0,0 +1,3 @@
1
+ // = link_tree ../images
2
+ // = link_directory ../javascripts .js
3
+ // = link_directory ../stylesheets .css
@@ -0,0 +1,6 @@
1
+ class WidgetsController < ApplicationController
2
+ before_action :authenticate_user!
3
+ def show
4
+ render plain: 'success'
5
+ end
6
+ end
@@ -25,5 +25,13 @@ class User < ApplicationRecord
25
25
  if DEVISE_ORM == :mongoid
26
26
  require './test/dummy/app/models/mongoid/mappings'
27
27
  include ::Mongoid::Mappings
28
+
29
+ def some_method_calling_mongoid
30
+ Mongoid.logger
31
+ end
32
+ elsif DEVISE_ORM == :active_record
33
+ def some_method_calling_active_record
34
+ ActiveRecord::Base.transaction {}
35
+ end
28
36
  end
29
37
  end
@@ -5,6 +5,7 @@ require File.expand_path('../boot', __FILE__)
5
5
  require 'action_mailer/railtie'
6
6
  require "action_mailer/railtie"
7
7
  require "rails/test_unit/railtie"
8
+ DEVISE_ORM = ENV.fetch('DEVISE_ORM', 'active_record').to_sym
8
9
 
9
10
  Bundler.require :default, DEVISE_ORM
10
11
  require "#{DEVISE_ORM}/railtie"
@@ -4,13 +4,8 @@ RailsApp::Application.configure do
4
4
  config.cache_classes = true
5
5
  config.eager_load = false
6
6
 
7
- if Rails.version > '5'
8
- config.public_file_server.enabled = true
9
- config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' }
10
- else
11
- config.serve_static_files = true
12
- config.static_cache_control = 'public, max-age=3600'
13
- end
7
+ config.public_file_server.enabled = true
8
+ config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' }
14
9
 
15
10
  config.consider_all_requests_local = true
16
11
  config.action_controller.perform_caching = false
@@ -27,11 +22,6 @@ RailsApp::Application.configure do
27
22
 
28
23
  config.active_support.test_order = :sorted
29
24
  config.log_level = :debug
30
- if Rails.gem_version >= Gem::Version.new('4.2') && Rails.gem_version.release < Gem::Version.new('5.0')
31
- config.active_record.raise_in_transactional_callbacks = true
32
- end
33
- if Rails.gem_version.release >= Gem::Version.new('5.2') && Rails.gem_version.release < Gem::Version.new('6.0')
34
- config.active_record.sqlite3.represent_boolean_as_integer = true
35
- end
25
+ config.active_record.sqlite3.represent_boolean_as_integer = true if Rails.gem_version.release >= Gem::Version.new('5.2') && Rails.gem_version.release < Gem::Version.new('6.0')
36
26
  end
37
27
  ActiveSupport::Deprecation.debug = true
@@ -1,10 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if DEVISE_ORM == :active_record
4
- MIGRATION_CLASS =
5
- if Rails.gem_version >= Gem::Version.new('5.0')
6
- ActiveRecord::Migration[Rails.version.to_f]
7
- else
8
- ActiveRecord::Migration
9
- end
10
- end
3
+ MIGRATION_CLASS = ActiveRecord::Migration[Rails.version.to_f] if DEVISE_ORM == :active_record