devise-security 0.12.0 → 0.13.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +63 -0
  3. data/.gitignore +2 -0
  4. data/.mdlrc +1 -0
  5. data/.rubocop.yml +2 -1
  6. data/.ruby-version +1 -1
  7. data/.travis.yml +9 -11
  8. data/Appraisals +2 -2
  9. data/README.md +72 -53
  10. data/app/controllers/devise/paranoid_verification_code_controller.rb +2 -0
  11. data/app/controllers/devise/password_expired_controller.rb +2 -0
  12. data/config/locales/de.yml +13 -1
  13. data/config/locales/en.yml +13 -1
  14. data/config/locales/es.yml +13 -1
  15. data/config/locales/fr.yml +29 -0
  16. data/config/locales/tr.yml +17 -0
  17. data/devise-security.gemspec +10 -10
  18. data/gemfiles/{rails_4.1_stable.gemfile → rails_5.2.0.gemfile} +1 -1
  19. data/lib/devise-security.rb +8 -4
  20. data/lib/devise-security/controllers/helpers.rb +2 -0
  21. data/lib/devise-security/hooks/expirable.rb +3 -1
  22. data/lib/devise-security/hooks/paranoid_verification.rb +2 -0
  23. data/lib/devise-security/hooks/password_expirable.rb +2 -0
  24. data/lib/devise-security/hooks/session_limitable.rb +2 -0
  25. data/lib/devise-security/models/compatibility.rb +2 -0
  26. data/lib/devise-security/models/database_authenticatable_patch.rb +2 -0
  27. data/lib/devise-security/models/expirable.rb +2 -0
  28. data/lib/devise-security/models/old_password.rb +2 -0
  29. data/lib/devise-security/models/paranoid_verification.rb +2 -0
  30. data/lib/devise-security/models/password_archivable.rb +2 -0
  31. data/lib/devise-security/models/password_expirable.rb +96 -50
  32. data/lib/devise-security/models/secure_validatable.rb +10 -4
  33. data/lib/devise-security/models/security_questionable.rb +2 -0
  34. data/lib/devise-security/models/session_limitable.rb +2 -0
  35. data/lib/devise-security/orm/active_record.rb +2 -0
  36. data/lib/devise-security/patches.rb +2 -0
  37. data/lib/devise-security/patches/confirmations_controller_captcha.rb +2 -0
  38. data/lib/devise-security/patches/confirmations_controller_security_question.rb +2 -0
  39. data/lib/devise-security/patches/controller_captcha.rb +2 -0
  40. data/lib/devise-security/patches/controller_security_question.rb +2 -0
  41. data/lib/devise-security/patches/passwords_controller_captcha.rb +2 -0
  42. data/lib/devise-security/patches/passwords_controller_security_question.rb +2 -0
  43. data/lib/devise-security/patches/registrations_controller_captcha.rb +2 -0
  44. data/lib/devise-security/patches/sessions_controller_captcha.rb +2 -0
  45. data/lib/devise-security/patches/unlocks_controller_captcha.rb +2 -0
  46. data/lib/devise-security/patches/unlocks_controller_security_question.rb +2 -0
  47. data/lib/devise-security/rails.rb +2 -0
  48. data/lib/devise-security/routes.rb +2 -0
  49. data/lib/devise-security/schema.rb +2 -0
  50. data/lib/devise-security/validators/password_complexity_validator.rb +33 -0
  51. data/lib/devise-security/version.rb +3 -1
  52. data/lib/generators/devise_security/install_generator.rb +3 -1
  53. data/lib/generators/templates/devise-security.rb +9 -3
  54. data/test/dummy/Rakefile +3 -1
  55. data/test/dummy/app/controllers/application_controller.rb +2 -0
  56. data/test/dummy/app/controllers/captcha/sessions_controller.rb +2 -0
  57. data/test/dummy/app/controllers/security_question/unlocks_controller.rb +2 -0
  58. data/test/dummy/app/models/application_record.rb +2 -0
  59. data/test/dummy/app/models/captcha_user.rb +3 -1
  60. data/test/dummy/app/models/secure_user.rb +3 -1
  61. data/test/dummy/app/models/security_question_user.rb +3 -1
  62. data/test/dummy/app/models/user.rb +2 -0
  63. data/test/dummy/app/models/widget.rb +2 -0
  64. data/test/dummy/config.ru +3 -1
  65. data/test/dummy/config/application.rb +2 -0
  66. data/test/dummy/config/boot.rb +2 -0
  67. data/test/dummy/config/environment.rb +2 -0
  68. data/test/dummy/config/environments/test.rb +2 -0
  69. data/test/dummy/config/initializers/devise.rb +8 -0
  70. data/test/dummy/config/initializers/migration_class.rb +2 -0
  71. data/test/dummy/config/routes.rb +2 -0
  72. data/test/dummy/db/migrate/20120508165529_create_tables.rb +2 -0
  73. data/test/dummy/db/migrate/20150402165590_add_verification_columns.rb +2 -0
  74. data/test/dummy/db/migrate/20150407162345_add_verification_attempt_column.rb +2 -0
  75. data/test/dummy/db/migrate/20160320162345_add_security_questions_fields.rb +2 -0
  76. data/test/dummy/db/migrate/20180318103603_add_expireable_columns.rb +2 -0
  77. data/test/dummy/db/migrate/20180318105329_add_confirmable_columns.rb +2 -0
  78. data/test/dummy/db/migrate/20180318105732_add_rememberable_columns.rb +2 -0
  79. data/test/dummy/db/migrate/20180318111336_add_recoverable_columns.rb +2 -0
  80. data/test/dummy/db/migrate/20180319114023_add_widget.rb +2 -0
  81. data/test/test_captcha_controller.rb +2 -0
  82. data/test/test_complexity_validator.rb +60 -0
  83. data/test/test_helper.rb +19 -8
  84. data/test/test_install_generator.rb +7 -1
  85. data/test/test_paranoid_verification.rb +2 -0
  86. data/test/test_password_archivable.rb +2 -0
  87. data/test/test_password_expirable.rb +68 -7
  88. data/test/test_password_expired_controller.rb +2 -0
  89. data/test/test_secure_validatable.rb +10 -11
  90. data/test/test_security_question_controller.rb +2 -0
  91. metadata +32 -39
  92. data/.circleci/config.yml +0 -41
  93. data/gemfiles/rails_5.2_rc1.gemfile +0 -8
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity
2
4
  module Patches
