devise-security 0.12.0 → 0.13.0

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