quo_vadis 2.1.11 → 2.2.0

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +50 -99
  4. data/app/controllers/quo_vadis/confirmations_controller.rb +26 -61
  5. data/app/controllers/quo_vadis/password_resets_controller.rb +64 -32
  6. data/app/controllers/quo_vadis/sessions_controller.rb +0 -5
  7. data/app/mailers/quo_vadis/mailer.rb +2 -2
  8. data/app/models/quo_vadis/account.rb +32 -0
  9. data/app/models/quo_vadis/password.rb +6 -1
  10. data/app/views/quo_vadis/confirmations/new.html.erb +19 -7
  11. data/app/views/quo_vadis/mailer/account_confirmation.text.erb +2 -3
  12. data/app/views/quo_vadis/mailer/reset_password.text.erb +2 -2
  13. data/app/views/quo_vadis/password_resets/edit.html.erb +13 -1
  14. data/app/views/quo_vadis/password_resets/new.html.erb +1 -1
  15. data/config/locales/quo_vadis.en.yml +9 -7
  16. data/config/routes.rb +4 -12
  17. data/lib/quo_vadis/controller.rb +20 -10
  18. data/lib/quo_vadis/defaults.rb +2 -2
  19. data/lib/quo_vadis/version.rb +1 -1
  20. data/lib/quo_vadis.rb +2 -2
  21. data/test/dummy/app/controllers/sign_ups_controller.rb +2 -11
  22. data/test/fixtures/quo_vadis/mailer/account_confirmation.text +2 -3
  23. data/test/fixtures/quo_vadis/mailer/reset_password.text +2 -2
  24. data/test/integration/account_confirmation_test.rb +42 -86
  25. data/test/integration/controller_test.rb +8 -8
  26. data/test/integration/logging_test.rb +31 -7
  27. data/test/integration/password_login_test.rb +1 -1
  28. data/test/integration/password_reset_test.rb +89 -54
  29. data/test/mailers/mailer_test.rb +2 -2
  30. data/test/models/account_test.rb +48 -0
  31. data/test/models/session_test.rb +4 -0
  32. metadata +2 -10
  33. data/app/models/quo_vadis/account_confirmation_token.rb +0 -17
  34. data/app/models/quo_vadis/password_reset_token.rb +0 -17
  35. data/app/models/quo_vadis/token.rb +0 -42
  36. data/app/views/quo_vadis/confirmations/edit.html.erb +0 -10
  37. data/app/views/quo_vadis/confirmations/edit_email.html.erb +0 -14
  38. data/app/views/quo_vadis/confirmations/index.html.erb +0 -14
  39. data/app/views/quo_vadis/password_resets/index.html.erb +0 -5
  40. data/test/models/token_test.rb +0 -70
@@ -14,102 +14,144 @@ class PasswordResetTest < IntegrationTest
14
14
 
15
15
 
16
16
  test 'unknown identifier' do
17
- post quo_vadis.password_resets_path(email: 'foo@example.com')
18
- assert_redirected_to quo_vadis.password_resets_path
19
- assert_equal 'A link to change your password has been emailed to you.', flash[:notice]
17
+ post quo_vadis.password_reset_path(email: 'foo@example.com')
18
+ assert_redirected_to quo_vadis.edit_password_reset_path
19
+ assert_equal 'Please check your email for your reset code.', flash[:notice]
20
20
  end
21
21
 
22
22
 
23
23
  test 'known identifier' do
24
24
  assert_emails 1 do
25
- post quo_vadis.password_resets_path(email: 'bob@example.com')
25
+ post quo_vadis.password_reset_path(email: 'bob@example.com')
26
26
  end
27
- assert_redirected_to quo_vadis.password_resets_path
28
- assert_equal 'A link to change your password has been emailed to you.', flash[:notice]
27
+ assert_redirected_to quo_vadis.edit_password_reset_path
28
+ assert_equal 'Please check your email for your reset code.', flash[:notice]
29
29
  end
