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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +2 -0
- data/README.md +111 -0
- data/Rakefile +36 -0
- data/app/views/devise/mailer/suspicious_login_instructions.html.erb +7 -0
- data/bin/test +7 -0
- data/config/locales/en.yml +10 -0
- data/devise-suspicious_login.gemspec +26 -0
- data/lib/generators/active_record/suspicious_login_generator.rb +97 -0
- data/lib/generators/active_record/templates/migration.rb +11 -0
- data/lib/generators/active_record/templates/migration_existing.rb +13 -0
- data/lib/generators/suspicious_login/install_generator.rb +23 -0
- data/lib/generators/suspicious_login/orm_helpers.rb +56 -0
- data/lib/generators/templates/suspicious_login.rb +26 -0
- data/lib/suspicious_login.rb +38 -0
- data/lib/suspicious_login/controllers/helpers.rb +6 -0
- data/lib/suspicious_login/hooks/suspicious_login.rb +8 -0
- data/lib/suspicious_login/mailer.rb +11 -0
- data/lib/suspicious_login/model.rb +79 -0
- data/lib/suspicious_login/patches.rb +12 -0
- data/lib/suspicious_login/rails.rb +9 -0
- data/lib/suspicious_login/schema.rb +19 -0
- data/lib/suspicious_login/strategies/token.rb +54 -0
- data/lib/suspicious_login/version.rb +3 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/controllers/home_controller.rb +5 -0
- data/test/dummy/app/mailers/application_mailer.rb +4 -0
- data/test/dummy/app/models/application_record.rb +3 -0
- data/test/dummy/app/models/user.rb +9 -0
- data/test/dummy/app/views/home/index.html +0 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +38 -0
- data/test/dummy/bin/update +29 -0
- data/test/dummy/bin/yarn +11 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/config/application.rb +17 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +21 -0
- data/test/dummy/config/environment.rb +2 -0
- data/test/dummy/config/environments/test.rb +19 -0
- data/test/dummy/config/initializers/devise.rb +13 -0
- data/test/dummy/config/locales/devise.en.yml +12 -0
- data/test/dummy/config/routes.rb +5 -0
- data/test/dummy/config/secrets.yml +5 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/db/migrate/20180910092425_create_tables.rb +16 -0
- data/test/dummy/db/migrate/20180910094718_add_login_token_columns.rb +8 -0
- data/test/dummy/db/migrate/20180910104707_add_trackable_columns.rb +11 -0
- data/test/dummy/db/migrate/20180919081730_add_recoverable_columns.rb +8 -0
- data/test/dummy/db/schema.rb +38 -0
- data/test/dummy/log/.keep +0 -0
- data/test/factories.rb +47 -0
- data/test/generators/active_record_generator_test.rb +40 -0
- data/test/generators/install_generator_test.rb +15 -0
- data/test/support/helpers.rb +16 -0
- data/test/suspicious_acceptance_test.rb +269 -0
- data/test/suspicious_login_test.rb +38 -0
- data/test/test_helper.rb +37 -0
- 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,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,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)
|
data/test/dummy/Rakefile
ADDED
|
File without changes
|
data/test/dummy/bin/rake
ADDED
|
@@ -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
|
data/test/dummy/bin/yarn
ADDED
|
@@ -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,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,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
|