3
5
  autoload :ControllerCaptcha, 'devise-security/patches/controller_captcha'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity::Patches
2
4
  module ConfirmationsControllerCaptcha
3
5
  extend ActiveSupport::Concern
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity::Patches
2
4
  module ConfirmationsControllerSecurityQuestion
3
5
  extend ActiveSupport::Concern
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity::Patches
2
4
  module ControllerCaptcha
3
5
  extend ActiveSupport::Concern
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity::Patches
2
4
  module ControllerSecurityQuestion
3
5
  extend ActiveSupport::Concern
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity::Patches
2
4
  module PasswordsControllerCaptcha
3
5
  extend ActiveSupport::Concern
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity::Patches
2
4
  module PasswordsControllerSecurityQuestion
3
5
  extend ActiveSupport::Concern
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity::Patches
2
4
  module RegistrationsControllerCaptcha
3
5
  extend ActiveSupport::Concern
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity::Patches
2
4
  module SessionsControllerCaptcha
3
5
  extend ActiveSupport::Concern
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity::Patches
2
4
  module UnlocksControllerCaptcha
3
5
  extend ActiveSupport::Concern
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity::Patches
2
4
  module UnlocksControllerSecurityQuestion
3
5
  extend ActiveSupport::Concern
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity
2
4
  class Engine < ::Rails::Engine
3
5
  ActiveSupport.on_load(:action_controller) do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionDispatch::Routing
2
4
  class Mapper
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity
2
4
  # add schema helper for migrations
3
5
  module Schema
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Password complexity validator
4
+ # Options:
5
+ # - digit: minimum number of digits in the validated string
6
+ # - lower: minimum number of lower-case letters in the validated string
7
+ # - symbol: minimum number of punctuation characters or symbols in the validated string
8
+ # - upper: minimum number of upper-case letters in the validated string
9
+ class DeviseSecurity::PasswordComplexityValidator < ActiveModel::EachValidator
10
+ PATTERNS = {
11
+ digit: /\p{Digit}/,
12
+ digits: /\p{Digit}/,
13
+ lower: /\p{Lower}/,
14
+ upper: /\p{Upper}/,
15
+ symbol: /\p{Punct}|\p{S}/,
16
+ symbols: /\p{Punct}|\p{S}/
17
+ }.freeze
18
+
19
+ def validate_each(record, attribute, value)
20
+ active_pattern_keys.each do |key|
21
+ minimum = [0, options[key].to_i].max
22
+ pattern = Regexp.new PATTERNS[key]
23
+
24
+ unless (value || '').scan(pattern).size >= minimum
25
+ record.errors.add attribute, :"password_complexity.#{key}", count: minimum
26
+ end
27
+ end
28
+ end
29
+
30
+ def active_pattern_keys
31
+ options.keys & PATTERNS.keys
32
+ end
33
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity
2
- VERSION = '0.12.0'.freeze
4
+ VERSION = '0.13.0'
3
5
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity
2
4
  module Generators
