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,18 @@
1
+ module Devise
2
+ module Models
3
+ # SecurityQuestionable is an accessible add-on for visually handicapped people,
4
+ # to ship around the captcha with screenreader compatibility.
5
+ #
6
+ # You need to add two text_field_tags to the associated forms (unlock,
7
+ # password, confirmation):
8
+ # :security_question_answer and :captcha
9
+ #
10
+ # And add the security_question to the register/edit form.
11
+ # f.select :security_question_id, SecurityQuestion.where(locale: I18n.locale).map{|s| [s.name, s.id]}
12
+ # f.text_field :security_question_answer
13
+ module SecurityQuestionable
14
+ extend ActiveSupport::Concern
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ require 'devise-security/hooks/session_limitable'
2
+
3
+ module Devise
4
+ module Models
5
+ # SessionLimited ensures, that there is only one session usable per account at once.
6
+ # If someone logs in, and some other is logging in with the same credentials,
7
+ # the session from the first one is invalidated and not usable anymore.
8
+ # The first one is redirected to the sign page with a message, telling that
9
+ # someone used his credentials to sign in.
10
+ module SessionLimitable
11
+ extend ActiveSupport::Concern
12
+
13
+ def update_unique_session_id!(unique_session_id)
14
+ self.unique_session_id = unique_session_id
15
+
16
+ save(:validate => false)
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ module DeviseSecurity
2
+ module Orm
3
+ # This module contains some helpers and handle schema (migrations):
4
+ #
5
+ # create_table :accounts do |t|
6
+ # t.password_expirable
7
+ # end
8
+ #
9
+ module ActiveRecord
10
+ module Schema
11
+ include DeviseSecurity::Schema
12
+
13
+
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ ActiveRecord::ConnectionAdapters::Table.send :include, DeviseSecurity::Orm::ActiveRecord::Schema
20
+ ActiveRecord::ConnectionAdapters::TableDefinition.send :include, DeviseSecurity::Orm::ActiveRecord::Schema
@@ -0,0 +1,21 @@
1
+ module DeviseSecurity
2
+ module Patches
3
+ autoload :ControllerCaptcha, 'devise-security/patches/controller_captcha'
4
+ autoload :ControllerSecurityQuestion, 'devise-security/patches/controller_security_question'
5
+
6
+ class << self
7
+ def apply
8
+ Devise::PasswordsController.send(:include, Patches::ControllerCaptcha) if Devise.captcha_for_recover || Devise.security_question_for_recover
9
+ Devise::UnlocksController.send(:include, Patches::ControllerCaptcha) if Devise.captcha_for_unlock || Devise.security_question_for_unlock
10
+ Devise::ConfirmationsController.send(:include, Patches::ControllerCaptcha) if Devise.captcha_for_confirmation
11
+
12
+ Devise::PasswordsController.send(:include, Patches::ControllerSecurityQuestion) if Devise.security_question_for_recover
13
+ Devise::UnlocksController.send(:include, Patches::ControllerSecurityQuestion) if Devise.security_question_for_unlock
14
+ Devise::ConfirmationsController.send(:include, Patches::ControllerSecurityQuestion) if Devise.security_question_for_confirmation
15
+
16
+ Devise::RegistrationsController.send(:include, Patches::ControllerCaptcha) if Devise.captcha_for_sign_up
17
+ Devise::SessionsController.send(:include, Patches::ControllerCaptcha) if Devise.captcha_for_sign_in
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module DeviseSecurity::Patches
2
+ module ConfirmationsControllerCaptcha
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ define_method :create do
6
+ if ((defined? verify_recaptcha) && (verify_recaptcha)) or ((defined? valid_captcha?) && (valid_captcha? params[:captcha]))
7
+ self.resource = resource_class.send_confirmation_instructions(params[resource_name])
8
+
9
+ if successfully_sent?(resource)
10
+ respond_with({}, :location => after_resending_confirmation_instructions_path_for(resource_name))
11
+ else
12
+ respond_with(resource)
13
+ end
14
+ else
15
+ flash[:alert] = t('devise.invalid_captcha') if is_navigational_format?
16
+ respond_with({}, :location => new_confirmation_path(resource_name))
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ module DeviseSecurity::Patches
2
+ module ConfirmationsControllerSecurityQuestion
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ define_method :create do
6
+ # only find via email, not login
7
+ resource = resource_class.find_or_initialize_with_error_by(:email, params[resource_name][:email], :not_found)
8
+
9
+ if ((defined? verify_recaptcha) && (verify_recaptcha)) or ((defined? valid_captcha?) && (valid_captcha? params[:captcha])) or
10
+ (resource.security_question_answer.present? and resource.security_question_answer == params[:security_question_answer])
11
+ self.resource = resource_class.send_confirmation_instructions(params[resource_name])
12
+
13
+ if successfully_sent?(resource)
14
+ respond_with({}, :location => after_resending_confirmation_instructions_path_for(resource_name))
15
+ else
16
+ respond_with(resource)
17
+ end
18
+ else
19
+ flash[:alert] = t('devise.invalid_security_question') if is_navigational_format?
20
+ respond_with({}, :location => new_confirmation_path(resource_name))
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ module DeviseSecurity::Patches
2
+ module ControllerCaptcha
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ prepend_before_action :check_captcha, only: [:create]
7
+ end
8
+
9
+ private
10
+ def check_captcha
11
+ return if ((defined? verify_recaptcha) && (verify_recaptcha)) || ((defined? valid_captcha?) && (valid_captcha? params[:captcha]))
12
+
13
+ flash[:alert] = t('devise.invalid_captcha') if is_navigational_format?
14
+ respond_with({}, location: url_for(action: :new))
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module DeviseSecurity::Patches
2
+ module ControllerSecurityQuestion
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ prepend_before_action :check_security_question, only: [:create]
7
+ end
8
+
9
+ private
10
+ def check_security_question
11
+ # only find via email, not login
12
+ resource = resource_class.find_or_initialize_with_error_by(:email, params[resource_name][:email], :not_found)
13
+ return if (resource.security_question_answer.present? && resource.security_question_answer == params[:security_question_answer])
14
+
15
+ flash[:alert] = t('devise.invalid_security_question') if is_navigational_format?
16
+ respond_with({}, location: url_for(action: :new))
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,20 @@
1
+ module DeviseSecurity::Patches
2
+ module PasswordsControllerCaptcha
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ define_method :create do
6
+ if ((defined? verify_recaptcha) && (verify_recaptcha)) or ((defined? valid_captcha?) && (valid_captcha? params[:captcha]))
7
+ self.resource = resource_class.send_reset_password_instructions(params[resource_name])
8
+ if successfully_sent?(resource)
9
+ respond_with({}, :location => new_session_path(resource_name))
10
+ else
11
+ respond_with(resource)
12
+ end
13
+ else
14
+ flash[:alert] = t('devise.invalid_captcha') if is_navigational_format?
15
+ respond_with({}, :location => new_password_path(resource_name))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module DeviseSecurity::Patches
2
+ module PasswordsControllerSecurityQuestion
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ define_method :create do
6
+ # only find via email, not login
7
+ resource = resource_class.find_or_initialize_with_error_by(:email, params[resource_name][:email], :not_found)
8
+
9
+ if ((defined? verify_recaptcha) && (verify_recaptcha)) or ((defined? valid_captcha?) && (valid_captcha? params[:captcha]))
10
+ (resource.security_question_answer.present? and resource.security_question_answer == params[:security_question_answer])
11
+ self.resource = resource_class.send_reset_password_instructions(params[resource_name])
12
+ if successfully_sent?(resource)
13
+ respond_with({}, :location => new_session_path(resource_name))
14
+ else
15
+ respond_with(resource)
16
+ end
17
+ else
18
+ flash[:alert] = t('devise.invalid_security_question') if is_navigational_format?
19
+ respond_with({}, :location => new_password_path(resource_name))
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ module DeviseSecurity::Patches
2
+ module RegistrationsControllerCaptcha
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ define_method :create do |&block|
6
+ build_resource(sign_up_params)
7
+
8
+ if ((defined? verify_recaptcha) && (verify_recaptcha)) or ((defined? valid_captcha?) && (valid_captcha? params[:captcha]))
9
+ if resource.save
10
+ block.call(resource) if block
11
+ if resource.active_for_authentication?
12
+ set_flash_message :notice, :signed_up if is_flashing_format?
13
+ sign_up(resource_name, resource)
14
+ respond_with resource, :location => after_sign_up_path_for(resource)
15
+ else
16
+ set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_flashing_format?
17
+ expire_data_after_sign_in!
18
+ respond_with resource, :location => after_inactive_sign_up_path_for(resource)
19
+ end
20
+ else
21
+ clean_up_passwords resource
22
+ respond_with resource
23
+ end
24
+
25
+ else
26
+ resource.errors.add :base, t('devise.invalid_captcha')
27
+ clean_up_passwords resource
28
+ respond_with resource
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ module DeviseSecurity::Patches
2
+ module SessionsControllerCaptcha
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ define_method :create do |&block|
6
+ if ((defined? verify_recaptcha) && (verify_recaptcha)) or ((defined? valid_captcha?) && (valid_captcha? params[:captcha]))
7
+ self.resource = warden.authenticate!(auth_options)
8
+ set_flash_message(:notice, :signed_in) if is_flashing_format?
9
+ sign_in(resource_name, resource)
10
+ block.call(resource) if block
11
+ respond_with resource, :location => after_sign_in_path_for(resource)
12
+ else
13
+ flash[:alert] = t('devise.invalid_captcha') if is_flashing_format?
14
+ respond_with({}, :location => new_session_path(resource_name))
15
+ end
16
+ end
17
+
18
+ # for bad protected use in controller
19
+ define_method :auth_options do
20
+ { :scope => resource_name, :recall => "#{controller_path}#new" }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ module DeviseSecurity::Patches
2
+ module UnlocksControllerCaptcha
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ define_method :create do
6
+ if ((defined? verify_recaptcha) && (verify_recaptcha)) or ((defined? valid_captcha?) && (valid_captcha? params[:captcha]))
7
+ self.resource = resource_class.send_unlock_instructions(params[resource_name])
8
+ if successfully_sent?(resource)
9
+ respond_with({}, :location => new_session_path(resource_name))
10
+ else
11
+ respond_with(resource)
12
+ end
13
+ else
14
+ flash[:alert] = t('devise.invalid_captcha') if is_navigational_format?
15
+ respond_with({}, :location => new_unlock_path(resource_name))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module DeviseSecurity::Patches
2
+ module UnlocksControllerSecurityQuestion
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ define_method :create do
6
+ # only find via email, not login
7
+ resource = resource_class.find_or_initialize_with_error_by(:email, params[resource_name][:email], :not_found)
8
+
9
+ if ((defined? verify_recaptcha) && (verify_recaptcha)) or ((defined? valid_captcha?) && (valid_captcha? params[:captcha]))
10
+ (resource.security_question_answer.present? and resource.security_question_answer == params[:security_question_answer])
11
+ self.resource = resource_class.send_unlock_instructions(params[resource_name])
12
+ if successfully_sent?(resource)
13
+ respond_with({}, :location => new_session_path(resource_name))
14
+ else
15
+ respond_with(resource)
16
+ end
17
+ else
18
+ flash[:alert] = t('devise.invalid_security_question') if is_navigational_format?
19
+ respond_with({}, :location => new_unlock_path(resource_name))
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module DeviseSecurity
2
+ class Engine < ::Rails::Engine
3
+ ActiveSupport.on_load(:action_controller) do
4
+ include DeviseSecurity::Controllers::Helpers
5
+ end
6
+
7
+ if Rails.version > "5"
8
+ ActiveSupport::Reloader.to_prepare do
9
+ DeviseSecurity::Patches.apply
10
+ end
11
+ else
12
+ ActionDispatch::Callbacks.to_prepare do
13
+ DeviseSecurity::Patches.apply
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module ActionDispatch::Routing
2
+ class Mapper
3
+
4
+ protected
5
+
6
+ # route for handle expired passwords
7
+ def devise_password_expired(mapping, controllers)
8
+ resource :password_expired, :only => [:show, :update], :path => mapping.path_names[:password_expired], :controller => controllers[:password_expired]
9
+ end
10
+
11
+ # route for handle paranoid verification
12
+ def devise_verification_code(mapping, controllers)
13
+ resource :paranoid_verification_code, :only => [:show, :update], :path => mapping.path_names[:verification_code], :controller => controllers[:paranoid_verification_code]
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,59 @@
1
+ module DeviseSecurity
2
+ # add schema helper for migrations
3
+ module Schema
4
+ # Add password_changed_at columns in the resource's database table.
5
+ #
6
+ # Examples
7
+ #
8
+ # # For a new resource migration:
9
+ # create_table :the_resources do |t|
10
+ # t.password_expirable
11
+ # ...
12
+ # end
13
+ #
14
+ # # or if the resource's table already exists, define a migration and put this in:
15
+ # change_table :the_resources do |t|
16
+ # t.datetime :password_changed_at
17
+ # end
18
+ #
19
+ def password_expirable
20
+ apply_devise_schema :password_changed_at, DateTime
21
+ end
22
+
23
+ # Add password_archivable columns
24
+ #
25
+ # Examples
26
+ #
27
+ # create_table :old_passwords do
28
+ # t.password_archivable
29
+ # end
30
+ # add_index :old_passwords, [:password_archivable_type, :password_archivable_id], :name => :index_password_archivable
31
+ #
32
+ def password_archivable
33
+ apply_devise_schema :encrypted_password, String, :limit => 128, :null => false
34
+ apply_devise_schema :password_salt, String
35
+ apply_devise_schema :password_archivable_id, Integer, :null => false
36
+ apply_devise_schema :password_archivable_type, String, :null => false
37
+ apply_devise_schema :created_at, DateTime
38
+ end
39
+
40
+ # Add session_limitable columns in the resource's database table.
41
+ #
42
+ # Examples
43
+ #
44
+ # # For a new resource migration:
45
+ # create_table :the_resources do |t|
46
+ # t.session_limitable
47
+ # ...
48
+ # end
49
+ #
50
+ # # or if the resource's table already exists, define a migration and put this in:
51
+ # change_table :the_resources do |t|
52
+ # t.string :unique_session_id, :limit => 20
53
+ # end
54
+ #
55
+ def session_limitable
56
+ apply_devise_schema :unique_session_id, String, :limit => 20
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module DeviseSecurity
2
+ VERSION = "0.11.0".freeze
3
+ end
@@ -0,0 +1,26 @@
1
+ module DeviseSecurity
2
+ module Generators
3
+ # Generator for Rails to create or append to a Devise initializer.
4
+ class InstallGenerator < Rails::Generators::Base
5
+ LOCALES = %w[ en de it ]
6
+
7
+ source_root File.expand_path('../../templates', __FILE__)
8
+ desc 'Install the devise security extension'
9
+
10
+ def copy_initializer
11
+ template('devise-security.rb',
12
+ 'config/initializers/devise-security.rb',
13
+ )
14
+ end
15
+
16
+ def copy_locales
17
+ LOCALES.each do |locale|
18
+ copy_file(
19
+ "../../../config/locales/#{locale}.yml",
20
+ "config/locales/devise.security_extension.#{locale}.yml",
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end