quo_vadis 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile +0 -3
  5. data/README.md +4 -5
  6. data/lib/quo_vadis/version.rb +1 -1
  7. data/quo_vadis.gemspec +5 -3
  8. data/test/dummy/README.markdown +1 -0
  9. data/test/dummy/Rakefile +3 -0
  10. data/test/dummy/app/controllers/application_controller.rb +2 -0
  11. data/test/dummy/app/controllers/articles_controller.rb +17 -0
  12. data/test/dummy/app/controllers/sign_ups_controller.rb +42 -0
  13. data/test/dummy/app/controllers/users_controller.rb +25 -0
  14. data/test/dummy/app/models/application_record.rb +3 -0
  15. data/test/dummy/app/models/article.rb +3 -0
  16. data/test/dummy/app/models/person.rb +6 -0
  17. data/test/dummy/app/models/user.rb +6 -0
  18. data/test/dummy/app/views/articles/also_secret.html.erb +1 -0
  19. data/test/dummy/app/views/articles/index.html.erb +1 -0
  20. data/test/dummy/app/views/articles/secret.html.erb +1 -0
  21. data/test/dummy/app/views/articles/very_secret.html.erb +2 -0
  22. data/test/dummy/app/views/layouts/application.html.erb +46 -0
  23. data/test/dummy/app/views/quo_vadis/confirmations/edit.html.erb +10 -0
  24. data/test/dummy/app/views/quo_vadis/confirmations/index.html.erb +5 -0
  25. data/test/dummy/app/views/quo_vadis/confirmations/new.html.erb +16 -0
  26. data/test/dummy/app/views/quo_vadis/logs/index.html.erb +28 -0
  27. data/test/dummy/app/views/quo_vadis/mailer/account_confirmation.text.erb +4 -0
  28. data/test/dummy/app/views/quo_vadis/mailer/email_change_notification.text.erb +8 -0
  29. data/test/dummy/app/views/quo_vadis/mailer/identifier_change_notification.text.erb +8 -0
  30. data/test/dummy/app/views/quo_vadis/mailer/password_change_notification.text.erb +8 -0
  31. data/test/dummy/app/views/quo_vadis/mailer/password_reset_notification.text.erb +8 -0
  32. data/test/dummy/app/views/quo_vadis/mailer/recovery_codes_generation_notification.text.erb +8 -0
  33. data/test/dummy/app/views/quo_vadis/mailer/reset_password.text.erb +4 -0
  34. data/test/dummy/app/views/quo_vadis/mailer/totp_reuse_notification.text.erb +6 -0
  35. data/test/dummy/app/views/quo_vadis/mailer/totp_setup_notification.text.erb +8 -0
  36. data/test/dummy/app/views/quo_vadis/mailer/twofa_deactivated_notification.text.erb +8 -0
  37. data/test/dummy/app/views/quo_vadis/password_resets/edit.html.erb +25 -0
  38. data/test/dummy/app/views/quo_vadis/password_resets/index.html.erb +5 -0
  39. data/test/dummy/app/views/quo_vadis/password_resets/new.html.erb +12 -0
  40. data/test/dummy/app/views/quo_vadis/passwords/edit.html.erb +30 -0
  41. data/test/dummy/app/views/quo_vadis/recovery_codes/challenge.html.erb +11 -0
  42. data/test/dummy/app/views/quo_vadis/recovery_codes/index.html.erb +25 -0
  43. data/test/dummy/app/views/quo_vadis/sessions/index.html.erb +26 -0
  44. data/test/dummy/app/views/quo_vadis/sessions/new.html.erb +24 -0
  45. data/test/dummy/app/views/quo_vadis/totps/challenge.html.erb +11 -0
  46. data/test/dummy/app/views/quo_vadis/totps/new.html.erb +17 -0
  47. data/test/dummy/app/views/quo_vadis/twofas/show.html.erb +20 -0
  48. data/test/dummy/app/views/sign_ups/new.html.erb +37 -0
  49. data/test/dummy/app/views/sign_ups/show.html.erb +5 -0
  50. data/test/dummy/app/views/users/new.html.erb +37 -0
  51. data/test/dummy/config.ru +7 -0
  52. data/test/dummy/config/application.rb +30 -0
  53. data/test/dummy/config/boot.rb +4 -0
  54. data/test/dummy/config/database.yml +10 -0
  55. data/test/dummy/config/environment.rb +4 -0
  56. data/test/dummy/config/initializers/quo_vadis.rb +7 -0
  57. data/test/dummy/config/routes.rb +13 -0
  58. data/test/dummy/db/migrate/202102121932_create_users.rb +10 -0
  59. data/test/dummy/db/migrate/202102121935_create_people.rb +10 -0
  60. data/test/dummy/db/schema.rb +92 -0
  61. data/test/dummy/public/favicon.ico +0 -0
  62. data/test/fixtures/quo_vadis/mailer/account_confirmation.text +4 -0
  63. data/test/fixtures/quo_vadis/mailer/email_change_notification.text +8 -0
  64. data/test/fixtures/quo_vadis/mailer/identifier_change_notification.text +8 -0
  65. data/test/fixtures/quo_vadis/mailer/password_change_notification.text +8 -0
  66. data/test/fixtures/quo_vadis/mailer/password_reset_notification.text +8 -0
  67. data/test/fixtures/quo_vadis/mailer/recovery_codes_generation_notification.text +8 -0
  68. data/test/fixtures/quo_vadis/mailer/reset_password.text +4 -0
  69. data/test/fixtures/quo_vadis/mailer/totp_reuse_notification.text +6 -0
  70. data/test/fixtures/quo_vadis/mailer/totp_setup_notification.text +8 -0
  71. data/test/fixtures/quo_vadis/mailer/twofa_deactivated_notification.text +8 -0
  72. data/test/integration/account_confirmation_test.rb +112 -0
  73. data/test/integration/controller_test.rb +280 -0
  74. data/test/integration/logging_test.rb +235 -0
  75. data/test/integration/password_change_test.rb +93 -0
  76. data/test/integration/password_login_test.rb +125 -0
  77. data/test/integration/password_reset_test.rb +136 -0
  78. data/test/integration/recovery_codes_test.rb +48 -0
  79. data/test/integration/sessions_test.rb +86 -0
  80. data/test/integration/sign_up_test.rb +35 -0
  81. data/test/integration/totps_test.rb +96 -0
  82. data/test/integration/twofa_test.rb +82 -0
  83. data/test/mailers/mailer_test.rb +200 -0
  84. data/test/models/account_test.rb +34 -0
  85. data/test/models/crypt_test.rb +22 -0
  86. data/test/models/log_test.rb +16 -0
  87. data/test/models/mask_ip_test.rb +27 -0
  88. data/test/models/model_test.rb +66 -0
  89. data/test/models/password_test.rb +163 -0
  90. data/test/models/recovery_code_test.rb +54 -0
  91. data/test/models/session_test.rb +67 -0
  92. data/test/models/token_test.rb +70 -0
  93. data/test/models/totp_test.rb +68 -0
  94. data/test/quo_vadis_test.rb +43 -0
  95. data/test/test_helper.rb +58 -0
  96. metadata +119 -4
  97. data/Gemfile.lock +0 -178
