devise-suspicious_login 0.1.1

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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/Gemfile +2 -0
  4. data/README.md +111 -0
  5. data/Rakefile +36 -0
  6. data/app/views/devise/mailer/suspicious_login_instructions.html.erb +7 -0
  7. data/bin/test +7 -0
  8. data/config/locales/en.yml +10 -0
  9. data/devise-suspicious_login.gemspec +26 -0
  10. data/lib/generators/active_record/suspicious_login_generator.rb +97 -0
  11. data/lib/generators/active_record/templates/migration.rb +11 -0
  12. data/lib/generators/active_record/templates/migration_existing.rb +13 -0
  13. data/lib/generators/suspicious_login/install_generator.rb +23 -0
  14. data/lib/generators/suspicious_login/orm_helpers.rb +56 -0
  15. data/lib/generators/templates/suspicious_login.rb +26 -0
  16. data/lib/suspicious_login.rb +38 -0
  17. data/lib/suspicious_login/controllers/helpers.rb +6 -0
  18. data/lib/suspicious_login/hooks/suspicious_login.rb +8 -0
  19. data/lib/suspicious_login/mailer.rb +11 -0
  20. data/lib/suspicious_login/model.rb +79 -0
  21. data/lib/suspicious_login/patches.rb +12 -0
  22. data/lib/suspicious_login/rails.rb +9 -0
  23. data/lib/suspicious_login/schema.rb +19 -0
  24. data/lib/suspicious_login/strategies/token.rb +54 -0
  25. data/lib/suspicious_login/version.rb +3 -0
  26. data/test/dummy/Rakefile +6 -0
  27. data/test/dummy/app/controllers/application_controller.rb +3 -0
  28. data/test/dummy/app/controllers/home_controller.rb +5 -0
  29. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  30. data/test/dummy/app/models/application_record.rb +3 -0
  31. data/test/dummy/app/models/user.rb +9 -0
  32. data/test/dummy/app/views/home/index.html +0 -0
  33. data/test/dummy/bin/bundle +3 -0
  34. data/test/dummy/bin/rails +4 -0
  35. data/test/dummy/bin/rake +4 -0
  36. data/test/dummy/bin/setup +38 -0
  37. data/test/dummy/bin/update +29 -0
  38. data/test/dummy/bin/yarn +11 -0
  39. data/test/dummy/config.ru +5 -0
  40. data/test/dummy/config/application.rb +17 -0
  41. data/test/dummy/config/boot.rb +5 -0
  42. data/test/dummy/config/database.yml +21 -0
  43. data/test/dummy/config/environment.rb +2 -0
  44. data/test/dummy/config/environments/test.rb +19 -0
  45. data/test/dummy/config/initializers/devise.rb +13 -0
  46. data/test/dummy/config/locales/devise.en.yml +12 -0
  47. data/test/dummy/config/routes.rb +5 -0
  48. data/test/dummy/config/secrets.yml +5 -0
  49. data/test/dummy/config/spring.rb +6 -0
  50. data/test/dummy/db/migrate/20180910092425_create_tables.rb +16 -0
  51. data/test/dummy/db/migrate/20180910094718_add_login_token_columns.rb +8 -0
  52. data/test/dummy/db/migrate/20180910104707_add_trackable_columns.rb +11 -0
  53. data/test/dummy/db/migrate/20180919081730_add_recoverable_columns.rb +8 -0
  54. data/test/dummy/db/schema.rb +38 -0
  55. data/test/dummy/log/.keep +0 -0
  56. data/test/factories.rb +47 -0
  57. data/test/generators/active_record_generator_test.rb +40 -0
  58. data/test/generators/install_generator_test.rb +15 -0
  59. data/test/support/helpers.rb +16 -0
  60. data/test/suspicious_acceptance_test.rb +269 -0
  61. data/test/suspicious_login_test.rb +38 -0
  62. data/test/test_helper.rb +37 -0
  63. metadata +245 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0222b12b1368c6bd070e59a5e8f5a85711cc16f9fe9f2565a9ae8f10c7172055
