devise-security 0.11.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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +38 -0
  4. data/.rubocop.yml +42 -0
  5. data/.travis.yml +14 -0
  6. data/Gemfile +2 -0
  7. data/Gemfile.lock +199 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.md +263 -0
  10. data/Rakefile +26 -0
  11. data/app/controllers/devise/paranoid_verification_code_controller.rb +42 -0
  12. data/app/controllers/devise/password_expired_controller.rb +48 -0
  13. data/app/views/devise/paranoid_verification_code/show.html.erb +10 -0
  14. data/app/views/devise/password_expired/show.html.erb +16 -0
  15. data/config/locales/de.yml +16 -0
  16. data/config/locales/en.yml +17 -0
  17. data/config/locales/es.yml +17 -0
  18. data/config/locales/it.yml +10 -0
  19. data/devise-security.gemspec +34 -0
  20. data/lib/devise-security.rb +106 -0
  21. data/lib/devise-security/controllers/helpers.rb +96 -0
  22. data/lib/devise-security/hooks/expirable.rb +10 -0
  23. data/lib/devise-security/hooks/paranoid_verification.rb +5 -0
  24. data/lib/devise-security/hooks/password_expirable.rb +5 -0
  25. data/lib/devise-security/hooks/session_limitable.rb +27 -0
  26. data/lib/devise-security/models/database_authenticatable_patch.rb +26 -0
  27. data/lib/devise-security/models/expirable.rb +120 -0
  28. data/lib/devise-security/models/old_password.rb +4 -0
  29. data/lib/devise-security/models/paranoid_verification.rb +35 -0
  30. data/lib/devise-security/models/password_archivable.rb +80 -0
  31. data/lib/devise-security/models/password_expirable.rb +67 -0
  32. data/lib/devise-security/models/secure_validatable.rb +100 -0
  33. data/lib/devise-security/models/security_questionable.rb +18 -0
  34. data/lib/devise-security/models/session_limitable.rb +21 -0
  35. data/lib/devise-security/orm/active_record.rb +20 -0
  36. data/lib/devise-security/patches.rb +21 -0
  37. data/lib/devise-security/patches/confirmations_controller_captcha.rb +21 -0
  38. data/lib/devise-security/patches/confirmations_controller_security_question.rb +25 -0
  39. data/lib/devise-security/patches/controller_captcha.rb +17 -0
  40. data/lib/devise-security/patches/controller_security_question.rb +20 -0
  41. data/lib/devise-security/patches/passwords_controller_captcha.rb +20 -0
  42. data/lib/devise-security/patches/passwords_controller_security_question.rb +24 -0
  43. data/lib/devise-security/patches/registrations_controller_captcha.rb +33 -0
  44. data/lib/devise-security/patches/sessions_controller_captcha.rb +24 -0
  45. data/lib/devise-security/patches/unlocks_controller_captcha.rb +20 -0
  46. data/lib/devise-security/patches/unlocks_controller_security_question.rb +24 -0
  47. data/lib/devise-security/rails.rb +17 -0
  48. data/lib/devise-security/routes.rb +17 -0
  49. data/lib/devise-security/schema.rb +59 -0
  50. data/lib/devise-security/version.rb +3 -0
  51. data/lib/generators/devise-security/install_generator.rb +26 -0
  52. data/lib/generators/templates/devise-security.rb +38 -0
  53. data/test/dummy/Rakefile +6 -0
  54. data/test/dummy/app/controllers/application_controller.rb +2 -0
  55. data/test/dummy/app/controllers/captcha/sessions_controller.rb +3 -0
  56. data/test/dummy/app/controllers/foos_controller.rb +0 -0
  57. data/test/dummy/app/controllers/security_question/unlocks_controller.rb +3 -0
  58. data/test/dummy/app/models/.gitkeep +0 -0
  59. data/test/dummy/app/models/captcha_user.rb +5 -0
  60. data/test/dummy/app/models/secure_user.rb +3 -0
  61. data/test/dummy/app/models/security_question_user.rb +6 -0
  62. data/test/dummy/app/models/user.rb +5 -0
  63. data/test/dummy/app/views/foos/index.html.erb +0 -0
  64. data/test/dummy/config.ru +4 -0
  65. data/test/dummy/config/application.rb +24 -0
  66. data/test/dummy/config/boot.rb +6 -0
  67. data/test/dummy/config/database.yml +7 -0
  68. data/test/dummy/config/environment.rb +5 -0
  69. data/test/dummy/config/environments/test.rb +27 -0
  70. data/test/dummy/config/initializers/devise.rb +9 -0
  71. data/test/dummy/config/initializers/migration_class.rb +6 -0
  72. data/test/dummy/config/routes.rb +10 -0
  73. data/test/dummy/config/secrets.yml +3 -0
  74. data/test/dummy/db/migrate/20120508165529_create_tables.rb +33 -0
  75. data/test/dummy/db/migrate/20150402165590_add_verification_columns.rb +11 -0
  76. data/test/dummy/db/migrate/20150407162345_add_verification_attempt_column.rb +9 -0
  77. data/test/dummy/db/migrate/20160320162345_add_security_questions_fields.rb +8 -0
  78. data/test/test_captcha_controller.rb +58 -0
  79. data/test/test_helper.rb +13 -0
  80. data/test/test_install_generator.rb +16 -0
  81. data/test/test_paranoid_verification.rb +124 -0
  82. data/test/test_password_archivable.rb +61 -0
  83. data/test/test_password_expirable.rb +32 -0
  84. data/test/test_password_expired_controller.rb +29 -0
  85. data/test/test_secure_validatable.rb +85 -0
  86. data/test/test_security_question_controller.rb +60 -0
  87. metadata +315 -0