30
30
 
31
31
 
32
- test 'click link in email' do
32
+ test 'reset code expired' do
33
33
  assert_emails 1 do
34
- post quo_vadis.password_resets_path(email: 'bob@example.com')
34
+ post quo_vadis.password_reset_path(email: 'bob@example.com')
35
+ follow_redirect!
35
36
  end
36
- get extract_url_from_email
37
- assert_response :success
38
- end
39
37
 
38
+ travel QuoVadis.password_reset_otp_lifetime + 1.minute
40
39
 
41
- test 'expired link' do
42
- assert_emails 1 do
43
- post quo_vadis.password_resets_path(email: 'bob@example.com')
44
- end
45
- travel QuoVadis.password_reset_token_lifetime + 1.minute
46
- get extract_url_from_email
40
+ # type in reset code from email
41
+ code = extract_code_from_email
42
+ put quo_vadis.password_reset_path(password: {
43
+ otp: code,
44
+ password: 'secretsecret',
45
+ password_confirmation: 'secretsecret',
46
+ })
47
+
48
+ assert_equal 'Your reset code has expired. Please request another one.', flash[:alert]
47
49
  assert_redirected_to quo_vadis.new_password_reset_path
48
- assert_equal 'Either the link has expired or you have already reset your password.', flash[:alert]
49
50
  end
50
51
 
51
52
 
52
- test 'link cannot be reused' do
53
+ test 'reset code incorrect' do
53
54
  assert_emails 1 do
54
- post quo_vadis.password_resets_path(email: 'bob@example.com')
55
+ post quo_vadis.password_reset_path(email: 'bob@example.com')
56
+ follow_redirect!
55
57
  end
56
- put quo_vadis.password_reset_path(extract_token_from_email, password: {password: 'xxxxxxxxxxxx', password_confirmation: 'xxxxxxxxxxxx'})
57
- assert controller.logged_in?
58
58
 
59
- get quo_vadis.password_reset_url(extract_token_from_email)
59
+ # type in reset code from email
60
+ put quo_vadis.password_reset_path(password: {
61
+ otp: '000000',
62
+ password: 'secretsecret',
63
+ password_confirmation: 'secretsecret',
64
+ })
65
+
60
66
  assert_redirected_to quo_vadis.new_password_reset_path
61
- assert_equal 'Either the link has expired or you have already reset your password.', flash[:alert]
67
+ assert_equal 'Sorry, the code was incorrect. Please try again.', flash[:alert]
62
68
  end
63
69
 
64
70
 
65
71
  test 'new password invalid' do
66
- digest = @user.qv_account.password.password_digest
67
-
68
72
  assert_emails 1 do
69
- post quo_vadis.password_resets_path(email: 'bob@example.com')
73
+ post quo_vadis.password_reset_path(email: 'bob@example.com')
74
+ follow_redirect!
70
75
  end
71
76
 
77
+ # type in reset code from email
78
+ code = extract_code_from_email
72
79
  assert_no_difference 'QuoVadis::Session.count' do
73
- put quo_vadis.password_reset_path(extract_token_from_email, password: {password: '', password_confirmation: ''})
80
+ put quo_vadis.password_reset_path(password: {
81
+ otp: code,
82
+ password: 'secret',
83
+ password_confirmation: 'secret',
84
+ })
74
85
  end
75
86
 
76
- assert_equal digest, @user.qv_account.password.reload.password_digest
77
- assert_response :unprocessable_entity
78
- assert_equal quo_vadis.password_reset_path(extract_token_from_email), path
87
+ assert_response 422
88
+ assert_nil flash[:notice]
89
+ refute controller.logged_in?
79
90
  end
80
91
 
81
92
 
82
93
  test 'new password valid' do
83
- QuoVadis.two_factor_authentication_mandatory false
84
-
85
- digest = @user.qv_account.password.password_digest
86
-
87
- desktop = session_login
88
94
  phone = session_login
