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,2 @@
1
+ require_relative 'application'
2
+ Rails.application.initialize!
@@ -0,0 +1,19 @@
1
+ Rails.application.configure do
2
+ config.cache_classes = true
3
+ config.eager_load = false
4
+
5
+ config.public_file_server.enabled = true
6
+ config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" }
7
+
8
+ config.consider_all_requests_local = true
9
+ config.action_controller.perform_caching = false
10
+
11
+ config.action_dispatch.show_exceptions = false
12
+
13
+ config.action_controller.allow_forgery_protection = false
14
+ config.action_mailer.perform_caching = false
15
+
16
+ config.action_mailer.delivery_method = :test
17
+
18
+ config.active_support.deprecation = :stderr
19
+ end
@@ -0,0 +1,13 @@
1
+ Devise.setup do |config|
2
+ config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
3
+ require 'devise/orm/active_record'
4
+
5
+ config.case_insensitive_keys = [:email]
6
+ config.strip_whitespace_keys = [:email]
7
+ config.skip_session_storage = [:http_auth]
8
+ config.stretches = Rails.env.test? ? 1 : 11
9
+
10
+ config.warden do |config|
11
+ config.default_strategies(:scope => :user).unshift :suspicious_login_token
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ en:
2
+ devise:
3
+ failure:
4
+ already_authenticated: ""
5
+ inactive: ""
6
+ invalid: "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."
7
+ locked: "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."
8
+ last_attempt: "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."
9
+ not_found_in_database: "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."
10
+ timeout: "Your session expired. Please log in again to continue."
11
+ unauthenticated: "You need to log in or sign up before continuing."
12
+ unconfirmed: ""
@@ -0,0 +1,5 @@
1
+ Rails.application.routes.draw do
2
+ devise_for :users, :controllers => {home: "/"}
3
+
4
+ root :to => 'home#index'
5
+ end
@@ -0,0 +1,5 @@
1
+ development:
2
+ secret_key_base: dev_dev_dev
3
+
4
+ test:
5
+ secret_key_base: test_test_test
@@ -0,0 +1,6 @@
1
+ %w(
2
+ .ruby-version
3
+ .rbenv-vars
4
+ tmp/restart.txt
5
+ tmp/caching-dev.txt
6
+ ).each { |path| Spring.watch(path) }
@@ -0,0 +1,16 @@
1
+ class CreateTables < ActiveRecord::Migration[5.1]
2
+ def change
3
+ create_table :users do |t|
4
+ t.string :unique_session_id, :limit => 20
5
+
6
+ ## Database authenticatable
7
+ t.string :email, null: false, default: ''
8
+ t.string :encrypted_password, null: false, default: ''
9
+
10
+ t.datetime :password_changed_at
11
+ t.timestamps null: false
12
+ end
13
+ add_index :users, :password_changed_at
14
+ add_index :users, :email
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ class AddLoginTokenColumns < ActiveRecord::Migration[5.1]
2
+ def change
3
+ change_table :users do |t|
4
+ add_column :users, :login_token, :string
5
+ add_column :users, :login_token_sent_at, :datetime
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ class AddTrackableColumns < ActiveRecord::Migration[5.1]
2
+ def change
3
+ change_table :users do |t|
4
+ add_column :users, :last_sign_in_at, :datetime
5
+ add_column :users, :current_sign_in_at, :datetime
6
+ add_column :users, :current_sign_in_ip, :string
7
+ add_column :users, :last_sign_in_ip, :string
8
+ add_column :users, :sign_in_count, :integer, default: 0, null: false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ class AddRecoverableColumns < ActiveRecord::Migration[5.1]
2
+ def change
3
+ change_table :users do |t|
4
+ add_column :users, :reset_password_token, :string
5
+ add_column :users, :reset_password_sent_at, :datetime
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,38 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # Note that this schema.rb definition is the authoritative source for your
6
+ # database schema. If you need to create the application database on another
7
+ # system, you should be using db:schema:load, not running all the migrations
8
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema.define(version: 2018_09_24_105700) do
14
+
15
+ create_table "users", force: :cascade do |t|
16
+ t.string "unique_session_id", limit: 20
17
+ t.string "email", default: "", null: false
18
+ t.string "encrypted_password", default: "", null: false
19
+ t.datetime "password_changed_at"
20
+ t.datetime "created_at", null: false
21
+ t.datetime "updated_at", null: false
22
+ t.string "login_token"
23
+ t.datetime "login_token_sent_at"
24
+ t.datetime "last_sign_in_at"
25
+ t.datetime "current_sign_in_at"
26
+ t.string "current_sign_in_ip"
27
+ t.string "last_sign_in_ip"
28
+ t.integer "sign_in_count", default: 0, null: false
29
+ t.string "reset_password_token"
30
+ t.datetime "reset_password_sent_at"
31
+ t.string "authentication_token"
32
+ t.datetime "authentication_token_created_at"
33
+ t.index ["authentication_token"], name: "index_users_on_authentication_token", unique: true
34
+ t.index ["email"], name: "index_users_on_email"
35
+ t.index ["password_changed_at"], name: "index_users_on_password_changed_at"
36
+ end
37
+
38
+ end
File without changes
data/test/factories.rb ADDED
@@ -0,0 +1,47 @@
1
+ FactoryBot.define do
2
+ sequence :email do |n|
3
+ "email#{n}@example.com"
4
+ end
5
+
6
+ sequence :uid do |n|
7
+ "12345#{n}"
8
+ end
9
+
10
+ factory :user, aliases: [:new_user] do
11
+ email
12
+ password { 'password' }
13
+ created_at { Time.now.utc }
14
+ updated_at { Time.now.utc }
15
+ last_sign_in_ip { '127.0.0.1' }
16
+ current_sign_in_ip { '127.0.0.2' }
17
+
18
+ factory :user_with_honest_login do
19
+ last_sign_in_at { 2.days.ago.utc }
20
+ current_sign_in_at { 1.day.ago.utc }
21
+ end
22
+
23
+ factory :user_with_dormant_login do
24
+ last_sign_in_at { 1.year.ago.utc }
25
+ current_sign_in_at { 6.months.ago.utc }
26
+
27
+ factory :user_with_dormant_login_and_recently_sent_login_token do
28
+ login_token_sent_at { Time.now.utc }
29
+ login_token { "TOKEN" }
30
+ end
31
+ end
32
+
33
+ factory :user_with_suspicious_login do
34
+ email { 'suspicious@example.org' }
35
+
36
+ factory :user_with_suspicious_login_and_recently_sent_login_token do
37
+ login_token_sent_at { Time.now.utc }
38
+ login_token { "TOKEN" }
39
+ end
40
+
41
+ factory :user_with_suspicious_login_and_ancient_login_token do
42
+ login_token_sent_at { 1.day.ago.utc }
43
+ login_token { "TOKEN" }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ require_relative '../test_helper.rb'
2
+
3
+ if DEVISE_ORM == :active_record
4
+ require 'generators/active_record/suspicious_login_generator'
5
+
6
+ class ActiveRecordGeneratorTest < Rails::Generators::TestCase
7
+ tests ActiveRecord::Generators::SuspiciousLoginGenerator
8
+ destination File.expand_path('../../tmp', __FILE__)
9
+ setup :prepare_destination
10
+
11
+ test "update model migration when model exists in test mode" do
12
+ run_generator %w(foo)
13
+ assert_file "app/models/foo.rb"
14
+ run_generator %w(foo)
15
+ assert_migration "db/migrate/add_devise_suspicious_login_to_foos.rb", /#{Devise.token_field_name}/
16
+ end
17
+
18
+ test "raise error if model is not present when not in test mode" do
19
+ Rails.env = 'prod'
20
+ assert_raise SuspiciousLogin::MissingModelError do
21
+ run_generator %w(foo)
22
+ end
23
+ Rails.env = 'test'
24
+ assert_no_file "app/models/foo.rb"
25
+ end
26
+
27
+ test "append warden strategy to model" do
28
+ run_generator %w(foo)
29
+ assert_file "config/initializers/suspicious_login.rb", /manager\.default_strategies\(:scope => :foo\)\.unshift :suspicious_login_token/
30
+ end
31
+
32
+ test "all files are deleted except the model" do
33
+ run_generator %w(foo)
34
+ run_generator %w(foo)
35
+ assert_migration "db/migrate/add_devise_suspicious_login_to_foos.rb"
36
+ run_generator %w(foo), behavior: :revoke
37
+ assert_no_migration "db/migrate/add_devise_suspicious_login_to_foos.rb"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ require_relative '../test_helper.rb'
2
+ require 'generators/suspicious_login/install_generator'
3
+
4
+ class InstallGeneratorTest < Rails::Generators::TestCase
5
+ tests SuspiciousLogin::Generators::InstallGenerator
6
+ destination File.expand_path("../../tmp", __FILE__)
7
+ setup :prepare_destination
8
+
9
+ test "assert all files created" do
10
+ run_generator
11
+ assert_file "config/initializers/suspicious_login.rb", /config\.warden do \|manager\|\n/
12
+ assert_file "config/locales/suspicious_login.en.yml", /missing_modules:/
13
+ assert_file "config/application.rb", /require 'suspicious_login'/
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ require 'active_support/test_case'
2
+
3
+ @@default_ip = '127.0.0.4'
4
+
5
+ class ActiveSupport::TestCase
6
+ def setup_mailer
7
+ Devise.mailer = Devise::Mailer
8
+ ActionMailer::Base.deliveries = []
9
+ end
10
+ end
11
+
12
+ class ActionDispatch::Request
13
+ def remote_ip
14
+ @@default_ip
15
+ end
16
+ end
@@ -0,0 +1,269 @@
1
+ class SuspiciousMailTest < ActionDispatch::IntegrationTest
2
+ def setup
3
+ setup_mailer
4
+ Devise.mailer = Devise::Mailer
5
+ Devise.mailer_sender = 'test@example.com'
6
+ Devise.clear_token_on_login = true
7
+ @@default_ip = '127.0.0.4'
8
+ end
9
+
10
+ def mail
11
+ @mail ||= begin
12
+ ActionMailer::Base.deliveries.first
13
+ end
14
+ end
15
+
16
+ test 'new user' do
17
+ user = create(:new_user)
18
+
19
+ params = {
20
+ user: {
21
+ email: user.email,
22
+ password: "password"
23
+ }
24
+ }
25
+
26
+ post user_session_path, params: params
27
+ assert_redirected_to root_path
28
+ assert_nil mail
29
+ end
30
+
31
+ test 'user with honest login' do
32
+ user = create(:user_with_honest_login)
33
+
34
+ params = {
35
+ user: {
36
+ email: user.email,
37
+ password: "password"
38
+ }
39
+ }
40
+
41
+ post user_session_path, params: params
42
+ assert_redirected_to root_path
43
+ assert_nil mail
44
+ end
45
+
46
+ test 'user with dormant login from same ip' do
47
+ user = create(:user_with_dormant_login)
48
+
49
+ @@default_ip = '127.0.0.1'
50
+ params = {
51
+ user: {
52
+ email: user.email,
53
+ password: "password"
54
+ }
55
+ }
56
+
57
+ post user_session_path, params: params
58
+ assert_redirected_to root_path
59
+ assert_nil mail
60
+ end
61
+
62
+ test 'user with dormant login from different ip' do
63
+ user = create(:user_with_dormant_login)
64
+
65
+ params = {
66
+ user: {
67
+ email: user.email,
68
+ password: "password"
69
+ }
70
+ }
71
+
72
+ post user_session_path, params: params
73
+ assert_redirected_to new_user_session_path
74
+ assert_not_nil mail
75
+ assert_equal "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.", flash[:alert]
76
+ end
77
+
78
+ test 'user with suspicious login' do
79
+ user = create(:user_with_suspicious_login)
80
+
81
+ params = {
82
+ user: {
83
+ email: user.email,
84
+ password: "password"
85
+ }
86
+ }
87
+
88
+ post user_session_path, params: params
89
+ assert_redirected_to new_user_session_path
90
+ assert_not_nil mail
91
+ assert_equal "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.", flash[:alert]
92
+ end
93
+
94
+ test 'user with dormant login from different ip and recently sent login token' do
95
+ user = create(:user_with_dormant_login_and_recently_sent_login_token)
96
+
97
+ params = {
98
+ user: {
99
+ email: user.email,
100
+ password: "password"
101
+ }
102
+ }
103
+
104
+ post user_session_path, params: params
105
+ assert_redirected_to new_user_session_path
106
+ assert_nil mail
107
+ assert_equal "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.", flash[:alert]
108
+ end
109
+
110
+ test 'user with suspicious login and recently sent login token' do
111
+ user = create(:user_with_suspicious_login_and_recently_sent_login_token)
112
+
113
+ params = {
114
+ user: {
115
+ email: user.email,
116
+ password: "password"
117
+ }
118
+ }
119
+
120
+ post user_session_path, params: params
121
+ assert_redirected_to new_user_session_path
122
+ assert_nil mail
123
+ assert_equal "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.", flash[:alert]
124
+ end
125
+
126
+ test 'user with suspicious login and no token' do
127
+ user = create(:user_with_suspicious_login_and_recently_sent_login_token)
128
+
129
+ get root_path
130
+ assert_redirected_to new_user_session_path
131
+ end
132
+
133
+ test 'user with suspicious login and valid token and config.clear_token_on_login=true' do
134
+ Devise.clear_token_on_login = true
135
+
136
+ user = create(:user_with_suspicious_login_and_recently_sent_login_token)
137
+ params = {
138
+ login_token: "TOKEN",
139
+ email: user.email
140
+ }
141
+ get root_path(params)
142
+ assert_response :success
143
+
144
+ user = User.find(user.id)
145
+ assert_nil user[Devise.token_field_name]
146
+ assert_nil user[Devise.token_created_at_field_name]
147
+ end
148
+
149
+ test 'user with suspicious login and valid token and config.clear_token_on_login=false' do
150
+ Devise.clear_token_on_login = false
151
+
152
+ user = create(:user_with_suspicious_login_and_recently_sent_login_token)
153
+ params = {
154
+ login_token: "TOKEN",
155
+ email: user.email
156
+ }
157
+ get root_path(params)
158
+ assert_response :success
159
+
160
+ user = User.find(user.id)
161
+ assert_equal user[Devise.token_field_name], "TOKEN"
162
+ assert_not_nil user[Devise.token_created_at_field_name]
163
+ end
164
+
165
+ test 'user with suspicious login and valid but expired token' do
166
+ Devise.clear_token_on_login = false
167
+
168
+ user = create(:user_with_suspicious_login_and_ancient_login_token)
169
+ params = {
170
+ login_token: "TOKEN",
171
+ email: user.email
172
+ }
173
+ get root_path(params)
174
+ assert_redirected_to new_user_session_path
175
+
176
+ user = User.find(user.id)
177
+ assert_equal user[Devise.token_field_name], "TOKEN"
178
+ assert_not_nil user[Devise.token_created_at_field_name]
179
+ end
180
+
181
+ test 'user with suspicious login and invalid token and config.clear_token_on_login=true' do
182
+ Devise.clear_token_on_login = true
183
+
184
+ user = create(:user_with_suspicious_login_and_recently_sent_login_token)
185
+ params = {
186
+ login_token: "WRONG TOKEN",
187
+ email: user.email
188
+ }
189
+ get root_path(params)
190
+ assert_redirected_to new_user_session_path
191
+
192
+ user = User.find(user.id)
193
+ assert_equal user[Devise.token_field_name], "TOKEN"
194
+ assert_not_nil user[Devise.token_created_at_field_name]
195
+ end
196
+
197
+ test 'user with suspicious login and invalid token and config.clear_token_on_login=false' do
198
+ Devise.clear_token_on_login = false
199
+
200
+ user = create(:user_with_suspicious_login_and_recently_sent_login_token)
201
+ params = {
202
+ login_token: "WRONG TOKEN",
203
+ email: user.email
204
+ }
205
+ get root_path(params)
206
+ assert_redirected_to new_user_session_path
207
+
208
+ user = User.find(user.id)
209
+ assert_equal user[Devise.token_field_name], "TOKEN"
210
+ assert_not_nil user[Devise.token_created_at_field_name]
211
+ end
212
+
213
+ test 'multiple failed signins does not update trackable data' do
214
+ user = create(:user_with_dormant_login)
215
+
216
+ params = {
217
+ user: {
218
+ email: user.email,
219
+ password: "password"
220
+ }
221
+ }
222
+
223
+ last_sign_in_at = user.last_sign_in_at
224
+ current_sign_in_at = user.current_sign_in_at
225
+ last_sign_in_ip = user.last_sign_in_ip
226
+ current_sign_in_ip = user.current_sign_in_ip
227
+
228
+ post user_session_path, params: params
229
+ assert_redirected_to new_user_session_path
230
+ user.reload
231
+ assert_equal user.last_sign_in_at, last_sign_in_at
232
+ assert_equal user.current_sign_in_at, current_sign_in_at
233
+ assert_equal user.last_sign_in_ip, last_sign_in_ip
234
+ assert_equal user.current_sign_in_ip, current_sign_in_ip
235
+
236
+ post user_session_path, params: params
237
+ assert_redirected_to new_user_session_path
238
+ user.reload
239
+ assert_equal user.last_sign_in_at, last_sign_in_at
240
+ assert_equal user.current_sign_in_at, current_sign_in_at
241
+ assert_equal user.last_sign_in_ip, last_sign_in_ip
242
+ assert_equal user.current_sign_in_ip, current_sign_in_ip
243
+ end
244
+
245
+ test 'successful login updates trackable fields correctly' do
246
+ user = create(:user_with_honest_login)
247
+
248
+ params = {
249
+ user: {
250
+ email: user.email,
251
+ password: "password"
252
+ }
253
+ }
254
+
255
+ last_sign_in_at = user.last_sign_in_at
256
+ current_sign_in_at = user.current_sign_in_at
257
+ last_sign_in_ip = user.last_sign_in_ip
258
+ current_sign_in_ip = user.current_sign_in_ip
259
+
260
+ post user_session_path, params: params
261
+ assert_redirected_to root_path
262
+
263
+ user.reload
264
+ assert_equal user.last_sign_in_at, current_sign_in_at
265
+ assert user.current_sign_in_at > current_sign_in_at
266
+ assert_equal user.last_sign_in_ip, current_sign_in_ip
267
+ assert_equal user.current_sign_in_ip, '127.0.0.4'
268
+ end
269
+ end