@@ -0,0 +1,235 @@
1
+ require 'test_helper'
2
+
3
+ class LoggingTest < IntegrationTest
4
+
5
+ setup do
6
+ user = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
7
+ @account = user.qv_account
8
+ end
9
+
10
+
11
+ test 'logs endpoint' do
12
+ QuoVadis::Log.create account: @account, action: 'password.change', ip: '1.2.3.4', metadata: {foo: 'bar', baz: 'qux'}
13
+ login
14
+ get quo_vadis.logs_path
15
+ assert_response :success
16
+
17
+ assert_select 'tbody tr' do
18
+ assert_select 'td', 'password.change'
19
+ assert_select 'td', '1.2.3.4'
20
+ assert_select 'td', 'foo: bar, baz: qux'
21
+ end
22
+
23
+ assert_select 'tbody tr' do
24
+ assert_select 'td', 'login.success'
25
+ assert_select 'td', '127.0.0.1'
26
+ assert_select 'td', ''
27
+ end
28
+ end
29
+
30
+
31
+ test 'login.success' do
32
+ assert_log QuoVadis::Log::LOGIN_SUCCESS do
33
+ login
34
+ end
35
+ end
36
+
37
+
38
+ test 'login.failure' do
39
+ assert_log QuoVadis::Log::LOGIN_FAILURE do
40
+ post quo_vadis.login_path(email: 'bob@example.com', password: 'wrong')
41
+ end
42
+ end
43
+
44
+
45
+ test 'login.unknown' do
46
+ assert_log QuoVadis::Log::LOGIN_UNKNOWN, {}, nil do
47
+ post quo_vadis.login_path(email: 'wrong', password: 'wrong')
48
+ end
49
+ end
50
+
51
+
52
+ test 'totp.setup' do
53
+ login
54
+ get quo_vadis.new_totp_path
55
+ totp = controller.instance_variable_get :@totp
56
+ assert_log QuoVadis::Log::TOTP_SETUP do
57
+ post quo_vadis.totps_path(totp: {
58
+ key: totp.key,
59
+ hmac_key: totp.hmac_key,
60
+ otp: ROTP::TOTP.new(totp.key).now
61
+ })
62
+ end
63
+ end
64
+
65
+
66
+ test 'totp.success' do
67
+ login
68
+ totp = User.last.qv_account.create_totp(last_used_at: 1.minute.ago)
69
+ assert_log QuoVadis::Log::TOTP_SUCCESS do
70
+ post quo_vadis.authenticate_totps_path(totp: ROTP::TOTP.new(totp.key).now)
71
+ end
72
+ end
73
+
74
+
75
+ test 'totp.failure' do
76
+ login
77
+ User.last.qv_account.create_totp(last_used_at: 1.minute.ago)
78
+ assert_log QuoVadis::Log::TOTP_FAILURE do
79
+ post quo_vadis.authenticate_totps_path(totp: '000000')
80
+ end
81
+ end
82
+
83
+
84
+ test 'totp.reuse' do
85
+ login
86
+ totp = User.last.qv_account.create_totp(last_used_at: 1.minute.ago)
87
+ post quo_vadis.authenticate_totps_path(totp: ROTP::TOTP.new(totp.key).now)
88
+ assert_log QuoVadis::Log::TOTP_REUSE do
89
+ post quo_vadis.authenticate_totps_path(totp: ROTP::TOTP.new(totp.key).now)
90
+ end
91
+ end
92
+
93
+
94
+ test 'recovery_code.success' do
95
+ login
96
+ codes = @account.generate_recovery_codes
97
+ assert_log QuoVadis::Log::RECOVERY_CODE_SUCCESS do
98
+ post quo_vadis.authenticate_recovery_codes_path(code: codes.first)
99
+ end
100
+ end
101
+
102
+
103
+ test 'recovery_code.failure' do
104
+ login
105
+ assert_log QuoVadis::Log::RECOVERY_CODE_FAILURE do
106
+ post quo_vadis.authenticate_recovery_codes_path(code: 'nope')
107
+ end
108
+ end
109
+
110
+
111
+ test 'recovery_code.generate' do
112
+ login
113
+ assert_log QuoVadis::Log::RECOVERY_CODE_GENERATE do
114
+ post quo_vadis.generate_recovery_codes_path
115
+ end
116
+ end
117
+
118
+
119
+ test '2fa.deactivated' do
120
+ login
121
+ assert_log QuoVadis::Log::TWOFA_DEACTIVATED do
122
+ delete quo_vadis.twofa_path
123
+ end
124
+ end
125
+
126
+
127
+ test 'identifier.change on account' do
128
+ assert_log QuoVadis::Log::IDENTIFIER_CHANGE, {'from' => 'bob@example.com', 'to' => 'x'} do
129
+ QuoVadis::CurrentRequestDetails.set(ip: '127.0.0.1') do
130
+ @account.update identifier: 'x'
131
+ end
132
+ end
133
+ end
134
+
135
+
136
+ test 'email.change aka identifier.change on model' do
137
+ # In our setup the identifier is the email so we expect changing the
138
+ # email to change the identifier too.
139
+ assert_difference 'QuoVadis::Log.count', 2 do
140
+ QuoVadis::CurrentRequestDetails.set(ip: '127.0.0.1') do
141
+ @account.model.update email: 'x'
142
+ end
143
+ end
144
+ log = QuoVadis::Log.first
145
+ assert_equal @account, log.account
146
+ assert_equal QuoVadis::Log::IDENTIFIER_CHANGE, log.action
147
+ assert_equal '127.0.0.1', log.ip
148
+ assert_equal({'from' => 'bob@example.com', 'to' => 'x'}, log.metadata)
149
+ log = QuoVadis::Log.last
150
+ assert_equal @account, log.account
151
+ assert_equal QuoVadis::Log::EMAIL_CHANGE, log.action
152
+ assert_equal '127.0.0.1', log.ip
153
+ assert_equal({'from' => 'bob@example.com', 'to' => 'x'}, log.metadata)
154
+ end
155
+
156
+
157
+ test 'password.change' do
158
+ login
159
+ assert_log QuoVadis::Log::PASSWORD_CHANGE do
160
+ put quo_vadis.password_path(password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'xxxxxxxxxxxx')
161
+ end
162
+ end
163
+
164
+
165
+ test 'password.reset' do
166
+ assert_difference 'QuoVadis::Log.count', 2 do
167
+ token = QuoVadis::PasswordResetToken.generate @account
168
+ put quo_vadis.password_reset_path(token, password: 'xxxxxxxxxxxx', password_confirmation: 'xxxxxxxxxxxx')
169
+ end
170
+ assert_equal QuoVadis::Log::PASSWORD_RESET, QuoVadis::Log.first.action
171
+ assert_equal QuoVadis::Log::LOGIN_SUCCESS, log.action
172
+ end
173
+
174
+
175
+ test 'account.confirmation' do
176
+ assert_difference 'QuoVadis::Log.count', 2 do
177
+ token = QuoVadis::AccountConfirmationToken.generate @account
178
+ put quo_vadis.confirmation_path(token)
179
+ end
180
+ assert_equal QuoVadis::Log::ACCOUNT_CONFIRMATION, QuoVadis::Log.first.action
181
+ assert_equal QuoVadis::Log::LOGIN_SUCCESS, log.action
182
+ end
183
+
184
+
185
+ test 'logout.other' do
186
+ login_new_session
187
+ phone = login_new_session
188
+
189
+ # logout first session from phone
190
+ assert_log QuoVadis::Log::LOGOUT_OTHER do
191
+ phone.delete quo_vadis.session_path(QuoVadis::Session.first.id)
192
+ end
193
+ end
194
+
195
+
196
+ test 'logout' do
197
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
198
+ assert_log QuoVadis::Log::LOGOUT do
199
+ delete quo_vadis.logout_path
200
+ end
201
+ end
202
+
203
+
204
+ private
205
+
206
+ def assert_log(action, metadata = {}, account = @account, &block)
207
+ assert_difference 'QuoVadis::Log.count' do
208
+ yield
209
+ end
210
+
211
+ if account.nil?
212
+ assert_nil log.account
213
+ else
214
+ assert_equal account, log.account
215
+ end
216
+ assert_equal action, log.action
217
+ assert_equal '127.0.0.1', log.ip
218
+ assert_equal metadata, log.metadata
219
+ end
220
+
221
+ def log
222
+ QuoVadis::Log.last
223
+ end
224
+
225
+ def login
226
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
227
+ end
228
+
229
+ def login_new_session
230
+ open_session do |sess|
231
+ sess.post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
232
+ end
233
+ end
234
+
235
+ end
@@ -0,0 +1,93 @@
1
+ require 'test_helper'
2
+
3
+ class PasswordChangeTest < IntegrationTest
4
+
5
+ setup do
6
+ QuoVadis.two_factor_authentication_mandatory false
7
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
8
+ login
9
+ end
10
+
11
+
12
+ test 'requires login' do
13
+ logout
14
+
15
+ put quo_vadis.password_path
16
+ assert_redirected_to quo_vadis.login_path
17
+ end
18
+
19
+
20
+ test 'incorrect password' do
21
+ put quo_vadis.password_path(password: 'x')
22
+ assert_response :success
23
+ assert_equal ['is incorrect'], password_instance.errors[:password]
24
+ end
25
+
26
+
27
+ test 'new password empty' do
28
+ put quo_vadis.password_path(password: '123456789abc', new_password: '')
29
+ assert_response :success
30
+ assert_equal ["can't be blank"], password_instance.errors[:new_password]
31
+ end
32
+
33
+
34
+ test 'new password too short' do
35
+ put quo_vadis.password_path(password: '123456789abc', new_password: 'x')
36
+ assert_response :success
37
+ assert_equal ["is too short (minimum is #{QuoVadis.password_minimum_length} characters)"], password_instance.errors[:new_password]
38
+ end
39
+
40
+
41
+ test 'new password confirmation does not match' do
42
+ put quo_vadis.password_path(password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'y')
43
+ assert_response :success
44
+ assert_equal ["doesn't match Password"], password_instance.errors[:new_password_confirmation]
45
+ end
46
+
47
+
48
+ test 'success' do
49
+ assert_emails 1 do
50
+ assert_session_replaced do
51
+ put quo_vadis.password_path(password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'xxxxxxxxxxxx')
52
+ assert_response :redirect
53
+ assert_equal 'Your password has been changed.', flash[:notice]
54
+ end
55
+ end
56
+ end
57
+
58
+
59
+ test 'logs out other sessions' do
60
+ desktop = session_login
61
+ phone = session_login
62
+
63
+ desktop.put quo_vadis.password_path(password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'xxxxxxxxxxxx')
64
+ desktop.follow_redirect!
65
+ assert desktop.controller.logged_in?
66
+
67
+ phone.get articles_path
68
+ refute phone.controller.logged_in?
69
+ end
70
+
71
+
72
+ private
73
+
74
+ # starts a new rails session and logs in
75
+ def session_login
76
+ open_session do |sess|
77
+ sess.post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
78
+ end
79
+ end
80
+
81
+ def login
82
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
83
+ end
84
+
85
+ def logout
86
+ delete quo_vadis.logout_path
87
+ end
88
+
89
+ def password_instance
90
+ controller.instance_variable_get :@password
91
+ end
92
+
93
+ end
@@ -0,0 +1,125 @@
1
+ require 'test_helper'
2
+
3
+ class PasswordLoginTest < IntegrationTest
4
+
5
+ setup do
6
+ QuoVadis.session_lifetime :session
7
+ end
8
+
9
+
10
+ test 'successful login' do
11
+ get quo_vadis.login_path
12
+ assert_response :success
13
+
14
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
15
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
16
+
17
+ assert_redirected_to secret_articles_path
18
+ assert_equal after_login_path, secret_articles_path
19
+ end
20
+
21
+
22
+ test 'successful login redirects to original path' do
23
+ get also_secret_articles_path
24
+
25
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
26
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
27
+
28
+ assert_redirected_to also_secret_articles_path
29
+ assert_nil session[:qv_bookmark]
30
+ end
31
+
32
+
33
+ test 'failed login' do
34
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
35
+ post quo_vadis.login_path(email: 'bob@example.com', password: 'wrong')
36
+
37
+ assert_response :success
38
+ assert_equal quo_vadis.login_path, path
39
+ end
40
+
41
+
42
+ test 'unknown login' do
43
+ post quo_vadis.login_path(email: 'bob@example.com', password: 'wrong')
44
+
45
+ assert_response :success
46
+ assert_equal quo_vadis.login_path, path
47
+ end
48
+
49
+
50
+ test 'logout' do
51
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
52
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
53
+
54
+ # logout
55
+ assert jar.encrypted[QuoVadis.cookie_name]
56
+ assert_difference 'QuoVadis::Session.count', -1 do
57
+ delete quo_vadis.logout_path
58
+ end
59
+ refute jar.encrypted[QuoVadis.cookie_name]
60
+ assert_redirected_to root_path
61
+ end
62
+
63
+
64
+ test 'login for browser session' do
65
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
66
+
67
+ open_session do |sess|
68
+ sess.post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
69
+ assert sess.controller.logged_in?
70
+ end
71
+
72
+ open_session do |sess|
73
+ sess.get articles_path
74
+ refute sess.controller.logged_in?
75
+ end
76
+ end
77
+
78
+
79
+ # Ideally we would use multiple sessions to distinguish this from a single
80
+ # browser session. We would log in in one session then assert we can log in
81
+ # in another session within the timeframe. But I can't find a way to share
82
+ # a cookie between test sessions (I tried nesting sessions). So we do all
83
+ # this within a single session, even though it has the same effect as being
84
+ # within a single browser session.
85
+ test 'login for 1 week' do
86
+ QuoVadis.session_lifetime 1.week
87
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
88
+
89
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
90
+ refute QuoVadis::Session.last.send(:browser_session?)
91
+ assert controller.logged_in?
92
+
93
+ travel 5.days
94
+
95
+ get articles_path
96
+ assert controller.logged_in? # flakey
97
+
98
+ travel 3.days
99
+
100
+ get articles_path
101
+ refute controller.logged_in?
102
+ end
103
+
104
+
105
+ test 'optional remember not opted in' do
106
+ QuoVadis.session_lifetime 1.week
107
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
108
+
109
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc', remember: '0')
110
+ assert QuoVadis::Session.last.send(:browser_session?)
111
+
112
+ # Cannot test this fully without being able to share cookies between test sessions.
113
+ end
114
+
115
+
116
+ test 'optional remember opted in' do
117
+ QuoVadis.session_lifetime 1.week
118
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
119
+
120
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc', remember: '1')
121
+ refute QuoVadis::Session.last.send(:browser_session?)
122
+
123
+ # Cannot test this fully without being able to share cookies between test sessions.
124
+ end
125
+ end