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.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gitignore +38 -0
- data/.rubocop.yml +42 -0
- data/.travis.yml +14 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +199 -0
- data/LICENSE.txt +20 -0
- data/README.md +263 -0
- data/Rakefile +26 -0
- data/app/controllers/devise/paranoid_verification_code_controller.rb +42 -0
- data/app/controllers/devise/password_expired_controller.rb +48 -0
- data/app/views/devise/paranoid_verification_code/show.html.erb +10 -0
- data/app/views/devise/password_expired/show.html.erb +16 -0
- data/config/locales/de.yml +16 -0
- data/config/locales/en.yml +17 -0
- data/config/locales/es.yml +17 -0
- data/config/locales/it.yml +10 -0
- data/devise-security.gemspec +34 -0
- data/lib/devise-security.rb +106 -0
- data/lib/devise-security/controllers/helpers.rb +96 -0
- data/lib/devise-security/hooks/expirable.rb +10 -0
- data/lib/devise-security/hooks/paranoid_verification.rb +5 -0
- data/lib/devise-security/hooks/password_expirable.rb +5 -0
- data/lib/devise-security/hooks/session_limitable.rb +27 -0
- data/lib/devise-security/models/database_authenticatable_patch.rb +26 -0
- data/lib/devise-security/models/expirable.rb +120 -0
- data/lib/devise-security/models/old_password.rb +4 -0
- data/lib/devise-security/models/paranoid_verification.rb +35 -0
- data/lib/devise-security/models/password_archivable.rb +80 -0
- data/lib/devise-security/models/password_expirable.rb +67 -0
- data/lib/devise-security/models/secure_validatable.rb +100 -0
- data/lib/devise-security/models/security_questionable.rb +18 -0
- data/lib/devise-security/models/session_limitable.rb +21 -0
- data/lib/devise-security/orm/active_record.rb +20 -0
- data/lib/devise-security/patches.rb +21 -0
- data/lib/devise-security/patches/confirmations_controller_captcha.rb +21 -0
- data/lib/devise-security/patches/confirmations_controller_security_question.rb +25 -0
- data/lib/devise-security/patches/controller_captcha.rb +17 -0
- data/lib/devise-security/patches/controller_security_question.rb +20 -0
- data/lib/devise-security/patches/passwords_controller_captcha.rb +20 -0
- data/lib/devise-security/patches/passwords_controller_security_question.rb +24 -0
- data/lib/devise-security/patches/registrations_controller_captcha.rb +33 -0
- data/lib/devise-security/patches/sessions_controller_captcha.rb +24 -0
- data/lib/devise-security/patches/unlocks_controller_captcha.rb +20 -0
- data/lib/devise-security/patches/unlocks_controller_security_question.rb +24 -0
- data/lib/devise-security/rails.rb +17 -0
- data/lib/devise-security/routes.rb +17 -0
- data/lib/devise-security/schema.rb +59 -0
- data/lib/devise-security/version.rb +3 -0
- data/lib/generators/devise-security/install_generator.rb +26 -0
- data/lib/generators/templates/devise-security.rb +38 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/controllers/application_controller.rb +2 -0
- data/test/dummy/app/controllers/captcha/sessions_controller.rb +3 -0
- data/test/dummy/app/controllers/foos_controller.rb +0 -0
- data/test/dummy/app/controllers/security_question/unlocks_controller.rb +3 -0
- data/test/dummy/app/models/.gitkeep +0 -0
- data/test/dummy/app/models/captcha_user.rb +5 -0
- data/test/dummy/app/models/secure_user.rb +3 -0
- data/test/dummy/app/models/security_question_user.rb +6 -0
- data/test/dummy/app/models/user.rb +5 -0
- data/test/dummy/app/views/foos/index.html.erb +0 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +24 -0
- data/test/dummy/config/boot.rb +6 -0
- data/test/dummy/config/database.yml +7 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/test.rb +27 -0
- data/test/dummy/config/initializers/devise.rb +9 -0
- data/test/dummy/config/initializers/migration_class.rb +6 -0
- data/test/dummy/config/routes.rb +10 -0
- data/test/dummy/config/secrets.yml +3 -0
- data/test/dummy/db/migrate/20120508165529_create_tables.rb +33 -0
- data/test/dummy/db/migrate/20150402165590_add_verification_columns.rb +11 -0
- data/test/dummy/db/migrate/20150407162345_add_verification_attempt_column.rb +9 -0
- data/test/dummy/db/migrate/20160320162345_add_security_questions_fields.rb +8 -0
- data/test/test_captcha_controller.rb +58 -0
- data/test/test_helper.rb +13 -0
- data/test/test_install_generator.rb +16 -0
- data/test/test_paranoid_verification.rb +124 -0
- data/test/test_password_archivable.rb +61 -0
- data/test/test_password_expirable.rb +32 -0
- data/test/test_password_expired_controller.rb +29 -0
- data/test/test_secure_validatable.rb +85 -0
- data/test/test_security_question_controller.rb +60 -0
- 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
|
data/test/dummy/Rakefile
ADDED
File without changes
|
File without changes
|
File without changes
|
@@ -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,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,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,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,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
|
data/test/test_helper.rb
ADDED
@@ -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
|