3
5
  # Generator for Rails to create or append to a Devise initializer.
4
6
  class InstallGenerator < Rails::Generators::Base
5
- LOCALES = %w[ en de it ]
7
+ LOCALES = %w[en es de fr it tr].freeze
6
8
 
7
9
  source_root File.expand_path('../../templates', __FILE__)
8
10
  desc 'Install the devise security extension'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Devise.setup do |config|
2
4
  # ==> Security Extension
3
5
  # Configure security extension for devise
@@ -6,16 +8,20 @@ Devise.setup do |config|
6
8
  # config.expire_password_after = false
7
9
 
8
10
  # Need 1 char of A-Z, a-z and 0-9
9
- # config.password_regex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/
11
+ # config.password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
10
12
 
11
13
  # How many passwords to keep in archive
12
14
  # config.password_archiving_count = 5
13
15
 
14
- # Deny old password (true, false, count)
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
15
21
  # config.deny_old_passwords = true
16
22
 
17
23
  # enable email validation for :secure_validatable. (true, false, validation_options)
18
- # dependency: need an email validator like rails_email_validator
24
+ # dependency: see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
19
25
  # config.email_validation = true
20
26
 
21
27
  # captcha integration for recover form
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Add your own tasks in files placed in lib/tasks ending in .rake,
2
4
  # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
5
 
4
- require File.expand_path('../config/application', __FILE__)
6
+ require File.expand_path('config/application', __dir__)
5
7
 
6
8
  Rails.application.load_tasks
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ApplicationController < ActionController::Base
2
4
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Captcha::SessionsController < Devise::SessionsController
2
4
  include DeviseSecurity::Patches::ControllerCaptcha
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class SecurityQuestion::UnlocksController < Devise::UnlocksController
2
4
  include DeviseSecurity::Patches::ControllerSecurityQuestion
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ApplicationRecord < ActiveRecord::Base
2
4
  self.abstract_class = true
3
5
  end
@@ -1,4 +1,6 @@
1
- class CaptchaUser < ActiveRecord::Base
1
+ # frozen_string_literal: true
2
+
3
+ class CaptchaUser < ApplicationRecord
2
4
  self.table_name = 'users'
3
5
  devise :database_authenticatable, :password_archivable,
4
6
  :paranoid_verification, :password_expirable
@@ -1,3 +1,5 @@
1
- class SecureUser < ActiveRecord::Base
1
+ # frozen_string_literal: true
2
+
3
+ class SecureUser < ApplicationRecord
2
4
  devise :database_authenticatable, :secure_validatable, email_validation: false
3
5
  end
@@ -1,4 +1,6 @@
1
- class SecurityQuestionUser < ActiveRecord::Base
1
+ # frozen_string_literal: true
2
+
3
+ class SecurityQuestionUser < ApplicationRecord
2
4
  self.table_name = 'users'
3
5
  devise :database_authenticatable, :password_archivable, :lockable,
4
6
  :paranoid_verification, :password_expirable, :security_questionable
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class User < ApplicationRecord
2
4
 
3
5
  devise :database_authenticatable,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Widget < ApplicationRecord
2
4
  belongs_to :user
3
5
  validates_associated :user
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This file is used by Rack-based servers to start the application.
2
4
 
3
- require ::File.expand_path('../config/environment', __FILE__)
5
+ require ::File.expand_path('../config/environment', __FILE__)
4
6
  run RailsApp::Application
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.expand_path('../boot', __FILE__)
2
4
 
3
5
  require 'rails/all'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rubygems'
2
4
 
3
5
  # Set up gems listed in the Gemfile.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Load the rails application
2
4
  require File.expand_path('../application', __FILE__)
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  RailsApp::Application.configure do
2
4
  config.cache_classes = true
3
5
  config.eager_load = false
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rails_email_validator'
2
4
  Devise.setup do |config|
3
5
  config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
@@ -7,4 +9,10 @@ Devise.setup do |config|
7
9
  config.case_insensitive_keys = [:email]
