quo_vadis 2.1.11 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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