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,136 @@
1
+ require 'test_helper'
2
+
3
+ class PasswordResetTest < IntegrationTest
4
+
5
+ setup do
6
+ @user = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
7
+ end
8
+
9
+
10
+ test 'new password reset' do
11
+ get quo_vadis.new_password_reset_path
12
+ assert_response :success
13
+ end
14
+
15
+
16
+ test 'unknown identifier' do
17
+ post quo_vadis.password_resets_path(email: 'foo@example.com')
18
+ assert_response :success
19
+ assert_equal 'A link to change your password has been emailed to you.', flash[:notice]
20
+ end
21
+
22
+
23
+ test 'known identifier' do
24
+ assert_emails 1 do
25
+ post quo_vadis.password_resets_path(email: 'bob@example.com')
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]
29
+ end
30
+
31
+
32
+ test 'click link in email' do
33
+ assert_emails 1 do
34
+ post quo_vadis.password_resets_path(email: 'bob@example.com')
35
+ end
36
+ get extract_url_from_email
37
+ assert_response :success
38
+ end
39
+
40
+
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
47
+ 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
+ end
50
+
51
+
52
+ test 'link cannot be reused' do
53
+ assert_emails 1 do
54
+ post quo_vadis.password_resets_path(email: 'bob@example.com')
55
+ end
56
+ put quo_vadis.password_reset_path(extract_token_from_email, password: 'xxxxxxxxxxxx', password_confirmation: 'xxxxxxxxxxxx')
57
+ assert controller.logged_in?
58
+
59
+ get quo_vadis.edit_password_reset_url(extract_token_from_email)
60
+ 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]
62
+ end
63
+
64
+
65
+ test 'new password invalid' do
66
+ digest = @user.qv_account.password.password_digest
67
+
68
+ assert_emails 1 do
69
+ post quo_vadis.password_resets_path(email: 'bob@example.com')
70
+ end
71
+
72
+ assert_no_difference 'QuoVadis::Session.count' do
73
+ put quo_vadis.password_reset_path(extract_token_from_email, password: '', password_confirmation: '')
74
+ end
75
+
76
+ assert_equal digest, @user.qv_account.password.reload.password_digest
77
+ assert_response :success
78
+ assert_equal quo_vadis.password_reset_path(extract_token_from_email), path
79
+ end
80
+
81
+
82
+ 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
+ phone = session_login
89
+
90
+ get articles_url
91
+ refute controller.logged_in?
92
+
93
+ assert_emails 1 do
94
+ post quo_vadis.password_resets_path(email: 'bob@example.com')
95
+ end
96
+
97
+ assert_difference 'QuoVadis::Session.count', (- 2 + 1) do
98
+ put quo_vadis.password_reset_path(extract_token_from_email, password: 'xxxxxxxxxxxx', password_confirmation: 'xxxxxxxxxxxx')
99
+ end
100
+
101
+ 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.
105
+
106
+ phone.get articles_url
107
+ refute phone.controller.logged_in?
108
+
109
+ refute_equal digest, @user.qv_account.password.reload.password_digest
110
+
111
+ assert_redirected_to '/articles/secret'
112
+ assert_equal 'Your password has been changed and you are logged in.', flash[:notice]
113
+ end
114
+
115
+
116
+ private
117
+
118
+ def session_login
119
+ open_session do |sess|
120
+ sess.post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
121
+ end
122
+ end
123
+
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
+
132
+ def extract_token_from_email
133
+ extract_url_from_email[%r{/([^/]*)$}, 1]
134
+ end
135
+
136
+ end
@@ -0,0 +1,48 @@
1
+ require 'test_helper'
2
+
3
+ class RecoveryCodesTest < IntegrationTest
4
+
5
+ setup do
6
+ u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
7
+ u.qv_account.create_totp last_used_at: 1.day.ago
8
+ QuoVadis.two_factor_authentication_mandatory true
9
+ @codes = u.qv_account.generate_recovery_codes
10
+ login
11
+ end
12
+
13
+
14
+ test 'use recovery code' do
15
+ get quo_vadis.challenge_recovery_codes_path
16
+ assert_response :success
17
+
18
+ assert_difference 'QuoVadis::Totp.count', -1 do
19
+ assert_difference 'QuoVadis::RecoveryCode.count', -1 do
20
+ assert_session_replaced do
21
+ post quo_vadis.authenticate_recovery_codes_path(code: @codes.first)
22
+ end
23
+ end
24
+ end
25
+
26
+ assert_nil User.last.qv_account.totp
27
+
28
+ assert_redirected_to '/articles/secret'
29
+
30
+ # use another recovery code to verify another TOTP-reset doesn't error
31
+ post quo_vadis.authenticate_recovery_codes_path(code: @codes[1])
32
+ end
33
+
34
+
35
+ test 'generate recovery codes' do
36
+ assert_emails 1 do
37
+ post quo_vadis.generate_recovery_codes_path
38
+ end
39
+ assert_redirected_to quo_vadis.recovery_codes_path
40
+ end
41
+
42
+
43
+ private
44
+
45
+ def login
46
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
47
+ end
48
+ end
@@ -0,0 +1,86 @@
1
+ require 'test_helper'
2
+
3
+ # sqlite: primary key needs AUTOINCREMENT to not reuse previous values
4
+ # https://www.sqlite.org/faq.html#q1
5
+ # AR should do this by default (but it seems sometimes doesn't)
6
+
7
+ class SessionsTest < IntegrationTest
8
+
9
+ setup do
10
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
11
+ QuoVadis.session_lifetime :session
12
+ QuoVadis.two_factor_authentication_mandatory false
13
+ end
14
+
15
+
16
+ test 'sessions require authentication' do
17
+ get quo_vadis.sessions_path
18
+ assert_redirected_to quo_vadis.login_path
19
+ end
20
+
21
+
22
+ test "user's sessions are independent" do
23
+ desktop = login
24
+ phone = login
25
+
26
+ refute_equal jar(desktop).encrypted[QuoVadis.cookie_name],
27
+ jar(phone).encrypted[QuoVadis.cookie_name]
28
+ end
29
+
30
+
31
+ test 'user can logout without logging out another session' do
32
+ desktop = login
33
+ phone = login
34
+
35
+ # logout on phone
36
+ phone.delete quo_vadis.logout_path
37
+
38
+ # assert phone logged out
39
+ phone.assert_response :redirect
40
+ assert_equal 'You have logged out.', phone.flash[:notice]
41
+ refute jar(phone).encrypted[QuoVadis.cookie_name]
42
+ refute phone.controller.logged_in?
43
+
44
+ # assert desktop still logged in
45
+ assert jar(desktop).encrypted[QuoVadis.cookie_name]
46
+ end
47
+
48
+
49
+ test "user can log out a separate session" do
50
+ desktop = login
51
+ phone = login
52
+
53
+ # on phone, list sessions
54
+ phone.get quo_vadis.sessions_path
55
+ phone.assert_response :success
56
+ phone.assert_select 'td', 'This session'
57
+ phone.assert_select 'td input[type=submit][value="Log out"]', 1
58
+
59
+ # on phone, log out the desktop session
60
+ phone.delete quo_vadis.session_path(QuoVadis::Session.first.id)
61
+ phone.assert_redirected_to quo_vadis.sessions_path
62
+
63
+ # phone is still logged in
64
+ assert_equal 'You have logged out of the other session.', phone.flash[:notice]
65
+
66
+ # desktop is logged out
67
+ desktop.get '/articles'
68
+ refute desktop.controller.logged_in?
69
+ end
70
+
71
+
72
+ private
73
+
74
+ # starts a new rails session and logs in
75
+ def 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
+ # logs in in current session
82
+ def plain_login
83
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
84
+ end
85
+
86
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ class SignUpTest < IntegrationTest
4
+
5
+ test 'successful sign up' do
6
+ assert_difference 'User.count' do
7
+ post "/users", params: {
8
+ user: {
9
+ name: 'Bob',
10
+ email: 'billy@example.com',
11
+ password: '123456789abc',
12
+ password_confirmation: '123456789abc'
13
+ }
14
+ }, headers: { 'HTTP_USER_AGENT' => 'Blah' }
15
+ assert_redirected_to '/articles'
16
+ follow_redirect!
17
+ assert_select 'p', 'Public Articles'
18
+ end
19
+ end
20
+
21
+
22
+ test 'failed sign up' do
23
+ assert_no_difference 'User.count' do
24
+ post "/users", params: {
25
+ user: {
26
+ name: 'Bob',
27
+ email: 'billy@example.com',
28
+ password: 'x'
29
+ }
30
+ }
31
+ assert_response :success
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,96 @@
1
+ require 'test_helper'
2
+
3
+ class TotpsTest < IntegrationTest
4
+
5
+ setup do
6
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
7
+ QuoVadis.two_factor_authentication_mandatory true
8
+ login
9
+ end
10
+
11
+
12
+ test ':challenge redirects to :new when no second factor setup' do
13
+ get quo_vadis.challenge_totps_path
14
+ assert_redirected_to quo_vadis.new_totp_path
15
+ assert_equal 'Please set up two factor authentication.', flash[:alert]
16
+ end
17
+
18
+
19
+ test 'set up second factor' do
20
+ get quo_vadis.new_totp_path
21
+ assert_response :success
22
+
23
+ totp = controller.instance_variable_get :@totp
24
+ assert_emails 1 do
25
+ assert_difference 'QuoVadis::RecoveryCode.count', 5 do
26
+ assert_difference 'QuoVadis::Totp.count' do
27
+ post quo_vadis.totps_path(totp: {
28
+ key: totp.key,
29
+ hmac_key: totp.hmac_key,
30
+ otp: ROTP::TOTP.new(totp.key).now
31
+ })
32
+ end
33
+ end
34
+ end
35
+
36
+ assert_redirected_to quo_vadis.recovery_codes_path
37
+ end
38
+
39
+
40
+ test 'does not set up second factor when key tampered with' do
41
+ get quo_vadis.new_totp_path
42
+
43
+ totp = controller.instance_variable_get :@totp
44
+ assert_no_difference 'QuoVadis::Totp.count' do
45
+ post quo_vadis.totps_path(totp: {
46
+ key: 'dodgy',
47
+ hmac_key: totp.hmac_key,
48
+ otp: ROTP::TOTP.new('dodgy').now
49
+ })
50
+ end
51
+
52
+ assert_equal 'Sorry, the code was incorrect. Please check your system clock is correct and try again.', flash[:alert]
53
+ assert_redirected_to quo_vadis.new_totp_path
54
+ end
55
+
56
+
57
+ test 'authenticate with second factor' do
58
+ totp = User.last.qv_account.create_totp(last_used_at: 1.minute.ago)
59
+
60
+ get quo_vadis.challenge_totps_path
61
+ assert_response :success
62
+
63
+ assert_session_replaced do
64
+ post quo_vadis.authenticate_totps_path(totp: ROTP::TOTP.new(totp.key).now)
65
+ end
66
+ assert QuoVadis::Session.last.second_factor_authenticated?
67
+ assert_equal 'Welcome back!', flash[:notice]
68
+ assert_redirected_to '/articles/secret'
69
+ end
70
+
71
+
72
+ test 'failed authentication with second factor' do
73
+ User.last.qv_account.create_totp(last_used_at: 1.minute.ago)
74
+
75
+ post quo_vadis.authenticate_totps_path(totp: '123456')
76
+ refute QuoVadis::Session.last.second_factor_authenticated?
77
+ assert_response :success
78
+ assert_equal 'Sorry, the code was incorrect. Please check your system clock is correct and try again.', flash[:alert]
79
+ end
80
+
81
+
82
+ test '2fa code reused' do
83
+ totp = User.last.qv_account.create_totp(last_used_at: 1.minute.ago)
84
+ post quo_vadis.authenticate_totps_path(totp: ROTP::TOTP.new(totp.key).now)
85
+ assert_emails 1 do
86
+ post quo_vadis.authenticate_totps_path(totp: ROTP::TOTP.new(totp.key).now)
87
+ end
88
+ end
89
+
90
+
91
+ private
92
+
93
+ def login
94
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
95
+ end
96
+ end
@@ -0,0 +1,82 @@
1
+ require 'test_helper'
2
+
3
+ class TwofaTest < IntegrationTest
4
+
5
+ setup do
6
+ u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
7
+ @account = u.qv_account
8
+ QuoVadis.two_factor_authentication_mandatory true
9
+ # @codes = u.qv_account.generate_recovery_codes
10
+ login
11
+ end
12
+
13
+
14
+ test 'no totp, no recovery codes' do
15
+ get quo_vadis.twofa_path
16
+ assert_response :success
17
+ assert_select "a[href=?]", quo_vadis.new_totp_path
18
+ assert_select "a[href=?]", quo_vadis.recovery_codes_path
19
+ end
20
+
21
+
22
+ test 'totp, no recovery codes' do
23
+ setup_totp
24
+ get quo_vadis.twofa_path
25
+ assert_select 'form[action=?]', quo_vadis.twofa_path do
26
+ assert_select 'input[value=delete]'
27
+ end
28
+ assert_select "a[href=?]", quo_vadis.recovery_codes_path
29
+ end
30
+
31
+
32
+ test 'no totp, recovery codes' do
33
+ setup_recovery_codes
34
+ get quo_vadis.twofa_path
35
+ assert_select "a[href=?]", quo_vadis.new_totp_path
36
+ assert_select "a[href=?]", quo_vadis.recovery_codes_path
37
+ end
38
+
39
+
40
+ test 'totp, recovery codes' do
41
+ setup_totp
42
+ setup_recovery_codes
43
+ get quo_vadis.twofa_path
44
+ assert_select 'form[action=?]', quo_vadis.twofa_path do
45
+ assert_select 'input[value=delete]'
46
+ end
47
+ assert_select "a[href=?]", quo_vadis.recovery_codes_path
48
+ end
49
+
50
+
51
+ test 'deactivate' do
52
+ setup_totp
53
+ setup_recovery_codes
54
+
55
+ assert_emails 1 do
56
+ delete quo_vadis.twofa_path
57
+ end
58
+
59
+ # The flash only seems to be set before the redirect. No idea why.
60
+ assert_equal 'You have invalidated your 2FA credentials and recovery codes.', flash[:notice]
61
+ @account.reload
62
+ assert_nil @account.totp
63
+ assert_empty @account.recovery_codes
64
+ @account.sessions.each { |s| refute s.second_factor_authenticated? }
65
+ assert_redirected_to quo_vadis.twofa_path
66
+ end
67
+
68
+
69
+ private
70
+
71
+ def login
72
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
73
+ end
74
+
75
+ def setup_totp
76
+ @account.create_totp last_used_at: 1.day.ago
77
+ end
78
+
79
+ def setup_recovery_codes
80
+ @account.generate_recovery_codes
81
+ end
82
+ end