8
10
 
9
11
  config.strip_whitespace_keys = [:email]
12
+
13
+ config.password_complexity = {
14
+ digit: 1,
15
+ lower: 1,
16
+ upper: 1,
17
+ }
10
18
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  MIGRATION_CLASS =
2
4
  if ActiveRecord::VERSION::MAJOR >= 5
3
5
  ActiveRecord::Migration[4.2]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  RailsApp::Application.routes.draw do
2
4
  devise_for :users
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class CreateTables < MIGRATION_CLASS
2
4
  def self.up
3
5
  create_table :users do |t|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddVerificationColumns < MIGRATION_CLASS
2
4
  def self.up
3
5
  add_column :users, :paranoid_verification_code, :string
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddVerificationAttemptColumn < MIGRATION_CLASS
2
4
  def self.up
3
5
  add_column :users, :paranoid_verification_attempt, :integer, default: 0
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddSecurityQuestionsFields < MIGRATION_CLASS
2
4
  def change
3
5
  add_column :users, :locked_at, :datetime
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddExpireableColumns < MIGRATION_CLASS
2
4
  def change
3
5
  add_column :users, :expired_at, :datetime
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddConfirmableColumns < MIGRATION_CLASS
2
4
  def change
3
5
  add_column :users, :confirmation_token, :string
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddRememberableColumns < MIGRATION_CLASS
2
4
  def change
3
5
  add_column :users, :remember_created_at, :datetime
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddRecoverableColumns < MIGRATION_CLASS
2
4
  def change
3
5
  add_column :users, :reset_password_token, :string
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddWidget < MIGRATION_CLASS
2
4
  def change
3
5
  create_table :widgets do |t|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class TestWithCaptcha < ActionController::TestCase
@@ -0,0 +1,60 @@
1
+ require 'test_helper'
2
+
3
+ class PasswordComplexityValidatorTest < Minitest::Test
4
+ class ModelWithPassword
5
+ include ActiveModel::Validations
6
+
7
+ attr_reader :password
8
+
9
+ def initialize(password)
10
+ @password = password
11
+ end
12
+ end
13
+
14
+ def setup
15
+ ModelWithPassword.clear_validators!
16
+ end
17
+
18
+ def test_with_no_rules_anything_goes
19
+ assert(ModelWithPassword.new('aaaa').valid?)
20
+ end
21
+
22
+ def test_enforces_uppercase
23
+ ModelWithPassword.validates :password, 'devise_security/password_complexity': { upper: 1 }
24
+ refute(ModelWithPassword.new('aaaa').valid?)
25
+ assert(ModelWithPassword.new('Aaaa').valid?)
26
+ end
27
+
28
+ def test_enforces_count
29
+ ModelWithPassword.validates :password, 'devise_security/password_complexity': { upper: 2 }
30
+ refute(ModelWithPassword.new('Aaaa').valid?)
31
+ assert(ModelWithPassword.new('AAaa').valid?)
32
+ end
33
+
34
+ def test_enforces_digit
35
+ ModelWithPassword.validates :password, 'devise_security/password_complexity': { digit: 1 }
36
+ refute(ModelWithPassword.new('aaaa').valid?)
37
+ assert(ModelWithPassword.new('aaa1').valid?)
38
+ end
39
+
40
+ def test_enforces_lower
41
+ ModelWithPassword.validates :password, 'devise_security/password_complexity': { lower: 1 }
42
+ refute(ModelWithPassword.new('AAAA').valid?)
43
+ assert(ModelWithPassword.new('AAAa').valid?)
44
+ end
45
+
46
+ def test_enforces_symbol
47
+ ModelWithPassword.validates :password, 'devise_security/password_complexity': { symbol: 1 }
48
+ refute(ModelWithPassword.new('aaaa').valid?)
49
+ assert(ModelWithPassword.new('aaa!').valid?)
50
+ end
51
+
52
+ def test_enforces_combination
53
+ ModelWithPassword.validates :password, 'devise_security/password_complexity': { lower: 1, upper: 1, digit: 1, symbol: 1 }
54
+ refute(ModelWithPassword.new('abcd').valid?)
55
+ refute(ModelWithPassword.new('ABCD').valid?)
56
+ refute(ModelWithPassword.new('1234').valid?)
57
+ refute(ModelWithPassword.new('$!,*').valid?)
58
+ assert(ModelWithPassword.new('aB3*').valid?)
59
+ end
60
+ end