quo_vadis 2.0.0 → 2.0.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 (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