4
+ data.tar.gz: b216d35f79d6d79f85cd01cfeba1ae2e13ddb61e6b35acf3edc8b826d9f7036d
5
+ SHA512:
6
+ metadata.gz: 4fbaf798720e48198aade72a7a21be195bdec0776e878d0424fb1ff342f3986d4fe6492e537cbdeffa0bbbcbdae49661660bfaa7ef92c09388d6de09448da16d
7
+ data.tar.gz: 6f3ff5333a4ca4dfc6f2b85747bdb6345a1659445078c6e2aa8ce60a7d98c9f3d84155cfef1b023becedf4f5023f8988450850cc02c8519f253df334e3171eb4
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ .bundle/
2
+ log/*.log
3
+ pkg/
4
+ test/tmp
5
+ test/dummy/db/*.sqlite3
6
+ test/dummy/db/*.sqlite3-journal
7
+ test/dummy/log/*.log
8
+ test/dummy/tmp/
9
+ Gemfile.lock
10
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # devise-suspicious_login
2
+
3
+ A devise extension that helps protect again suspicious logins.
4
+
5
+ ## Getting started
6
+
7
+ ```ruby
8
+ gem 'devise-suspicious_login
9
+ ```
10
+
11
+ Run `bundle` command to install.
12
+
13
+
14
+ ### Quick Installation
15
+
16
+ Quick Installation should work on most default rails apps.
17
+
18
+ Run the install generator:
19
+
20
+ `rails generate suspicious_login:install`
21
+
22
+ to install the relevant config files `config/initializers/suspicious_login.rb` with default settings.
23
+
24
+ Next run the ActiveRecord generator for each model you want to enable suspicious_login detection for.
25
+
26
+ `rails generate active_record:suspicious_login User`
27
+
28
+ will update and configure the User model automatically. This will also automatically create a database migration that adds the necessary fields to the User model.
29
+
30
+ Run this migration wih
31
+
32
+ `rails db:migrate`
33
+
34
+ By default only dormant users (3 months without a login by default setting) are considered suspicious. Suspicious check for dormant users can be turned of by setting:
35
+
36
+ ```ruby
37
+ config.dormant_sign_in after = nil
38
+ ```
39
+
40
+ To add a custom suspicious check for a model simply define a method `suspicious_login_attempt?(request)` eg:
41
+
42
+ ```ruby
43
+ # request parameter contains the contents of the rails request
44
+ def suspicious_login_attempt?(request)
45
+ if request.ip.botnet? return true
46
+ false
47
+ end
48
+ ```
49
+
50
+
51
+ ### Manual Installation
52
+
53
+ Once installed you need to add `login_token` and `login_token_sent_at` fields to any resources (eg User) that will use need this feature. Below shows how to add this to the `User` model.
54
+
55
+
56
+ ```ruby
57
+ # For a new migration for the users table, define a migration as follows:
58
+ create_table :users do |t|
59
+ t.login_token
60
+ end
61
+ ```
62
+
63
+ ```ruby
64
+ # If the table already exists, define a migration and add the following:
65
+ change_table :users do |t|
66
+ t.string login_token
67
+ t.datetime :login_token_sent_at
68
+ end
69
+ ```
70
+
71
+ Add the `:suspicious_login` module to the resource.
72
+ This extension requires the resource to have the native devise modules `:trackable` and `:recoverable` attached to it eg:
73
+
74
+ ```ruby
75
+ class User < ApplicationRecord
76
+ devise :database_authenticatable, :recoverable, :registerable, :authenticatable, :trackable, :suspicious_login
77
+ end
78
+ ```
79
+
80
+ ## Configuration
81
+
82
+ ```ruby
83
+ Devise.setup do |config|
84
+ # Period of time to expire token after login_tokens
85
+ # config.expire_login_token_after = 10.minutes
86
+
87
+ # Period of time to wait before resending another email for a suspicious login
88
+ # config.resend_login_token_after = 1.minute
89
+
90
+ # Period of time after which a user is considered to be dormant and a login treated as suspicious
91
+ # dormant_sign_in_after = 3.months
92
+
93
+ # Column to store login token for resource
94
+ # config.token_field_name = :login_token
95
+
96
+ # Column to store login token create time for resource
97
+ # config.token_created_at_field_name = :login_token_sent_at
98
+
99
+ # Clear token on login (allows tokens to be one time use only)
100
+ # config.clear_token_on_login = true
101
+ end
102
+ ```
103
+
104
+ Be sure to set all of your devise login failure messages to be the same otherwise an attack will know if the login credentials are correct depending on the failure message returned!
105
+
106
+ See (test/dummy/config/locales/devise.en.yml)
107
+
108
+ ## Requirements
109
+
110
+ * Devise (https://github.com/plataformatec/devise)
111
+ * Rails 5.1 onwards (http://github.com/rails/rails)
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "bundler/setup"
5
+ require "bundler/gem_tasks"
6
+ rescue LoadError
7
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
8
+ end
9
+
10
+ require "rdoc/task"
11
+
12
+ RDoc::Task.new(:rdoc) do |rdoc|
13
+ rdoc.rdoc_dir = "rdoc"
14
+ rdoc.title = "Devise::SuspiciousLogin"
15
+ rdoc.options << "--line-numbers"
16
+ rdoc.rdoc_files.include("README.md")
17
+ rdoc.rdoc_files.include("lib/**/*.rb")
18
+ end
19
+
20
+
21
+
22
+
23
+
24
+
25
+ require "bundler/gem_tasks"
26
+
27
+ require "rake/testtask"
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << "test"
31
+ t.pattern = "test/**/*_test.rb"
32
+ t.verbose = false
33
+ end
34
+
35
+
36
+ task default: :test
@@ -0,0 +1,7 @@
1
+ <h2><%= "Hi #{@resource.email}" %></h2>
2
+ <p>We noticed a new login to your account</p>
3
+ <h4>If this was you:</h4>
4
+ <p>Click the login link below to config and log in</p>
5
+ <p><%= link_to("Log in", root_url(login_token: @token, email: @resource.email)) %></p>
6
+ <h4>If this wasn't you:</h4>
7
+ <p>Your account may have been compromised and you should <%= link_to("reset your password now.", edit_user_password_url(@resource, reset_password_token: @reset_password_token)) %></p>
data/bin/test ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $: << File.expand_path(File.expand_path("../../spec", __FILE__))
5
+
6
+ require "bundler/setup"
7
+ require "rails/plugin/test"
@@ -0,0 +1,10 @@
1
+ en:
2
+ devise:
3
+ installer:
4
+ missing_modules:
5
+ one: "The following devise module is missing from your model and has been automatically added -> :%{missing_modules}.\n\n"
6
+ other: "The following devise modules are missing from your model and have been automatically added -> :%{missing_modules}.\n\n"
7
+ missing_model: "Model %{name} does not exist."
8
+ devise_missing: "Devise not found on %{name}"
9
+ failure:
10
+ suspicious_login: "Your email or password are invalid, OR we need to verify your sign in. If you have received an email from us, please follow the instructions to complete your sign in."
@@ -0,0 +1,26 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+
3
+ # Maintain your gem's version:
4
+ require "suspicious_login/version"
5
+
6
+ # Describe your gem and declare its dependencies:
7
+ Gem::Specification.new do |s|
8
+ s.name = "devise-suspicious_login"
9
+ s.version = SuspiciousLogin::VERSION.dup
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = ["ceres629"]
12
+ s.email = ["ceres629@gmail.com"]
13
+ s.summary = "Devise extension that check for suspicious logins"
14
+ s.summary = "Devise extension that check for suspicious logins"
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- test/*`.split("\n")
17
+ s.require_paths = ['lib']
18
+
19
+ s.add_runtime_dependency "devise", ">= 4"
20
+ s.add_development_dependency "colorize"
21
+ s.add_development_dependency 'rails', '>= 5.0', '< 6.0'
22
+ s.add_development_dependency 'minitest'
23
+ s.add_development_dependency "rake"
24
+ s.add_development_dependency "sqlite3"
25
+ s.add_development_dependency "factory_bot_rails"
26
+ end
@@ -0,0 +1,97 @@
1
+ require 'generators/active_record/devise_generator'
2
+ require 'generators/suspicious_login/orm_helpers'
3
+
4
+ module ActiveRecord
5
+ module Generators
6
+ class SuspiciousLoginGenerator < ActiveRecord::Generators::Base
7
+ argument :attributes, type: :array, default: [], banner: "field:type field:type"
8
+ include SuspiciousLogin::Generators::OrmHelpers
9
+ source_root File.expand_path("../templates", __FILE__)
10
+
11
+ @required_devise_modules = [:trackable, :recoverable]
12
+
13
+ def generate_model
14
+ class_path = namespaced? ? class_name.to_s.split("::") : [class_name]
15
+
16
+ if behavior == :invoke && !model_exists?
17
+ if Rails.env.test?
18
+ invoke "active_record:model", [name], migration: false
19
+ else
20
+ raise(SuspiciousLogin::MissingModelError, I18n.t('devise.installer.missing_model', name: class_path.last))
21
+ end
22
+ elsif behavior == :revoke
23
+ return
24
+ end
25
+ end
26
+
27
+ def copy_devise_migration
28
+ if (behavior == :invoke && model_exists?) || (behavior == :revoke && migration_exists?(table_name))
29
+ migration_template "migration_existing.rb", "#{migration_path}/add_devise_suspicious_login_to_#{table_name}.rb", migration_version: migration_version
30
+ else
31
+ migration_template "migration.rb", "#{migration_path}/devise_create_#{table_name}.rb", migration_version: migration_version
32
+ end
33
+ end
34
+
35
+ def add_warden_strategy
36
+ template('../../templates/suspicious_login.rb', 'config/initializers/suspicious_login.rb') unless File.exist?(File.join(destination_root, 'config/initializers/suspicious_login.rb'))
37
+
38
+ content = File.read(File.join(destination_root, 'config/initializers/suspicious_login.rb'))
39
+
40
+ if content.include?("config.warden do |manager|\n")
41
+ inject_into_file 'config/initializers/suspicious_login.rb', :after => "config.warden do |manager|\n" do
42
+ " manager.default_strategies(:scope => :#{name.downcase.to_sym}).unshift :suspicious_login_token\n"
43
+ end
44
+ else
45
+ inject_into_file 'config/initializers/suspicious_login.rb', :after => "Devise.setup do |config|" do
46
+ """ config.warden do |manager|
47
+ manager.default_strategies(:scope => :#{name.downcase.to_sym}).unshift :suspicious_login_token
48
+ end
49
+ """
50
+ end
51
+ end
52
+ end
53
+
54
+ def migration_data
55
+ <<RUBY
56
+ t.string #{Devise.token_field_name}
57
+ t.datetime #{Devise.token_created_at_field_name}
58
+ RUBY
59
+ end
60
+
61
+ def inject_devise_content
62
+ devise_modules_re = /devise((\s*)(:\S*))+/
63
+ suspicious_login_re = /devise(?:(?:\s*)(?::\S*))+(,\s*:suspicious_login)/
64
+ content = File.read(File.join(destination_root, model_path))
65
+
66
+ if model_exists?
67
+ class_path = namespaced? ? class_name.to_s.split("::") : [class_name]
68
+
69
+ if behavior == :invoke
70
+ devise_content = devise_modules_re.match(content)&.[](0)
71
+ devise_missing_modules = missing_modules(devise_content)
72
+ if devise_content && devise_missing_modules
73
+ updated_devise_content = devise_missing_modules.reduce(devise_content) { |acc, mod| "#{acc}, :#{mod.to_s}" }
74
+ puts I18n.t('devise.installer.missing_modules', missing_modules: devise_missing_modules.join(', '), count: devise_missing_modules.length) if devise_missing_modules.any? && gsub_file(model_path, devise_modules_re, updated_devise_content) > 0
75
+ puts "\n"
76
+ else
77
+ raise(SuspiciousLogin::DeviseMissingFromModel, I18n.t('devise.installer.devise_missing', name: class_path.last)) if !Rails.env.test?
78
+ end
79
+ elsif behavior == :revoke
80
+ chop_content = suspicious_login_re.match(content)&.[](1)
81
+ force_gsub_file(model_path, chop_content, "") if chop_content
82
+ end
83
+ end
84
+ end
85
+
86
+ def rails5?
87
+ Rails.version.start_with? '5'
88
+ end
89
+
90
+ def migration_version
91
+ if rails5?
92
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,11 @@
1
+ class DeviseCreate<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= table_name %><%= primary_key_type %> do |t|
4
+ <%= migration_data -%>
5
+
6
+ <% attributes.each do |attribute| -%>
7
+ t.<%= attribute.type %> :<%= attribute.name %>
8
+ <% end -%>
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ class AddDeviseSuspiciousLoginTo<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def self.up
3
+ change_table :<%= table_name %> do |t|
4
+ <%= migration_data -%>
5
+ end
6
+ end
7
+
8
+ def self.down
9
+ # By default, we don't want to make any assumption about how to roll back a migration when your
10
+ # model already existed. Please edit below which fields you would like to remove in this migration.
11
+ raise ActiveRecord::IrreversibleMigration
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module SuspiciousLogin
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+
5
+ source_root File.expand_path("../../templates", __FILE__)
6
+ desc "Add SuspiciousLogin config variables and files to rails project"
7
+ def create_initializer
8
+ template('suspicious_login.rb', 'config/initializers/suspicious_login.rb')
9
+ end
10
+
11
+ def copy_locale
12
+ copy_file "../../../config/locales/en.yml", "config/locales/suspicious_login.en.yml"
13
+ puts "\n*** IMPORTANT: Be sure to set all devise authentication failure messages to be the same as 'devise.failure.suspicious_login' ***".red
14
+ puts "See https://github.com/ceres629/devise-suspicious_login/blob/master/test/dummy/config/locales/devise.en.yml for an example devise.en.yml.".yellow
15
+ end
16
+
17
+ def prepend_application_file
18
+ create_file "config/application.rb", "module Rails\n class Application < Rails::Application\n end\nend" if Rails.env.test?
19
+ application "require 'suspicious_login'"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,56 @@
1
+ module SuspiciousLogin
2
+ module Generators
3
+ module OrmHelpers
4
+ def force_gsub_file(path, flag, *args, &block)
5
+ config = args.last.is_a?(Hash) ? args.pop : {}
6
+
7
+ path = File.expand_path(path, destination_root)
8
+ say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
9
+
10
+ unless options[:pretend]
11
+ content = File.binread(path)
12
+ content.gsub!(flag, *args, &block)
13
+ File.open(path, "wb") { |file| file.write(content) }
14
+ end
15
+ end
16
+
17
+ def required_devise_modules
18
+ [:trackable, :recoverable, :suspicious_login]
19
+ end
20
+
21
+ def missing_modules(devise_content)
22
+ missing_modules = []
23
+ required_devise_modules.each { |mod| missing_modules << mod unless devise_content&.include?(mod.to_s)}
24
+ missing_modules
25
+ end
26
+
27
+ def devise_module_exists?(devise_content, module_name)
28
+ devise_content.include?(module_name)
29
+ end
30
+
31
+ def append_devise_module(devise_content, module_name)
32
+ (devise_content.blank? || devise_content.include?(module_name.to_s)) ? devise_content : devise_content + ", :#{module_name.to_s}"
33
+ end
34
+
35
+ def model_exists?
36
+ File.exist?(File.join(destination_root, model_path))
37
+ end
38
+
39
+ def migration_exists?(table_name)
40
+ Dir.glob("#{File.join(destination_root, migration_path)}/[0-9]*_*.rb").grep(/\d+_add_devise_suspicious_login_to_#{table_name}.rb$/).first
41
+ end
42
+
43
+ def migration_path
44
+ if Rails.version >= '5.0.3'
45
+ db_migrate_path
46
+ else
47
+ @migration_path ||= File.join("db", "migrate")
48
+ end
49
+ end
50
+
51
+ def model_path
52
+ @model_path ||= File.join("app", "models", "#{file_path}.rb")
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,26 @@
1
+ Devise.setup do |config|
2
+ # ==> SuspiciousLogin Extension
3
+
4
+ # Configure suspicious extension for devise
5
+
6
+ # Period of time after which tokens expire
7
+ # config.expire_login_token_after = 10.minutes
8
+
9
+ # Minimum period of time before another token can be resent
10
+ # config.resend_login_token_after = 1.minute
11
+
12
+ # Period of time after which a user is considered to be dormant
13
+ # config.dormant_sign_in_after = 3.months
14
+
15
+ # Resource field that will be used to store the login_token
16
+ # config.token_field_name = :login_token
17
+
18
+ # Resource field that will be used to store the token_created_at time
19
+ # config.token_created_at_field_name = :login_token_sent_at
20
+
21
+ # Clear login_token after user login (true means each token can only be used once)
22
+ # config.clear_token_on_login = true
23
+
24
+ config.warden do |manager|
25
+ end
26
+ end