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
@@ -0,0 +1,38 @@
1
+ module SuspiciousLogin
2
+ autoload :Schema, 'suspcious_login/schema'
3
+ autoload :Patches, 'suspicious_login/patches'
4
+ autoload :Mailer, 'suspicious_login/mailer'
5
+
6
+ class MissingModelError < StandardError; end
7
+ class DeviseMissingFromModel < StandardError; end
8
+ end
9
+
10
+ require 'devise'
11
+ require 'suspicious_login/model'
12
+ require 'suspicious_login/rails'
13
+ require 'suspicious_login/strategies/token'
14
+ require 'colorize'
15
+
16
+ module Devise
17
+ mattr_accessor :expire_login_token_after
18
+ @@expire_login_token_after = 10.minutes
19
+
20
+ mattr_accessor :resend_login_token_after
21
+ @@resend_login_token_after = 1.minute
22
+
23
+ mattr_accessor :dormant_sign_in_after
24
+ @@dormant_sign_in_after = 3.months
25
+
26
+ mattr_accessor :token_field_name
27
+ @@token_field_name = :login_token
28
+
29
+ mattr_accessor :token_created_at_field_name
30
+ @@token_created_at_field_name = :login_token_sent_at
31
+
32
+ mattr_accessor :clear_token_on_login
33
+ @@clear_token_on_login = true
34
+ end
35
+
36
+ I18n.load_path.unshift File.join(File.dirname(__FILE__), *%w[suspicious_login locales en.yml])
37
+
38
+ Devise.add_module :suspicious_login, model: "suspicious_login/model"
@@ -0,0 +1,6 @@
1
+ module SuspiciousLogin::Controllers::Helpers
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ Warden::Manager.after_set_user except: :fetch do |user, warden, opts|
2
+ if user && !user.token_login? && user.respond_to?(:suspicious?) && user.suspicious?(warden.request)
3
+ user.send_suspicious_login_instructions(warden.request) if user.login_token_sent_at.nil? || Time.now.utc - user.login_token_sent_at > Devise.resend_login_token_after
4
+ scope = opts[:scope]
5
+ warden.logout(scope) if warden.authenticated?(scope)
6
+ throw(:warden, message: I18n.t('devise.failure.suspicious_login'))
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module SuspiciousLogin
2
+ module Mailer
3
+ def suspicious_login_instructions(record, token=nil, opts={})
4
+ @record = record
5
+ @token = ERB::Util.url_encode(record.reset_suspicious_login_token!(token))
6
+ @reset_password_token = record.generate_reset_password_token!
7
+
8
+ devise_mail(record, :suspicious_login_instructions, opts)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,79 @@
1
+ require "suspicious_login/hooks/suspicious_login"
2
+
3
+ module Devise
4
+ module Models
5
+ module SuspiciousLogin
6
+ extend ActiveSupport::Concern
7
+
8
+ def self.required_fields(klass)
9
+ [Devise.token_field_name, Devise.token_created_at_field_name]
10
+ end
11
+
12
+ def generate_login_token
13
+ loop do
14
+ token = Devise.friendly_token
15
+ break token unless User.where(authentication_token: token).first
16
+ end
17
+ end
18
+
19
+ def generate_reset_password_token
20
+ raw, enc = Devise.token_generator.generate(self.class, :reset_password_token)
21
+ self.reset_password_token = enc
22
+ self.reset_password_sent_at = Time.now.utc
23
+ raw
24
+ end
25
+
26
+ def reset_suspicious_login_token(token=nil)
27
+ token = token || generate_login_token
28
+ self[Devise.token_field_name] = token
29
+ self[Devise.token_created_at_field_name] = Time.now.utc unless Devise.expire_login_token_after.blank?
30
+ token
31
+ end
32
+
33
+ def generate_reset_password_token!
34
+ token = generate_reset_password_token
35
+ save(validate: false)
36
+ token
37
+ end
38
+
39
+ def reset_suspicious_login_token!(token=nil)
40
+ token = reset_suspicious_login_token(token)
41
+ save(validate: false)
42
+ token
43
+ end
44
+
45
+ def clear_suspicious_login_token!
46
+ self[Devise.token_field_name] = nil
47
+ self[Devise.token_created_at_field_name] = nil
48
+ save(validate: false)
49
+ end
50
+
51
+ def after_login_token_authentication
52
+ clear_suspicious_login_token! if Devise.clear_token_on_login
53
+ @token_login = true
54
+ end
55
+
56
+ def suspicious?(request = {})
57
+ respond_to?(:suspicious_login_attempt?) ? suspicious_login_attempt?(request) || dormant_account?(request) : dormant_account?(request)
58
+ end
59
+
60
+ def send_suspicious_login_instructions(request = {})
61
+ send_devise_notification(:suspicious_login_instructions, nil, {})
62
+ end
63
+
64
+ def token_login?
65
+ @token_login || false
66
+ end
67
+
68
+ def dormant_account?(request)
69
+ return true if !(respond_to?(:last_sign_in_at))
70
+
71
+ !last_sign_in_at.nil? &&
72
+ !current_sign_in_ip.nil? &&
73
+ request.remote_ip != last_sign_in_ip &&
74
+ request.remote_ip != current_sign_in_ip &&
75
+ Time.now.utc - current_sign_in_at > Devise.dormant_sign_in_after
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,12 @@
1
+ require 'suspicious_login/mailer'
2
+
3
+ module SuspiciousLogin
4
+ module Patches
5
+ class << self
6
+ def apply
7
+ Devise.mailer.send(:include, SuspiciousLogin::Mailer)
8
+ Devise.mailer.send(:include, Devise::Mailers::Helpers) unless Devise.mailer.ancestors.include?(Devise::Mailers::Helpers)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ require 'suspicious_login'
2
+
3
+ module SuspiciousLogin
4
+ class Engine < ::Rails::Engine
5
+ ActiveSupport::Reloader.to_prepare do
6
+ SuspiciousLogin::Patches.apply
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ module SuspiciousLogin
2
+ module Schema
3
+ # # For a new resource migration:
4
+ # create_table :the_resource do |t|
5
+ # t.login_token
6
+ # end
7
+
8
+ # # For existing resource, define a migration and add the following:
9
+ # change_table :the_resource do |t|
10
+ # t.string login_token
11
+ # t.datetime :login_token_sent_at
12
+ # end
13
+
14
+ def login_token
15
+ apply_devise_schema :login_token, String
16
+ apply_devise_schema :login_token_sent_at, DateTime
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,54 @@
1
+ require 'devise/strategies/base'
2
+
3
+ module Devise
4
+ module Strategies
5
+ class SecureTokenAuthenticatable < Authenticatable
6
+ def store?
7
+ super
8
+ end
9
+
10
+ def valid?
11
+ resource_email.present? && login_token.present?
12
+ end
13
+
14
+ def authenticate!
15
+ resource = resource_email && mapping.to.find_by(:email => resource_email)
16
+
17
+ if resource
18
+ if Time.now.utc.to_i < (resource[Devise.token_created_at_field_name].to_i + token_expires_after.to_i) && Devise.secure_compare(resource[Devise.token_field_name], login_token)
19
+ resource.after_login_token_authentication
20
+ return success!(resource)
21
+ end
22
+ else
23
+ Devise.secure_compare("foo", login_token)
24
+ throw(:warden, message: I18n.t('devise.failure.invalid'))
25
+ return fail!
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def valid_params_request?
32
+ true
33
+ end
34
+
35
+ def resource
36
+ @resource ||= mapping.to.find_by(:email => params)
37
+ end
38
+
39
+ def resource_email
40
+ @email ||= params[:email]
41
+ end
42
+
43
+ def login_token
44
+ @login_token ||= params[:login_token]
45
+ end
46
+
47
+ def token_expires_after
48
+ @token_expires_after ||= Devise.expire_login_token_after
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ Warden::Strategies.add(:suspicious_login_token, Devise::Strategies::SecureTokenAuthenticatable)
@@ -0,0 +1,3 @@
1
+ module SuspiciousLogin
2
+ VERSION = '0.1.1'
3
+ end
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative 'config/application'
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery with: :exception
3
+ end
@@ -0,0 +1,5 @@
1
+ class HomeController < ::ApplicationController
2
+ before_action :authenticate_user!
3
+ def index
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: 'from@example.com'
3
+ layout 'mailer'
4
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,9 @@
1
+ class User < ApplicationRecord
2
+ devise :database_authenticatable, :recoverable, :registerable, :authenticatable, :trackable, :suspicious_login
3
+
4
+ has_many :ip_addresses
5
+
6
+ def suspicious_login_attempt?(request = nil)
7
+ email.include?("suspicious")
8
+ end
9
+ end
File without changes
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+ load Gem.bin_path('bundler', 'bundle')
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ APP_PATH = File.expand_path('../config/application', __dir__)
3
+ require_relative '../config/boot'
4
+ require 'rails/commands'
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../config/boot'
3
+ require 'rake'
4
+ Rake.application.run
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ require 'pathname'
3
+ require 'fileutils'
4
+ include FileUtils
5
+
6
+ # path to your application root.
7
+ APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8
+
9
+ def system!(*args)
10
+ system(*args) || abort("\n== Command #{args} failed ==")
11
+ end
12
+
13
+ chdir APP_ROOT do
14
+ # This script is a starting point to setup your application.
15
+ # Add necessary setup steps to this file.
16
+
17
+ puts '== Installing dependencies =='
18
+ system! 'gem install bundler --conservative'
19
+ system('bundle check') || system!('bundle install')
20
+
21
+ # Install JavaScript dependencies if using Yarn
22
+ # system('bin/yarn')
23
+
24
+
25
+ # puts "\n== Copying sample files =="
26
+ # unless File.exist?('config/database.yml')
27
+ # cp 'config/database.yml.sample', 'config/database.yml'
28
+ # end
29
+
30
+ puts "\n== Preparing database =="
31
+ system! 'bin/rails db:setup'
32
+
33
+ puts "\n== Removing old logs and tempfiles =="
34
+ system! 'bin/rails log:clear tmp:clear'
35
+
36
+ puts "\n== Restarting application server =="
37
+ system! 'bin/rails restart'
38
+ end
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ require 'pathname'
3
+ require 'fileutils'
4
+ include FileUtils
5
+
6
+ # path to your application root.
7
+ APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8
+
9
+ def system!(*args)
10
+ system(*args) || abort("\n== Command #{args} failed ==")
11
+ end
12
+
13
+ chdir APP_ROOT do
14
+ # This script is a way to update your development environment automatically.
15
+ # Add necessary update steps to this file.
16
+
17
+ puts '== Installing dependencies =='
18
+ system! 'gem install bundler --conservative'
19
+ system('bundle check') || system!('bundle install')
20
+
21
+ puts "\n== Updating database =="
22
+ system! 'bin/rails db:migrate'
23
+
24
+ puts "\n== Removing old logs and tempfiles =="
25
+ system! 'bin/rails log:clear tmp:clear'
26
+
27
+ puts "\n== Restarting application server =="
28
+ system! 'bin/rails restart'
29
+ end
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ VENDOR_PATH = File.expand_path('..', __dir__)
3
+ Dir.chdir(VENDOR_PATH) do
4
+ begin
5
+ exec "yarnpkg #{ARGV.join(" ")}"
6
+ rescue Errno::ENOENT
7
+ $stderr.puts "Yarn executable was not detected in the system."
8
+ $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
9
+ exit 1
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require_relative 'config/environment'
4
+
5
+ run Rails.application
@@ -0,0 +1,17 @@
1
+ require_relative 'boot'
2
+
3
+ require 'rails/all'
4
+
5
+ Bundler.require(*Rails.groups)
6
+ require "suspicious_login"
7
+
8
+ module Dummy
9
+ class Application < Rails::Application
10
+ config.active_record.sqlite3.represent_boolean_as_integer = true
11
+ config.encoding = 'utf-8'
12
+ config.filter_parameters += [:password]
13
+ config.assets.enabled = true
14
+ config.assets.version = '1.0'
15
+ config.secret_key_base = 'change_me'
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # Set up gems listed in the Gemfile.
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
3
+
4
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5
+ $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
@@ -0,0 +1,21 @@
1
+ # SQLite version 3.x
2
+ # gem install sqlite3
3
+ #
4
+ # Ensure the SQLite 3 gem is defined in your Gemfile
5
+ # gem 'sqlite3'
6
+ #
7
+ default: &default
8
+ adapter: sqlite3
9
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10
+ timeout: 5000
11
+
12
+ development:
13
+ <<: *default
14
+ database: db/development.sqlite3
15
+
16
+ # Warning: The database defined as "test" will be erased and
17
+ # re-generated from your development database when you run "rake".
18
+ # Do not set this db to the same as development or production.
19
+ test:
20
+ <<: *default
21
+ database: db/test.sqlite3