95
+ tablet = session_login
89
96
 
90
97
  get articles_url
91
98
  refute controller.logged_in?
92
99
 
93
100
  assert_emails 1 do
94
- post quo_vadis.password_resets_path(email: 'bob@example.com')
101
+ post quo_vadis.password_reset_path(email: 'bob@example.com')
102
+ follow_redirect!
95
103
  end
96
104
 
97
- assert_difference 'QuoVadis::Session.count', (- 2 + 1) do
98
- put quo_vadis.password_reset_path(extract_token_from_email, password: {password: 'xxxxxxxxxxxx', password_confirmation: 'xxxxxxxxxxxx'})
105
+ # type in reset code from email
106
+ code = extract_code_from_email
107
+ # deletes phone and tablet sessions and creates new session
108
+ assert_difference 'QuoVadis::Session.count', -1 do
109
+ put quo_vadis.password_reset_path(password: {
110
+ otp: code,
111
+ password: 'secretsecret',
112
+ password_confirmation: 'secretsecret',
113
+ })
99
114
  end
100
115
 
116
+ assert_equal 'Your password has been changed and you are logged in.', flash[:notice]
101
117
  assert controller.logged_in?
102
-
103
- desktop.get articles_url
104
- refute desktop.controller.logged_in? # NOTE: flaky; if this fails, re-migrate the database.
118
+ assert_redirected_to '/articles/secret'
105
119
 
106
120
  phone.get articles_url
107
121
  refute phone.controller.logged_in?
108
122
 
109
- refute_equal digest, @user.qv_account.password.reload.password_digest
123
+ tablet.get articles_url
124
+ refute tablet.controller.logged_in?
125
+ end
110
126
 
111
- assert_redirected_to '/articles/secret'
112
- assert_equal 'Your password has been changed and you are logged in.', flash[:notice]
127
+
128
+ test 'reset code cannot be reused' do
129
+ assert_emails 1 do
130
+ post quo_vadis.password_reset_path(email: 'bob@example.com')
131
+ follow_redirect!
132
+ end
133
+
134
+ # type in reset code from email
135
+ code = extract_code_from_email
136
+ put quo_vadis.password_reset_path(password: {
137
+ otp: code,
138
+ password: 'secretsecret',
139
+ password_confirmation: 'secretsecret',
140
+ })
141
+
142
+ # The password-reset process is now finished.
143
+ # Test what happens if reset code is resubmitted with a different password.
144
+
145
+ put quo_vadis.password_reset_path(password: {
146
+ otp: code,
147
+ password: 'foobarfoobar',
148
+ password_confirmation: 'foobarfoobar',
149
+ })
150
+
151
+ assert_redirected_to quo_vadis.new_password_reset_path
152
+ assert_nil flash[:alert]
153
+ refute @user.qv_account.password.authenticate('foobarfoobar')
154
+ assert @user.qv_account.password.authenticate('123456789abc')
113
155
  end
114
156
 
115
157
 
@@ -121,16 +163,9 @@ class PasswordResetTest < IntegrationTest
121
163
  end
122
164
  end
123
165
 
