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
File without changes
@@ -0,0 +1,4 @@
1
+ You can confirm your account here:
2
+
3
+ http://example.com/confirm/123abc
4
+
@@ -0,0 +1,8 @@
1
+ Your email address was changed just now.
2
+
3
+ Location: 1.2.3.4
4
+ Time: TIMESTAMP
5
+
6
+ If this was you, you don't need to do anything.
7
+
8
+ If this wasn't you, please let us know.
@@ -0,0 +1,8 @@
1
+ Your email was changed just now.
2
+
3
+ Location: 1.2.3.4
4
+ Time: TIMESTAMP
5
+
6
+ If this was you, you don't need to do anything.
7
+
8
+ If this wasn't you, please let us know.
@@ -0,0 +1,8 @@
1
+ Your password was changed just now.
2
+
3
+ Location: 1.2.3.4
4
+ Time: TIMESTAMP
5
+
6
+ If this was you, you don't need to do anything.
7
+
8
+ If this wasn't you, please let us know.
@@ -0,0 +1,8 @@
1
+ Your password was reset just now.
2
+
3
+ Location: 1.2.3.4
4
+ Time: TIMESTAMP
5
+
6
+ If this was you, you don't need to do anything.
7
+
8
+ If this wasn't you, please let us know.
@@ -0,0 +1,8 @@
1
+ Recovery codes for two-factor authentication were generated for your account just now.
2
+
3
+ Location: 1.2.3.4
4
+ Time: TIMESTAMP
5
+
6
+ If this was you, you don't need to do anything.
7
+
8
+ If this wasn't you, please let us know.
@@ -0,0 +1,4 @@
1
+ You can reset your password here:
2
+
3
+ http://example.com/pwd-reset/123abc
4
+
@@ -0,0 +1,6 @@
1
+ Your two-factor authentication code was reused just now.
2
+
3
+ It was rejected and whoever reused it was not able to log in.
4
+
5
+ Location: 1.2.3.4
6
+ Time: TIMESTAMP
@@ -0,0 +1,8 @@
1
+ Two-factor authentication was set up on your account just now.
2
+
3
+ Location: 1.2.3.4
4
+ Time: TIMESTAMP
5
+
6
+ If this was you, you don't need to do anything.
7
+
8
+ If this wasn't you, please let us know.
@@ -0,0 +1,8 @@
1
+ Two-factor authentication was deactivated on your account just now.
2
+
3
+ Location: 1.2.3.4
4
+ Time: TIMESTAMP
5
+
6
+ If this was you, you don't need to do anything.
7
+
8
+ If this wasn't you, please let us know.
@@ -0,0 +1,112 @@
1
+ require 'test_helper'
2
+
3
+ class AccountConfirmationTest < IntegrationTest
4
+
5
+ setup do
6
+ QuoVadis.accounts_require_confirmation true
7
+ end
8
+
9
+
10
+ test 'new signup requiring confirmation' do
11
+ assert_emails 1 do
12
+ post sign_ups_path(user: {name: 'Bob', email: 'bob@example.com', password: '123456789abc'})
13
+ end
14
+ refute QuoVadis::Account.last.confirmed?
15
+
16
+ # verify response
17
+ assert_response :redirect
18
+ follow_redirect!
19
+ assert_equal 'A link to confirm your account has been emailed to you.', flash[:notice]
20
+
21
+ # click link in email
22
+ url = extract_url_from_email
23
+ get url
24
+ assert_response :success
25
+ action_path = extract_path_from_email
26
+ assert_select "form[action='#{action_path}']"
27
+
28
+ # click button on confirmation page
29
+ put action_path
30
+
31
+ # verify logged in
32
+ assert_redirected_to '/articles/secret'
33
+ assert_equal 'Thanks for confirming your account. You are now logged in.', flash[:notice]
34
+ assert controller.logged_in?
35
+ assert QuoVadis::Account.last.confirmed?
36
+ end
37
+
38
+
39
+ test 'resend confirmation email: valid identifier' do
40
+ user = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
41
+
42
+ get quo_vadis.new_confirmation_path
43
+ assert_response :success
44
+
45
+ assert_emails 1 do
46
+ post quo_vadis.confirmations_path(email: 'bob@example.com')
47
+ end
48
+
49
+ assert_redirected_to '/confirmations'
50
+ assert_equal 'A link to confirm your account has been emailed to you.', flash[:notice]
51
+ end
52
+
53
+
54
+ test 'resend confirmation email: unknown identifier' do
55
+ assert_no_emails do
56
+ post quo_vadis.confirmations_path(email: 'bob@example.com')
57
+ end
58
+
59
+ assert_redirected_to quo_vadis.new_confirmation_path
60
+ assert_equal 'Sorry, your account could not be found. Please try again.', flash[:alert]
61
+ end
62
+
63
+
64
+ test 'reusing a token' do
65
+ assert_emails 1 do
66
+ post sign_ups_path(user: {name: 'Bob', email: 'bob@example.com', password: '123456789abc'})
67
+ end
68
+
69
+ put extract_path_from_email
70
+
71
+ assert_redirected_to '/articles/secret'
72
+ assert_equal 'Thanks for confirming your account. You are now logged in.', flash[:notice]
73
+
74
+ put extract_path_from_email
75
+ assert_redirected_to quo_vadis.new_confirmation_path
76
+ assert_equal 'Either the link has expired or your account has already been confirmed.', flash[:alert]
77
+ end
78
+
79
+
80
+ test 'token expired' do
81
+ assert_emails 1 do
82
+ post sign_ups_path(user: {name: 'Bob', email: 'bob@example.com', password: '123456789abc'})
83
+ end
84
+
85
+ travel QuoVadis.account_confirmation_token_lifetime + 1.minute
86
+ get extract_url_from_email
87
+
88
+ assert_redirected_to quo_vadis.new_confirmation_path
89
+ assert_equal 'Either the link has expired or your account has already been confirmed.', flash[:alert]
90
+ end
91
+
92
+
93
+ test 'accounts requiring confirmation cannot log in' do
94
+ user = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
95
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
96
+ assert_redirected_to quo_vadis.new_confirmation_path
97
+ assert_equal 'Please confirm your account first.', flash[:notice]
98
+ refute controller.logged_in?
99
+ end
100
+
101
+
102
+ private
103
+
104
+ def extract_url_from_email
105
+ ActionMailer::Base.deliveries.last.decoded[%r{^http://.*$}, 0]
106
+ end
107
+
108
+ def extract_path_from_email
109
+ extract_url_from_email.sub 'http://www.example.com', ''
110
+ end
111
+
112
+ end
@@ -0,0 +1,280 @@
1
+ require 'test_helper'
2
+
3
+ class ControllerTest < IntegrationTest
4
+
5
+ setup do
6
+ User.first_or_create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
7
+ QuoVadis.two_factor_authentication_mandatory false
8
+ end
9
+
10
+
11
+ teardown do
12
+ QuoVadis.session_lifetime_extend_to_end_of_day false
13
+ QuoVadis.session_idle_timeout :lifetime
14
+ end
15
+
16
+
17
+ test 'require_authentication when not logged in' do
18
+ get secret_articles_path
19
+
20
+ assert_redirected_to quo_vadis.login_path
21
+ assert_equal 'Please log in first.', flash[:notice]
22
+ end
23
+
24
+
25
+ test 'require_authentication when logged in' do
26
+ login
27
+ get secret_articles_path
28
+
29
+ assert_response :success
30
+ assert_equal secret_articles_path, path
31
+ end
32
+
33
+
34
+ test 'require_authentication remembers original path' do
35
+ get also_secret_articles_path
36
+ assert_equal '/articles/also_secret', session[:qv_bookmark]
37
+ end
38
+
39
+
40
+ test 'require_two_factor_authentication when not logged in' do
41
+ QuoVadis.two_factor_authentication_mandatory true
42
+ get very_secret_articles_path
43
+
44
+ assert_redirected_to quo_vadis.login_path
45
+ assert_equal 'Please log in first.', flash[:notice]
46
+ end
47
+
48
+
49
+ test 'require_two_factor_authentication when logged in but second factor not required' do
50
+ QuoVadis.two_factor_authentication_mandatory false
51
+ login
52
+ get very_secret_articles_path
53
+
54
+ assert_response :success
55
+ assert_equal very_secret_articles_path, path
56
+ end
57
+
58
+
59
+ test 'require_two_factor_authentication when logged in and second factor required' do
60
+ QuoVadis.two_factor_authentication_mandatory true
61
+ login
62
+ get very_secret_articles_path
63
+
64
+ assert_redirected_to quo_vadis.challenge_totps_path
65
+ follow_redirect!
66
+
67
+ # This second redirect is part of the totps controller but I think
68
+ # it makes sense to test here.
69
+ assert_redirected_to quo_vadis.new_totp_path
70
+ assert_equal 'Please set up two factor authentication.', flash[:alert]
71
+ end
72
+
73
+
74
+ test 'require_two_factor_authentication when already authenticated with two factors' do
75
+ User.first.qv_account.create_totp! last_used_at: Time.now
76
+ QuoVadis.two_factor_authentication_mandatory true
77
+ login
78
+ QuoVadis::Session.last.authenticated_with_second_factor
79
+
80
+ get very_secret_articles_path
81
+ assert_equal very_secret_articles_path, path
82
+ end
83
+
84
+
85
+ test 'second_factor_required?' do
86
+ # 2FA optional, user has not set up 2nd factor
87
+ QuoVadis.two_factor_authentication_mandatory false
88
+ login
89
+ get articles_path
90
+ assert controller.logged_in?
91
+ refute controller.qv.second_factor_required?
92
+
93
+ # 2FA optional, user has set up 2nd factor
94
+ QuoVadis.two_factor_authentication_mandatory false
95
+ User.first.qv_account.create_totp! last_used_at: Time.now
96
+ login
97
+ get articles_path
98
+ assert controller.logged_in?
99
+ assert controller.qv.second_factor_required?
100
+
101
+ # 2FA mandatory
102
+ QuoVadis.two_factor_authentication_mandatory true
103
+ login
104
+ get articles_path
105
+ assert controller.logged_in?
106
+ assert controller.qv.second_factor_required?
107
+ end
108
+
109
+
110
+ test 'logged_in?' do
111
+ # not logged in
112
+ get articles_path
113
+ refute @controller.logged_in?
114
+
115
+ # logged in
116
+ login
117
+ get articles_path
118
+ assert @controller.logged_in?
119
+
120
+ # session remotely deleted
121
+ QuoVadis::Session.destroy jar.encrypted[QuoVadis.cookie_name]
122
+ get articles_path
123
+ refute @controller.logged_in?
124
+
125
+ # login and logout
126
+ login
127
+ get articles_path
128
+ logout
129
+ refute @controller.logged_in?
130
+ end
131
+
132
+
133
+ test 'login' do
134
+ QuoVadis.two_factor_authentication_mandatory false
135
+
136
+ get articles_path
137
+ session_id = session.id
138
+ assert_nil jar.encrypted[QuoVadis.cookie_name]
139
+
140
+ assert_difference 'QuoVadis::Session.count' do
141
+ # We want to test the controller mixin's login method, not the session
142
+ # controller's login action, i.e. the following:
143
+ #
144
+ # @controller.login user
145
+ #
146
+ # But we store the QuoVadis session id in an encrypted cookie, and we can
147
+ # only access cookies as part of a request-response cycle. So we have to
148
+ # trigger the mixin's login method via the session controller's login action.
149
+ login
150
+ end
151
+
152
+ refute_equal session_id, session.id
153
+ qv_session = QuoVadis::Session.last
154
+ assert_equal qv_session.id, jar.encrypted[QuoVadis.cookie_name]
155
+ end
156
+
157
+
158
+ test 'logout' do
159
+ login
160
+
161
+ session_id = session.id
162
+ qv_session_id = jar.encrypted[QuoVadis.cookie_name]
163
+ assert QuoVadis::Session.exists?(qv_session_id)
164
+
165
+ assert_difference 'QuoVadis::Session.count', -1 do
166
+ # We want to test the controller mixin's logout method, not the session
167
+ # controller's logout action, i.e. the following:
168
+ #
169
+ # @controller.logout
170
+ #
171
+ # But we store the QuoVadis session id in an encrypted cookie, and we can
172
+ # only access cookies as part of a request-response cycle. So we have to
173
+ # trigger the mixin's logout method via the session controller's logout action.
174
+ logout
175
+ end
176
+
177
+ refute_equal session_id, session.id
178
+ assert_nil jar.encrypted[QuoVadis.cookie_name]
179
+ refute QuoVadis::Session.exists?(qv_session_id)
180
+ end
181
+
182
+
183
+ test 'qv_logout_other_sessions' do
184
+ desktop = session_login
185
+ phone = session_login
186
+
187
+ desktop.controller.qv.logout_other_sessions
188
+
189
+ phone.get articles_path
190
+ refute phone.controller.logged_in?
191
+
192
+ desktop.get articles_path
193
+ assert desktop.controller.logged_in?
194
+ end
195
+
196
+
197
+ test 'authenticated_model' do
198
+ # not logged in
199
+ get articles_path
200
+ assert_nil @controller.authenticated_model
201
+
202
+ # logged in
203
+ login
204
+ get articles_path
205
+ assert_equal User.first, @controller.authenticated_model
206
+ end
207
+
208
+
209
+ test 'request_confirmation' do
210
+ get articles_path
211
+
212
+ assert_emails 1 do
213
+ controller.request_confirmation User.last
214
+ end
215
+ assert_equal 'A link to confirm your account has been emailed to you.', flash[:notice]
216
+ end
217
+
218
+
219
+ test 'session lifetime exceeded' do
220
+ QuoVadis.session_lifetime 1.week
221
+ login
222
+
223
+ travel 1.week
224
+ travel (-5).minutes
225
+ get articles_path
226
+ assert controller.logged_in?
227
+
228
+ travel 10.minutes
229
+ get articles_path
230
+ refute controller.logged_in?
231
+ end
232
+
233
+
234
+ test 'session lifetime extended to end of day' do
235
+ QuoVadis.session_lifetime 1.week
236
+ QuoVadis.session_lifetime_extend_to_end_of_day true
237
+ login
238
+
239
+ travel 1.week
240
+ travel 10.minutes
241
+ get articles_path
242
+ assert controller.logged_in?
243
+
244
+ travel 1.day
245
+ get articles_path
246
+ refute controller.logged_in?
247
+ end
248
+
249
+
250
+ test 'idle timeout exceeded' do
251
+ QuoVadis.session_lifetime 1.week
252
+ QuoVadis.session_idle_timeout 1.day
253
+ login
254
+
255
+ get articles_path
256
+ assert controller.logged_in?
257
+
258
+ travel 2.days
259
+
260
+ get articles_path
261
+ refute controller.logged_in?
262
+ end
263
+
264
+
265
+ private
266
+
267
+ def login
268
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
269
+ end
270
+
271
+ def logout
272
+ delete quo_vadis.logout_path
273
+ end
274
+
275
+ def session_login
276
+ open_session do |sess|
277
+ sess.post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
278
+ end
279
+ end
280
+ end