devise-security 0.11.0

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