124
- def extract_url_from_email
125
- ActionMailer::Base.deliveries.last.decoded[%r{^http://.*$}, 0]
126
- end
127
-
128
- def extract_path_from_email
129
- extract_url_from_email.sub 'http://www.example.com', ''
130
- end
131
166
 
132
- def extract_token_from_email
133
- extract_url_from_email[%r{/([^/]*)$}, 1]
167
+ def extract_code_from_email
168
+ ActionMailer::Base.deliveries.last.decoded[%r{\d{6}}, 0]
134
169
  end
135
170
 
136
171
  end
@@ -12,7 +12,7 @@ class MailerTest < ActionMailer::TestCase
12
12
  test 'reset_password' do
13
13
  email = QuoVadis::Mailer.with(
14
14
  email: 'Foo <foo@example.com>',
15
- url: 'http://example.com/pwd-reset/123abc'
15
+ otp: '314159'
16
16
  ).reset_password
17
17
 
18
18
  assert_emails 1 do
@@ -29,7 +29,7 @@ class MailerTest < ActionMailer::TestCase
29
29
  test 'account_confirmation' do
30
30
  email = QuoVadis::Mailer.with(
31
31
  email: 'Foo <foo@example.com>',
32
- url: 'http://example.com/confirm/123abc'
32
+ otp: 271828
33
33
  ).account_confirmation
34
34
 
35
35
  assert_emails 1 do
@@ -53,4 +53,52 @@ class AccountTest < ActiveSupport::TestCase
53
53
  assert_empty account.sessions
54
54
  end
55
55
 
56
+
57
+ test 'otp_for_confirmation' do
58
+ u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
59
+ account = u.qv_account
60
+
61
+ otp = account.otp_for_confirmation(1)
62
+ assert_match /^\d{6}$/, otp
63
+ refute_equal otp, account.otp_for_confirmation(2)
64
+ end
65
+
66
+
67
+ test 'confirm' do
68
+ u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
69
+ account = u.qv_account
70
+
71
+ otp = account.otp_for_confirmation(1)
72
+ refute account.confirm('000000', 1)
73
+ refute account.confirm(otp, 2)
74
+
75
+ assert account.confirm(otp, 1)
76
+ assert account.confirmed?
77
+ end
78
+
79
+
80
+ test 'otp_for_password_reset' do
81
+ u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
82
+ account = u.qv_account
83
+
84
+ otp = account.otp_for_password_reset(1)
85
+ assert_match /^\d{6}$/, otp
86
+ refute_equal otp, account.otp_for_password_reset(2)
87
+
88
+ account.password.change('123456789abc', 'secretsecret', 'secretsecret')
89
+ refute_equal otp, account.otp_for_password_reset(1)
90
+ end
91
+
92
+
93
+ test 'verify_password_reset' do
94
+ u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
95
+ account = u.qv_account
96
+
97
+ otp = account.otp_for_password_reset(1)
98
+ refute account.verify_password_reset('000000', 1)
99
+ refute account.verify_password_reset(otp, 2)
100
+
101
+ assert account.verify_password_reset(otp, 1)
102
+ end
103
+
56
104
  end
@@ -2,6 +2,10 @@ require 'test_helper'
2
2
 
3
3
  class SessionTest < ActiveSupport::TestCase
4
4
 
5
+ teardown do
6
+ QuoVadis.session_idle_timeout :lifetime
7
+ end
8
+
5
9
  test 'expired?' do
6
10
  refute QuoVadis::Session.new.expired?
7
11
  assert QuoVadis::Session.new(lifetime_expires_at: 1.day.ago).expired?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quo_vadis
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.11
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Stewart
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-09-14 00:00:00.000000000 Z
11
+ date: 2023-04-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -90,17 +90,11 @@ files:
90
90
  - app/controllers/quo_vadis/twofas_controller.rb
91
91
  - app/mailers/quo_vadis/mailer.rb
92
92
  - app/models/quo_vadis/account.rb
93
- - app/models/quo_vadis/account_confirmation_token.rb
94
93
  - app/models/quo_vadis/log.rb
95
94
  - app/models/quo_vadis/password.rb
96
- - app/models/quo_vadis/password_reset_token.rb
97
95
  - app/models/quo_vadis/recovery_code.rb
98
96
  - app/models/quo_vadis/session.rb
99
- - app/models/quo_vadis/token.rb
100
97
  - app/models/quo_vadis/totp.rb
101
- - app/views/quo_vadis/confirmations/edit.html.erb
102
- - app/views/quo_vadis/confirmations/edit_email.html.erb
103
- - app/views/quo_vadis/confirmations/index.html.erb
104
98
  - app/views/quo_vadis/confirmations/new.html.erb
105
99
  - app/views/quo_vadis/logs/index.html.erb
106
100
  - app/views/quo_vadis/mailer/account_confirmation.text.erb
@@ -114,7 +108,6 @@ files:
114
108
  - app/views/quo_vadis/mailer/totp_setup_notification.text.erb
115
109
  - app/views/quo_vadis/mailer/twofa_deactivated_notification.text.erb
116
110
  - app/views/quo_vadis/password_resets/edit.html.erb
117
- - app/views/quo_vadis/password_resets/index.html.erb
118
111
  - app/views/quo_vadis/password_resets/new.html.erb
119
112
  - app/views/quo_vadis/passwords/edit.html.erb
120
113
  - app/views/quo_vadis/recovery_codes/challenge.html.erb
@@ -203,7 +196,6 @@ files:
203
196
  - test/models/password_test.rb
204
197
  - test/models/recovery_code_test.rb
205
198
  - test/models/session_test.rb
206
- - test/models/token_test.rb
207
199
  - test/models/totp_test.rb
208
200
  - test/quo_vadis_test.rb
209
201
  - test/test_helper.rb
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module QuoVadis
4
- class AccountConfirmationToken < Token
5
- class << self
6
-
7
- def expires_at
8
- QuoVadis.account_confirmation_token_lifetime.from_now.to_i
9
- end
10
-
11
- def data_for_hmac(data, account)
12
- "#{data}-#{account.confirmed?}"
13
- end
14
-
15
- end
16
- end
17
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module QuoVadis
4
- class PasswordResetToken < Token
5
- class << self
6
-
7
- def expires_at
8
- QuoVadis.password_reset_token_lifetime.from_now.to_i
9
- end
10
-
11
- def data_for_hmac(data, account)
12
- "#{data}-#{account.password.password_digest}"
13
- end
14
-
15
- end
16
- end
17
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module QuoVadis
4
- class Token
5
- extend Hmacable
6
-
7
- class << self
8
-
9
- def generate(account)
10
- public_data = "#{account.id}-#{expires_at}"
11
- data = data_for_hmac public_data, account
12
- "#{public_data}--#{compute_hmac(data)}"
13
- end
14
-
15
- def find_account(token)
16
- provided_public_data, provided_hmac = token.split '--'
17
- id, expires_at = provided_public_data.split '-'
18
- account = Account.find id
19
- data = data_for_hmac provided_public_data, account
20
- actual_hmac = compute_hmac data
21
- return nil unless timing_safe_eql? provided_hmac, actual_hmac
22
- return nil if expires_at.to_i < Time.current.to_i
23
- account
24
- rescue
25
- nil
26
- end
27
-
28
- private
29
-
30
- attr_reader :account
31
-
32
- def expires_at
33
- raise NotImplementedError
34
- end
35
-
36
- def data_for_hmac(public_data, account)
37
- raise NotImplementedError
38
- end
39
-
40
- end
41
- end
42
- end
@@ -1,10 +0,0 @@
1
- <h1>Confirm account</h1>
2
-
3
- <%= form_with url: confirmation_path(params[:token]), method: :put do |f| %>
4
-
5
- <p>
6
- <label>Please click the button to confirm your account:</label>
7
- <%= f.submit %>
8
- </p>
9
-
10
- <% end %>
@@ -1,14 +0,0 @@
1
- <h1>Account confirmation: change email</h1>
2
-
3
- <p>Please update your email address.</p>
4
-
5
- <%= form_with url: update_email_confirmations_path, method: :put do |f| %>
6
- <p>
7
- <%= f.label :email %>
8
- <%= f.text_field :email, value: @email, inputmode: 'email', autocomplete: 'email' %>
9
- </p>
10
-
11
- <p>
12
- <%= f.submit 'Update my email address and send me a new confirmation email' %>
13
- </p>
14
- <% end %>
@@ -1,14 +0,0 @@
1
- <h1>Account confirmation</h1>
2
-
3
- <% if @account %>
4
- <p>We have sent an email to <%= @account.model.email %>.</p>
5
-
6
- <p>Wrong address? <%= link_to 'Change it', edit_email_confirmations_path %>.</p>
7
-
8
- <p>Didn't receive it? <%= button_to 'Get another one', resend_confirmations_path %></p>
9
-
10
- <% else %>
11
- <p>We have sent an email to you.</p>
12
-
13
- <p><%= link_to 'Request a new email', new_confirmation_path %></p>
14
- <% end %>
@@ -1,5 +0,0 @@
1
- <h1>Password reset</h1>
2
-
3
- <p>Please check your email.</p>
4
-
5
- <p><%= link_to 'Request a new email', new_password_reset_path %></p>
@@ -1,70 +0,0 @@
1
- require 'test_helper'
2
-
3
- class TokenTest < ActiveSupport::TestCase
4
-
5
- setup do
6
- u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
7
- @account = u.qv_account
8
- end
9
-
10
-
11
- test 'account confirmation' do
12
- token = QuoVadis::AccountConfirmationToken.generate @account
13
- assert_match /^\d+-\d+--\h+$/, token
14
- assert_equal @account, QuoVadis::AccountConfirmationToken.find_account(token)
15
- end
16
-
17
- test 'account confirmation expired' do
18
- token = QuoVadis::AccountConfirmationToken.generate @account
19
- travel QuoVadis.account_confirmation_token_lifetime + 1.second
20
- assert_nil QuoVadis::AccountConfirmationToken.find_account(token)
21
- end
22
-
23
- test 'account confirmation already done' do
24
- token = QuoVadis::AccountConfirmationToken.generate @account
25
- @account.confirmed!
26
- assert_nil QuoVadis::AccountConfirmationToken.find_account(token)
27
- end
28
-
29
- test 'account confirmation token tampered with' do
30
- assert_nil QuoVadis::AccountConfirmationToken.find_account(nil)
31
- assert_nil QuoVadis::AccountConfirmationToken.find_account('')
32
- assert_nil QuoVadis::AccountConfirmationToken.find_account('asdf')
33
-
34
- token = QuoVadis::AccountConfirmationToken.generate @account
35
- id, expires_at, hash = token.match(/^(\d+)-(\d+)--(\h+)$/).captures
36
- fake_token = "#{id}-#{expires_at.to_i + 1}--#{hash}"
37
- assert_nil QuoVadis::AccountConfirmationToken.find_account(fake_token)
38
- end
39
-
40
-
41
- test 'password reset' do
42
- token = QuoVadis::PasswordResetToken.generate @account
43
- assert_match /^\d+-\d+--\h+$/, token
44
- assert_equal @account, QuoVadis::PasswordResetToken.find_account(token)
45
- end
46
-
47
- test 'password reset expired' do
48
- token = QuoVadis::PasswordResetToken.generate @account
49
- travel QuoVadis.password_reset_token_lifetime + 1.second
50
- assert_nil QuoVadis::PasswordResetToken.find_account(token)
51
- end
52
-
53
- test 'password reset already done' do
54
- token = QuoVadis::PasswordResetToken.generate @account
55
- @account.password.reset 'secretsecret', 'secretsecret'
56
- assert_nil QuoVadis::PasswordResetToken.find_account(token)
57
- end
58
-
59
- test 'password reset token tampered with' do
60
- assert_nil QuoVadis::PasswordResetToken.find_account(nil)
61
- assert_nil QuoVadis::PasswordResetToken.find_account('')
62
- assert_nil QuoVadis::PasswordResetToken.find_account('asdf')
63
-
64
- token = QuoVadis::PasswordResetToken.generate @account
65
- id, expires_at, hash = token.match(/^(\d+)-(\d+)--(\h+)$/).captures
66
- fake_token = "#{id}-#{expires_at.to_i + 1}--#{hash}"
67
- assert_nil QuoVadis::PasswordResetToken.find_account(fake_token)
68
- end
69
-
70
- end