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,26 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), 'lib')
2
+ require 'rubygems'
3
+ require 'bundler'
4
+ require 'rake/testtask'
5
+ require 'rdoc/task'
6
+ require 'devise-security/version'
7
+
8
+ desc 'Default: Run DeviseSecurity unit tests'
9
+ task default: :test
10
+
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << 'lib'
13
+ t.libs << 'test'
14
+ t.test_files = FileList['test/*test*.rb']
15
+ t.verbose = true
16
+ t.warning = false
17
+ end
18
+
19
+ Rake::RDocTask.new do |rdoc|
20
+ version = DeviseSecurity::VERSION.dup
21
+
22
+ rdoc.rdoc_dir = 'rdoc'
23
+ rdoc.title = "devise-security #{version}"
24
+ rdoc.rdoc_files.include('README*')
25
+ rdoc.rdoc_files.include('lib/**/*.rb')
26
+ end
@@ -0,0 +1,42 @@
1
+ class Devise::ParanoidVerificationCodeController < DeviseController
2
+ skip_before_action :handle_paranoid_verification
3
+ prepend_before_action :authenticate_scope!, :only => [:show, :update]
4
+
5
+ def show
6
+ if !resource.nil? && resource.need_paranoid_verification?
7
+ respond_with(resource)
8
+ else
9
+ redirect_to :root
10
+ end
11
+ end
12
+
13
+ def update
14
+ if resource.verify_code(resource_params[:paranoid_verification_code])
15
+ warden.session(scope)['paranoid_verify'] = false
16
+ set_flash_message :notice, :updated
17
+ bypass_sign_in resource, scope: scope
18
+ redirect_to stored_location_for(scope) || :root
19
+ else
20
+ respond_with(resource, action: :show)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def resource_params
27
+ if params.respond_to?(:permit)
28
+ params.require(resource_name).permit(:paranoid_verification_code)
29
+ else
30
+ params[scope].slice(:paranoid_verification_code)
31
+ end
32
+ end
33
+
34
+ def scope
35
+ resource_name.to_sym
36
+ end
37
+
38
+ def authenticate_scope!
39
+ send(:"authenticate_#{resource_name}!")
40
+ self.resource = send("current_#{resource_name}")
41
+ end
42
+ end
@@ -0,0 +1,48 @@
1
+ class Devise::PasswordExpiredController < DeviseController
2
+ skip_before_action :handle_password_change
3
+ before_action :skip_password_change, only: [:show, :update]
4
+ prepend_before_action :authenticate_scope!, :only => [:show, :update]
5
+
6
+ def show
7
+ respond_with(resource)
8
+ end
9
+
10
+ def update
11
+ resource.extend(Devise::Models::DatabaseAuthenticatablePatch)
12
+ if resource.update_with_password(resource_params)
13
+ warden.session(scope)['password_expired'] = false
14
+ set_flash_message :notice, :updated
15
+ bypass_sign_in resource, scope: scope
16
+ redirect_to stored_location_for(scope) || :root
17
+ else
18
+ clean_up_passwords(resource)
19
+ respond_with(resource, action: :show)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def skip_password_change
26
+ return if !resource.nil? && resource.need_change_password?
27
+ redirect_to :root
28
+ end
29
+
30
+ def resource_params
31
+ permitted_params = [:current_password, :password, :password_confirmation]
32
+
33
+ if params.respond_to?(:permit)
34
+ params.require(resource_name).permit(*permitted_params)
35
+ else
36
+ params[scope].slice(*permitted_params)
37
+ end
38
+ end
39
+
40
+ def scope
41
+ resource_name.to_sym
42
+ end
43
+
44
+ def authenticate_scope!
45
+ send(:"authenticate_#{resource_name}!")
46
+ self.resource = send("current_#{resource_name}")
47
+ end
48
+ end
@@ -0,0 +1,10 @@
1
+ <h2>Submit verification code</h2>
2
+
3
+ <%= form_for(resource, :as => resource_name, :url => [resource_name, :paranoid_verification_code], :html => { :method => :put }) do |f| %>
4
+ <%= devise_error_messages! %>
5
+
6
+ <p><%= f.label :paranoid_verification_code, 'Verification code' %><br />
7
+ <%= f.text_field :paranoid_verification_code, value: '' %></p>
8
+
9
+ <p><%= f.submit "Submit" %></p>
10
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <h2>Renew your password</h2>
2
+
3
+ <%= form_for(resource, :as => resource_name, :url => [resource_name, :password_expired], :html => { :method => :put }) do |f| %>
4
+ <%= devise_error_messages! %>
5
+
6
+ <p><%= f.label :current_password, "Current password" %><br />
7
+ <%= f.password_field :current_password %></p>
8
+
9
+ <p><%= f.label :password, "New password" %><br />
10
+ <%= f.password_field :password %></p>
11
+
12
+ <p><%= f.label :password_confirmation, "Confirm new password" %><br />
13
+ <%= f.password_field :password_confirmation %></p>
14
+
15
+ <p><%= f.submit "Change my password" %></p>
16
+ <% end %>
@@ -0,0 +1,16 @@
1
+ de:
2
+ errors:
3
+ messages:
4
+ taken_in_past: "wurde bereits in der Vergangenheit verwendet!"
5
+ equal_to_current_password: "darf nicht dem aktuellen Passwort entsprechen!"
6
+ password_format: "müssen große, kleine Buchstaben und Ziffern enthalten"
7
+ devise:
8
+ invalid_captcha: "Die Captchaeingabe ist nicht gültig!"
9
+ paranoid_verify:
10
+ code_required: "Bitte geben Sie den Code unser Support-Team zur Verfügung gestellt"
11
+ password_expired:
12
+ updated: "Das neue Passwort wurde übernommen."
13
+ change_required: "Ihr Passwort ist abgelaufen. Bitte vergeben sie ein neues Passwort!"
14
+ failure:
15
+ session_limited: 'Ihre Anmeldedaten wurden in einem anderen Browser genutzt. Bitte melden Sie sich erneut an, um in diesem Browser fortzufahren.'
16
+ expired: 'Ihr Account ist aufgrund zu langer Inaktiviät abgelaufen. Bitte kontaktieren Sie den Administrator.'
@@ -0,0 +1,17 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ taken_in_past: "was used previously."
5
+ equal_to_current_password: "must be different than the current password."
6
+ password_format: "must contain big, small letters and digits"
7
+ devise:
8
+ invalid_captcha: "The captcha input was invalid."
9
+ invalid_security_question: "The security question answer was invalid."
10
+ paranoid_verify:
11
+ code_required: "Please enter the code our support team provided"
12
+ password_expired:
13
+ updated: "Your new password is saved."
14
+ change_required: "Your password is expired. Please renew your password."
15
+ failure:
16
+ session_limited: 'Your login credentials were used in another browser. Please sign in again to continue in this browser.'
17
+ expired: 'Your account has expired due to inactivity. Please contact the site administrator.'
@@ -0,0 +1,17 @@
1
+ es:
2
+ errors:
3
+ messages:
4
+ taken_in_past: "la contraseña fue usada previamente, favor elegir otra."
5
+ equal_to_current_password: "tiene que ser diferente a la contraseña actual."
6
+ password_format: "tiene que contener mayúsculas, minúsculas y digitos "
7
+ devise:
8
+ invalid_captcha: "El captcha ingresado es inválido."
9
+ invalid_security_question: "La respuesta a la pregunta de suguridad fue incorrecta."
10
+ paranoid_verify:
11
+ code_required: "Por favor ingrese el código provisto por nuestro equipo de soporte"
12
+ password_expired:
13
+ updated: "Su nueva contraña ha sido guardada."
14
+ change_required: "Su contraña ha expirado. Por favor renueve su contraseña."
15
+ failure:
16
+ session_limited: 'Sus credenciales de inicio de sesión fueron usadas en otro navegador. Por favor inicie sesión nuevamente para continuar en éste navegador.'
17
+ expired: 'Su cuenta ha expirado debido a inactividad. Por favor contacte al administrador de la aplicación.'
@@ -0,0 +1,10 @@
1
+ it:
2
+ errors:
3
+ messages:
4
+ taken_in_past: "e' stata gia' utilizzata in passato!"
5
+ equal_to_current_password: " deve essere differente dalla password corrente!"
6
+ devise:
7
+ invalid_captcha: "Il captcha inserito non e' valido!"
8
+ password_expired:
9
+ updated: "La tua nuova password e' stata salvata."
10
+ change_required: "La tua password e' scaduta. Si prega di rinnovarla!"
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
3
+ require 'devise-security/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'devise-security'
7
+ s.version = DeviseSecurity::VERSION.dup
8
+ s.platform = Gem::Platform::RUBY
9
+ s.licenses = ['MIT']
10
+ s.summary = 'Security extension for devise'
11
+ s.email = 'natebird@gmail.com'
12
+ s.homepage = 'https://github.com/devise-security/devise-security'
13
+ s.description = 'An enterprise security extension for devise, trying to meet industrial standard security demands for web applications.'
14
+ s.authors = ['Marco Scholl', 'Alexander Dreher', 'Nate Bird']
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- test/*`.split("\n")
18
+ s.require_paths = ['lib']
19
+ s.required_ruby_version = '>= 2.1.0'
20
+
21
+ if RUBY_VERSION >= '2.4'
22
+ s.add_runtime_dependency 'rails', '>= 4.2.8', '< 6.0'
23
+ else
24
+ s.add_runtime_dependency 'railties', '>= 3.2.6', '< 6.0'
25
+ end
26
+ s.add_runtime_dependency 'devise', '>= 3.0.0', '< 5.0'
27
+ s.add_development_dependency 'bundler', '>= 1.3.0', '< 2.0'
28
+ s.add_development_dependency 'sqlite3', '~> 1.3', '>= 1.3.10'
29
+ s.add_development_dependency 'rubocop', '~> 0'
30
+ s.add_development_dependency 'minitest', '~> 5.0'
31
+ s.add_development_dependency 'easy_captcha', '~> 0'
32
+ s.add_development_dependency 'rails_email_validator', '~> 0'
33
+ s.add_development_dependency 'coveralls', '~> 0.8'
34
+ end
@@ -0,0 +1,106 @@
1
+ require 'active_record'
2
+ require 'active_support/core_ext/integer'
3
+ require 'active_support/ordered_hash'
4
+ require 'active_support/concern'
5
+ require 'devise'
6
+
7
+ module Devise
8
+
9
+ # Should the password expire (e.g 3.months)
10
+ mattr_accessor :expire_password_after
11
+ @@expire_password_after = 3.months
12
+
13
+ # Validate password for strongness
14
+ mattr_accessor :password_regex
15
+ @@password_regex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/
16
+
17
+ # Number of old passwords in archive
18
+ mattr_accessor :password_archiving_count
19
+ @@password_archiving_count = 5
20
+
21
+ # Deny old password (true, false, count)
22
+ mattr_accessor :deny_old_passwords
23
+ @@deny_old_passwords = true
24
+
25
+ # enable email validation for :secure_validatable. (true, false, validation_options)
26
+ # dependency: need an email validator like rails_email_validator
27
+ mattr_accessor :email_validation
28
+ @@email_validation = true
29
+
30
+ # captcha integration for recover form
31
+ mattr_accessor :captcha_for_recover
32
+ @@captcha_for_recover = false
33
+
34
+ # captcha integration for sign up form
35
+ mattr_accessor :captcha_for_sign_up
36
+ @@captcha_for_sign_up = false
37
+
38
+ # captcha integration for sign in form
39
+ mattr_accessor :captcha_for_sign_in
40
+ @@captcha_for_sign_in = false
41
+
42
+ # captcha integration for unlock form
43
+ mattr_accessor :captcha_for_unlock
44
+ @@captcha_for_unlock = false
45
+
46
+ # security_question integration for recover form
47
+ # this automatically enables captchas (captcha_for_recover, as fallback)
48
+ mattr_accessor :security_question_for_recover
49
+ @@security_question_for_recover = false
50
+
51
+ # security_question integration for unlock form
52
+ # this automatically enables captchas (captcha_for_unlock, as fallback)
53
+ mattr_accessor :security_question_for_unlock
54
+ @@security_question_for_unlock = false
55
+
56
+ # security_question integration for confirmation form
57
+ # this automatically enables captchas (captcha_for_confirmation, as fallback)
58
+ mattr_accessor :security_question_for_confirmation
59
+ @@security_question_for_confirmation = false
60
+
61
+ # captcha integration for confirmation form
62
+ mattr_accessor :captcha_for_confirmation
63
+ @@captcha_for_confirmation = false
64
+
65
+ # captcha integration for confirmation form
66
+ mattr_accessor :verification_code_generator
67
+ @@verification_code_generator = -> { SecureRandom.hex[0..4] }
68
+
69
+ # Time period for account expiry from last_activity_at
70
+ mattr_accessor :expire_after
71
+ @@expire_after = 90.days
72
+ mattr_accessor :delete_expired_after
73
+ @@delete_expired_after = 90.days
74
+
75
+ # paranoid_verification will regenerate verifacation code after faild attempt
76
+ mattr_accessor :paranoid_code_regenerate_after_attempt
77
+ @@paranoid_code_regenerate_after_attempt = 10
78
+ end
79
+
80
+ # an security extension for devise
81
+ module DeviseSecurity
82
+ autoload :Schema, 'devise-security/schema'
83
+ autoload :Patches, 'devise-security/patches'
84
+
85
+ module Controllers
86
+ autoload :Helpers, 'devise-security/controllers/helpers'
87
+ end
88
+ end
89
+
90
+ # modules
91
+ Devise.add_module :password_expirable, controller: :password_expirable, model: 'devise-security/models/password_expirable', route: :password_expired
92
+ Devise.add_module :secure_validatable, model: 'devise-security/models/secure_validatable'
93
+ Devise.add_module :password_archivable, model: 'devise-security/models/password_archivable'
94
+ Devise.add_module :session_limitable, model: 'devise-security/models/session_limitable'
95
+ Devise.add_module :session_non_transferable, model: 'devise-security/models/session_non_transferable'
96
+ Devise.add_module :expirable, model: 'devise-security/models/expirable'
97
+ Devise.add_module :security_questionable, model: 'devise-security/models/security_questionable'
98
+ Devise.add_module :paranoid_verification, controller: :paranoid_verification_code, model: 'devise-security/models/paranoid_verification', route: :verification_code
99
+
100
+ # requires
101
+ require 'devise-security/routes'
102
+ require 'devise-security/rails'
103
+ require 'devise-security/orm/active_record'
104
+ require 'devise-security/models/old_password'
105
+ require 'devise-security/models/database_authenticatable_patch'
106
+ require 'devise-security/models/paranoid_verification'
@@ -0,0 +1,96 @@
1
+ module DeviseSecurity
2
+ module Controllers
3
+ module Helpers
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_action :handle_password_change
8
+ before_action :handle_paranoid_verification
9
+ end
10
+
11
+ module ClassMethods
12
+ # helper for captcha
13
+ def init_recover_password_captcha
14
+ include RecoverPasswordCaptcha
15
+ end
16
+ end
17
+
18
+ module RecoverPasswordCaptcha
19
+ def new
20
+ super
21
+ end
22
+ end
23
+
24
+ # controller instance methods
25
+
26
+ private
27
+
28
+ # lookup if an password change needed
29
+ def handle_password_change
30
+ return if warden.nil?
31
+
32
+ if not devise_controller? and not ignore_password_expire? and not request.format.nil? and request.format.html?
33
+ Devise.mappings.keys.flatten.any? do |scope|
34
+ if signed_in?(scope) and warden.session(scope)['password_expired']
35
+ # re-check to avoid infinite loop if date changed after login attempt
36
+ if send(:"current_#{scope}").try(:need_change_password?)
37
+ store_location_for(scope, request.original_fullpath) if request.get?
38
+ redirect_for_password_change scope
39
+ return
40
+ else
41
+ warden.session(scope)[:password_expired] = false
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ # lookup if extra (paranoid) code verification is needed
49
+ def handle_paranoid_verification
50
+ return if warden.nil?
51
+
52
+ if !devise_controller? && !request.format.nil? && request.format.html?
53
+ Devise.mappings.keys.flatten.any? do |scope|
54
+ if signed_in?(scope) && warden.session(scope)['paranoid_verify']
55
+ store_location_for(scope, request.original_fullpath) if request.get?
56
+ redirect_for_paranoid_verification scope
57
+ return
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ # redirect for password update with alert message
64
+ def redirect_for_password_change(scope)
65
+ redirect_to change_password_required_path_for(scope), :alert => I18n.t('change_required', {:scope => 'devise.password_expired'})
66
+ end
67
+
68
+ def redirect_for_paranoid_verification(scope)
69
+ redirect_to paranoid_verification_code_path_for(scope), :alert => I18n.t('code_required', {:scope => 'devise.paranoid_verify'})
70
+ end
71
+
72
+ # path for change password
73
+ def change_password_required_path_for(resource_or_scope = nil)
74
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
75
+ change_path = "#{scope}_password_expired_path"
76
+ send(change_path)
77
+ end
78
+
79
+ def paranoid_verification_code_path_for(resource_or_scope = nil)
80
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
81
+ change_path = "#{scope}_paranoid_verification_code_path"
82
+ send(change_path)
83
+ end
84
+
85
+ protected
86
+
87
+ # allow to overwrite for some special handlings
88
+ def ignore_password_expire?
89
+ false
90
+ end
91
+
92
+
93
+ end
94
+ end
95
+
96
+ end