@@ -0,0 +1,38 @@
1
+ Devise.setup do |config|
2
+ # ==> Security Extension
3
+ # Configure security extension for devise
4
+
5
+ # Should the password expire (e.g 3.months)
6
+ # config.expire_password_after = false
7
+
8
+ # Need 1 char of A-Z, a-z and 0-9
9
+ # config.password_regex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/
10
+
11
+ # How many passwords to keep in archive
12
+ # config.password_archiving_count = 5
13
+
14
+ # Deny old password (true, false, count)
15
+ # config.deny_old_passwords = true
16
+
17
+ # enable email validation for :secure_validatable. (true, false, validation_options)
18
+ # dependency: need an email validator like rails_email_validator
19
+ # config.email_validation = true
20
+
21
+ # captcha integration for recover form
22
+ # config.captcha_for_recover = true
23
+
24
+ # captcha integration for sign up form
25
+ # config.captcha_for_sign_up = true
26
+
27
+ # captcha integration for sign in form
28
+ # config.captcha_for_sign_in = true
29
+
30
+ # captcha integration for unlock form
31
+ # config.captcha_for_unlock = true
32
+
33
+ # captcha integration for confirmation form
34
+ # config.captcha_for_confirmation = true
35
+
36
+ # Time period for account expiry from last_activity_at
37
+ # config.expire_after = 90.days
38
+ end
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -0,0 +1,3 @@
1
+ class Captcha::SessionsController < Devise::SessionsController
2
+ include DeviseSecurity::Patches::ControllerCaptcha
3
+ end
@@ -0,0 +1,3 @@
1
+ class SecurityQuestion::UnlocksController < Devise::UnlocksController
2
+ include DeviseSecurity::Patches::ControllerSecurityQuestion
3
+ end
File without changes
@@ -0,0 +1,5 @@
1
+ class CaptchaUser < ActiveRecord::Base
2
+ self.table_name = "users"
3
+ devise :database_authenticatable, :password_archivable,
4
+ :paranoid_verification, :password_expirable
5
+ end
@@ -0,0 +1,3 @@
1
+ class SecureUser < ActiveRecord::Base
2
+ devise :database_authenticatable, :secure_validatable, email_validation: false
3
+ end
@@ -0,0 +1,6 @@
1
+ class SecurityQuestionUser < ActiveRecord::Base
2
+ self.table_name = "users"
3
+ devise :database_authenticatable, :password_archivable, :lockable,
4
+ :paranoid_verification, :password_expirable,
5
+ :security_questionable
6
+ end
@@ -0,0 +1,5 @@
1
+ class User < ActiveRecord::Base
2
+ devise :database_authenticatable, :password_archivable, :lockable,
3
+ :paranoid_verification, :password_expirable,
4
+ :security_questionable
5
+ end
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run RailsApp::Application
@@ -0,0 +1,24 @@
1
+ require File.expand_path('../boot', __FILE__)
2
+
3
+ require 'rails/all'
4
+ require 'devise-security'
5
+
6
+ if defined?(Bundler)
7
+ # If you precompile assets before deploying to production, use this line
8
+ Bundler.require(*Rails.groups(assets: %w[development test]))
9
+ # If you want your assets lazily compiled in production, use this line
10
+ # Bundler.require(:default, :assets, Rails.env)
11
+ end
12
+
13
+ module RailsApp
14
+ class Application < Rails::Application
15
+ config.encoding = 'utf-8'
16
+
17
+ config.filter_parameters += [:password]
18
+
19
+ config.assets.enabled = true
20
+
21
+ config.assets.version = '1.0'
22
+ config.secret_key_base = 'fuuuuuuuuuuu'
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+
3
+ # Set up gems listed in the Gemfile.
4
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5
+
6
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
@@ -0,0 +1,7 @@
1
+ development:
2
+ adapter: sqlite3
3
+ database: ":memory:"
4
+
5
+ test:
6
+ adapter: sqlite3
7
+ database: ":memory:"
@@ -0,0 +1,5 @@
1
+ # Load the rails application
2
+ require File.expand_path('../application', __FILE__)
3
+
4
+ # Initialize the rails application
5
+ RailsApp::Application.initialize!
@@ -0,0 +1,27 @@
1
+ RailsApp::Application.configure do
2
+ config.cache_classes = true
3
+ config.eager_load = false
4
+
5
+ if Rails.version > "5"
6
+ config.public_file_server.enabled = true
7
+ config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' }
8
+ else
9
+ config.serve_static_files = true
10
+ config.static_cache_control = 'public, max-age=3600'
11
+ end
12
+
13
+ config.consider_all_requests_local = true
14
+ config.action_controller.perform_caching = false
15
+
16
+ config.action_dispatch.show_exceptions = false
17
+
18
+ config.action_controller.allow_forgery_protection = false
19
+
20
+ config.action_mailer.delivery_method = :test
21
+ config.action_mailer.default_url_options = { host: 'test.host' }
22
+
23
+ config.active_support.deprecation = :stderr
24
+ I18n.enforce_available_locales = false
25
+
26
+ config.active_support.test_order = :sorted
27
+ end
@@ -0,0 +1,9 @@
1
+ Devise.setup do |config|
2
+ config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
3
+
4
+ require 'devise/orm/active_record'
5
+ config.secret_key = 'f08cf11a38906f531d2dfc9a2c2d671aa0021be806c21255d4'
6
+ config.case_insensitive_keys = [:email]
7
+
8
+ config.strip_whitespace_keys = [:email]
9
+ end
@@ -0,0 +1,6 @@
1
+ MIGRATION_CLASS =
2
+ if ActiveRecord::VERSION::MAJOR >= 5
3
+ ActiveRecord::Migration[4.2]
4
+ else
5
+ ActiveRecord::Migration
6
+ end
@@ -0,0 +1,10 @@
1
+ RailsApp::Application.routes.draw do
2
+ devise_for :users
3
+
4
+ devise_for :captcha_users, only: [:sessions], controllers: { sessions: "captcha/sessions" }
5
+ devise_for :security_question_users, only: [:sessions, :unlocks], controllers: { unlocks: "security_question/unlocks" }
6
+
7
+ resources :foos
8
+
9
+ root to: 'foos#index'
10
+ end
@@ -0,0 +1,3 @@
1
+ test:
2
+ secret_token: 'fooooooooooo'
3
+ secret_key_base: 'fuuuuuuuuuuu'
@@ -0,0 +1,33 @@
1
+ class CreateTables < MIGRATION_CLASS
2
+ def self.up
3
+ create_table :users do |t|
4
+ t.string :username
5
+ t.string :facebook_token
6
+
7
+ ## Database authenticatable
8
+ t.string :email, null: false, default: ''
9
+ t.string :encrypted_password, null: false, default: ''
10
+
11
+ t.datetime :password_changed_at
12
+ t.timestamps null: false
13
+ end
14
+
15
+ create_table :secure_users do |t|
16
+ t.string :email
17
+ t.string :encrypted_password, null: false, default: ''
18
+ t.timestamps null: false
19
+ end
20
+
21
+ create_table :old_passwords do |t|
22
+ t.string :encrypted_password
23
+
24
+ t.references :password_archivable, polymorphic: true
25
+ end
26
+ end
27
+
28
+ def self.down
29
+ drop_table :users
30
+ drop_table :secure_users
31
+ drop_table :old_passwords
32
+ end
33
+ end
@@ -0,0 +1,11 @@
1
+ class AddVerificationColumns < MIGRATION_CLASS
2
+ def self.up
3
+ add_column :users, :paranoid_verification_code, :string
4
+ add_column :users, :paranoid_verified_at, :datetime
5
+ end
6
+
7
+ def self.down
8
+ remove_column :users, :paranoid_verification_code
9
+ remove_column :users, :paranoid_verified_at
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ class AddVerificationAttemptColumn < MIGRATION_CLASS
2
+ def self.up
3
+ add_column :users, :paranoid_verification_attempt, :integer, default: 0
4
+ end
5
+
6
+ def self.down
7
+ remove_column :users, :paranoid_verification_attempt
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ class AddSecurityQuestionsFields < MIGRATION_CLASS
2
+ def change
3
+ add_column :users, :locked_at, :datetime
4
+ add_column :users, :unlock_token, :string
5
+ add_column :users, :security_question_id, :integer
6
+ add_column :users, :security_question_answer, :string
7
+ end
8
+ end
@@ -0,0 +1,58 @@
1
+ require 'test_helper'
2
+
3
+ class TestWithCaptcha < ActionController::TestCase
4
+ include Devise::Test::ControllerHelpers
5
+ tests Captcha::SessionsController
6
+
7
+ setup do
8
+ @request.env["devise.mapping"] = Devise.mappings[:captcha_user]
9
+ end
10
+
11
+ test 'When captcha is enabled, it is inserted correctly' do
12
+ post :create, params: {
13
+ captcha_user: {
14
+ email: "wrong@email.com",
15
+ password: "wrongpassword"
16
+ }
17
+ }
18
+
19
+ assert_equal "The captcha input was invalid.", flash[:alert]
20
+ assert_redirected_to new_captcha_user_session_path
21
+ end
22
+
23
+ test 'When captcha is valid, it runs as normal' do
24
+ @controller.define_singleton_method(:verify_recaptcha) do
25
+ true
26
+ end
27
+
28
+ post :create, params: {
29
+ captcha: "ABCDE",
30
+ captcha_user: {
31
+ email: "wrong@email.com",
32
+ password: "wrongpassword"
33
+ }
34
+ }
35
+
36
+ assert_equal "Invalid Email or password.", flash[:alert]
37
+ end
38
+ end
39
+
40
+ class TestWithoutCaptcha < ActionController::TestCase
41
+ include Devise::Test::ControllerHelpers
42
+ tests Devise::SessionsController
43
+
44
+ setup do
45
+ @request.env["devise.mapping"] = Devise.mappings[:user]
46
+ end
47
+
48
+ test 'When captcha is not enabled, it is not inserted' do
49
+ post :create, params: {
50
+ user: {
51
+ email: "wrong@email.com",
52
+ password: "wrongpassword"
53
+ }
54
+ }
55
+
56
+ assert_equal "Invalid Email or password.", flash[:alert]
57
+ end
58
+ end
@@ -0,0 +1,13 @@
1
+ ENV['RAILS_ENV'] ||= 'test'
2
+
3
+ require 'coveralls'
4
+ Coveralls.wear!
5
+
6
+ require 'dummy/config/environment'
7
+ require 'minitest/autorun'
8
+ require 'rails/test_help'
9
+ require 'devise-security'
10
+
11
+ ActiveRecord::Migration.verbose = false
12
+ ActiveRecord::Base.logger = Logger.new(nil)
13
+ ActiveRecord::Migrator.migrate(File.expand_path('../dummy/db/migrate', __FILE__))
@@ -0,0 +1,16 @@
1
+ require 'test_helper'
2
+ require 'rails/generators/test_case'
3
+ require 'generators/devise-security/install_generator'
4
+
5
+ class TestInstallGenerator < Rails::Generators::TestCase
6
+ tests DeviseSecurity::Generators::InstallGenerator
7
+ destination File.expand_path('../tmp', __FILE__)
8
+ setup :prepare_destination
9
+
10
+ test 'Assert all files are properly created' do
11
+ run_generator
12
+ assert_file 'config/initializers/devise-security.rb'
13
+ assert_file 'config/locales/devise.security_extension.en.yml'
14
+ assert_file 'config/locales/devise.security_extension.de.yml'
15
+ end
16
+ end
@@ -0,0 +1,124 @@
1
+ require 'test_helper'
2
+
3
+ class TestParanoidVerification < ActiveSupport::TestCase
4
+ test 'need to paranoid verify if code present' do
5
+ user = User.new
6
+ user.generate_paranoid_code
7
+ assert_equal(true, user.need_paranoid_verification?)
8
+ end
9
+
10
+ test 'no need to paranoid verify if no code' do
11
+ user = User.new
12
+ assert_equal(false, user.need_paranoid_verification?)
13
+ end
14
+
15
+ test 'generate code' do
16
+ user = User.new
17
+ user.generate_paranoid_code
18
+ assert_equal(0, user.paranoid_verification_attempt)
19
+ user.verify_code('wrong')
20
+ assert_equal(1, user.paranoid_verification_attempt)
21
+ user.generate_paranoid_code
22
+ assert_equal(0, user.paranoid_verification_attempt)
23
+ end
24
+
25
+ test "generate code must reset attempt counter" do
26
+ user = User.new
27
+ user.generate_paranoid_code
28
+ # default generator generates 5 char string
29
+ assert_equal(user.paranoid_verification_code.class, String)
30
+ assert_equal(user.paranoid_verification_code.length, 5)
31
+ end
32
+
33
+ test "when code match upon verify code, should mark record that it's no loger needed to verify" do
34
+ user = User.new(paranoid_verification_code: 'abcde')
35
+
36
+ assert_equal(true, user.need_paranoid_verification?)
37
+ user.verify_code('abcde')
38
+ assert_equal(false, user.need_paranoid_verification?)
39
+ end
40
+
41
+ test 'when code match upon verify code, should no longer need verification' do
42
+ user = User.new(paranoid_verification_code: 'abcde')
43
+
44
+ assert_equal(true, user.need_paranoid_verification?)
45
+ user.verify_code('abcde')
46
+ assert_equal(false, user.need_paranoid_verification?)
47
+ end
48
+
49
+ test 'when code match upon verification code, should set when verification was accepted' do
50
+ user = User.new(paranoid_verification_code: 'abcde')
51
+ user.verify_code('abcde')
52
+ assert_in_delta(4, Time.now.to_i, user.paranoid_verified_at.to_i)
53
+ end
54
+
55
+ test 'when code not match upon verify code, should still need verification' do
56
+ user = User.new(paranoid_verification_code: 'abcde')
57
+ user.verify_code('wrong')
58
+ assert_equal(true, user.need_paranoid_verification?)
59
+ end
60
+
61
+ test 'when code not match upon verification code, should not set paranoid_verified_at' do
62
+ user = User.new(paranoid_verification_code: 'abcde')
63
+ user.verify_code('wrong')
64
+ assert_nil(user.paranoid_verified_at)
65
+ end
66
+
67
+ test 'when code not match upon verification code too many attempts should generate new code' do
68
+ original_regenerate = Devise.paranoid_code_regenerate_after_attempt
69
+ Devise.paranoid_code_regenerate_after_attempt = 2
70
+
71
+ user = User.create(paranoid_verification_code: 'abcde')
72
+ user.verify_code('wrong')
73
+ assert_equal 'abcde', user.paranoid_verification_code
74
+ user.verify_code('wrong-again')
75
+ assert_not_equal 'abcde', user.paranoid_verification_code
76
+
77
+ Devise.paranoid_code_regenerate_after_attempt = original_regenerate
78
+ end
79
+
80
+ test 'upon generating new code due to too many attempts reset attempt counter' do
81
+ original_regenerate = Devise.paranoid_code_regenerate_after_attempt
82
+ Devise.paranoid_code_regenerate_after_attempt = 3
83
+
84
+ user = User.create(paranoid_verification_code: 'abcde')
85
+ user.verify_code('wrong')
86
+ assert_equal 1, user.paranoid_verification_attempt
87
+ user.verify_code('wrong-again')
88
+ assert_equal 2, user.paranoid_verification_attempt
89
+ user.verify_code('WRONG!')
90
+ assert_equal 0, user.paranoid_verification_attempt
91
+
92
+ Devise.paranoid_code_regenerate_after_attempt = original_regenerate
93
+ end
94
+
95
+
96
+ test 'by default paranoid code regenerate should have 10 attempts' do
97
+ user = User.new(paranoid_verification_code: 'abcde')
98
+ assert_equal 10, user.paranoid_attempts_remaining
99
+ end
100
+
101
+ test 'paranoid_attempts_remaining should re-callculate how many attemps remains after each wrong attempt' do
102
+ original_regenerate = Devise.paranoid_code_regenerate_after_attempt
103
+ Devise.paranoid_code_regenerate_after_attempt = 2
104
+
105
+ user = User.create(paranoid_verification_code: 'abcde')
106
+ assert_equal 2, user.paranoid_attempts_remaining
107
+
108
+ user.verify_code('WRONG!')
109
+ assert_equal 1, user.paranoid_attempts_remaining
110
+
111
+ Devise.paranoid_code_regenerate_after_attempt = original_regenerate
112
+ end
113
+
114
+ test 'when code not match upon verification code too many times, reset paranoid_attempts_remaining' do
115
+ original_regenerate = Devise.paranoid_code_regenerate_after_attempt
116
+ Devise.paranoid_code_regenerate_after_attempt = 1
117
+
118
+ user = User.create(paranoid_verification_code: 'abcde')
119
+ user.verify_code('wrong') # at this point code was regenerated
120
+ assert_equal Devise.paranoid_code_regenerate_after_attempt, user.paranoid_attempts_remaining
121
+
122
+ Devise.paranoid_code_regenerate_after_attempt = original_regenerate
123
+ end
124
+ end