rodauth 1.19.1 → 1.20.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +72 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +100 -7
  5. data/doc/base.rdoc +25 -0
  6. data/doc/email_auth.rdoc +1 -1
  7. data/doc/email_base.rdoc +5 -1
  8. data/doc/internals.rdoc +2 -2
  9. data/doc/jwt_refresh.rdoc +35 -0
  10. data/doc/lockout.rdoc +3 -0
  11. data/doc/login_password_requirements_base.rdoc +4 -1
  12. data/doc/otp.rdoc +22 -39
  13. data/doc/recovery_codes.rdoc +15 -28
  14. data/doc/release_notes/1.20.0.txt +175 -0
  15. data/doc/remember.rdoc +3 -0
  16. data/doc/reset_password.rdoc +2 -1
  17. data/doc/single_session.rdoc +3 -0
  18. data/doc/verify_account.rdoc +4 -3
  19. data/doc/verify_login_change.rdoc +1 -1
  20. data/lib/rodauth.rb +33 -4
  21. data/lib/rodauth/features/base.rb +93 -10
  22. data/lib/rodauth/features/change_login.rb +1 -1
  23. data/lib/rodauth/features/confirm_password.rb +1 -1
  24. data/lib/rodauth/features/create_account.rb +2 -2
  25. data/lib/rodauth/features/disallow_password_reuse.rb +5 -3
  26. data/lib/rodauth/features/email_auth.rb +4 -2
  27. data/lib/rodauth/features/email_base.rb +12 -6
  28. data/lib/rodauth/features/jwt.rb +9 -0
  29. data/lib/rodauth/features/jwt_refresh.rb +142 -0
  30. data/lib/rodauth/features/lockout.rb +8 -4
  31. data/lib/rodauth/features/login_password_requirements_base.rb +1 -0
  32. data/lib/rodauth/features/otp.rb +63 -6
  33. data/lib/rodauth/features/recovery_codes.rb +1 -0
  34. data/lib/rodauth/features/remember.rb +20 -2
  35. data/lib/rodauth/features/reset_password.rb +5 -2
  36. data/lib/rodauth/features/single_session.rb +15 -2
  37. data/lib/rodauth/features/verify_account.rb +11 -6
  38. data/lib/rodauth/features/verify_login_change.rb +5 -3
  39. data/lib/rodauth/version.rb +2 -2
  40. data/spec/disallow_password_reuse_spec.rb +115 -28
  41. data/spec/email_auth_spec.rb +2 -2
  42. data/spec/jwt_refresh_spec.rb +256 -0
  43. data/spec/lockout_spec.rb +4 -4
  44. data/spec/login_spec.rb +52 -11
  45. data/spec/migrate/001_tables.rb +10 -0
  46. data/spec/migrate_travis/001_tables.rb +8 -0
  47. data/spec/remember_spec.rb +27 -0
  48. data/spec/reset_password_spec.rb +2 -2
  49. data/spec/rodauth_spec.rb +25 -1
  50. data/spec/single_session_spec.rb +20 -0
  51. data/spec/spec_helper.rb +29 -0
  52. data/spec/two_factor_spec.rb +57 -3
  53. data/spec/verify_account_spec.rb +18 -1
  54. data/spec/verify_login_change_spec.rb +2 -2
  55. data/templates/add-recovery-codes.str +1 -1
  56. data/templates/change-password.str +2 -2
  57. data/templates/login-confirm-field.str +2 -2
  58. data/templates/login-field.str +2 -2
  59. data/templates/otp-auth-code-field.str +2 -2
  60. data/templates/otp-setup.str +4 -3
  61. data/templates/password-confirm-field.str +2 -2
  62. data/templates/password-field.str +2 -2
  63. data/templates/recovery-auth.str +2 -2
  64. data/templates/reset-password-request.str +1 -1
  65. data/templates/sms-code-field.str +2 -2
  66. data/templates/sms-setup.str +2 -2
  67. data/templates/unlock-account-request.str +1 -1
  68. data/templates/unlock-account.str +1 -1
  69. data/templates/verify-account-resend.str +1 -1
  70. metadata +15 -5
@@ -32,6 +32,7 @@ module Rodauth
32
32
  view 'recovery-codes', 'View Authentication Recovery Codes', 'recovery_codes'
33
33
 
34
34
  auth_value_method :add_recovery_codes_param, 'add'
35
+ auth_value_method :add_recovery_codes_heading, '<h2>Add Additional Recovery Codes</h2>'
35
36
  auth_value_method :invalid_recovery_code_message, "Invalid recovery code"
36
37
  auth_value_method :recovery_codes_limit, 16
37
38
  auth_value_method :recovery_codes_column, :code
@@ -16,6 +16,7 @@ module Rodauth
16
16
  after 'load_memory'
17
17
  redirect
18
18
 
19
+ auth_value_method :raw_remember_token_deadline, nil
19
20
  auth_value_method :remember_cookie_options, {}
20
21
  auth_value_method :extend_remember_deadline?, false
21
22
  auth_value_method :remember_period, {:days=>14}
@@ -88,7 +89,22 @@ module Rodauth
88
89
  id, key = cookie.split('_', 2)
89
90
  return unless id && key
90
91
 
91
- unless (actual = active_remember_key_ds(id).get(remember_key_column)) && timing_safe_eql?(key, actual)
92
+ actual, deadline = active_remember_key_ds(id).get([remember_key_column, remember_deadline_column])
93
+ unless actual
94
+ forget_login
95
+ return
96
+ end
97
+
98
+ if hmac_secret
99
+ unless valid = timing_safe_eql?(key, compute_hmac(actual))
100
+ unless raw_remember_token_deadline && raw_remember_token_deadline > convert_timestamp(deadline)
101
+ forget_login
102
+ return
103
+ end
104
+ end
105
+ end
106
+
107
+ unless valid || timing_safe_eql?(key, actual)
92
108
  forget_login
93
109
  return
94
110
  end
@@ -117,7 +133,9 @@ module Rodauth
117
133
  def remember_login
118
134
  get_remember_key
119
135
  opts = Hash[remember_cookie_options]
120
- opts[:value] = "#{account_id}_#{remember_key_value}"
136
+ key = remember_key_value
137
+ key = compute_hmac(key) if hmac_secret
138
+ opts[:value] = "#{account_id}_#{key}"
121
139
  opts[:expires] = convert_timestamp(active_remember_key_ds.get(remember_deadline_column))
122
140
  ::Rack::Utils.set_cookie_header!(response.headers, remember_cookie_key, opts)
123
141
  end
@@ -4,11 +4,14 @@ module Rodauth
4
4
  Feature.define(:reset_password, :ResetPassword) do
5
5
  depends :login, :email_base, :login_password_requirements_base
6
6
 
7
+ def_deprecated_alias :no_matching_reset_password_key_error_flash, :no_matching_reset_password_key_message
8
+
7
9
  notice_flash "Your password has been reset"
8
10
  notice_flash "An email has been sent to you with a link to reset the password for your account", 'reset_password_email_sent'
9
11
  error_flash "There was an error resetting your password"
10
12
  error_flash "There was an error requesting a password reset", 'reset_password_request'
11
13
  error_flash "An email has recently been sent to you with a link to reset your password", 'reset_password_email_recently_sent'
14
+ error_flash "There was an error resetting your password: invalid or expired password reset key", 'no_matching_reset_password_key'
12
15
  loaded_templates %w'reset-password-request reset-password password-field password-confirm-field reset-password-email'
13
16
  view 'reset-password', 'Reset Password'
14
17
  view 'reset-password-request', 'Request Password Reset', 'reset_password_request'
@@ -26,7 +29,6 @@ module Rodauth
26
29
 
27
30
  auth_value_method :reset_password_deadline_column, :deadline
28
31
  auth_value_method :reset_password_deadline_interval, {:days=>1}
29
- auth_value_method :no_matching_reset_password_key_message, "invalid password reset key"
30
32
  auth_value_method :reset_password_email_subject, 'Reset Password'
31
33
  auth_value_method :reset_password_key_param, 'key'
32
34
  auth_value_method :reset_password_autologin?, false
@@ -34,6 +36,7 @@ module Rodauth
34
36
  auth_value_method :reset_password_id_column, :id
35
37
  auth_value_method :reset_password_key_column, :key
36
38
  auth_value_method :reset_password_email_last_sent_column, nil
39
+ auth_value_method :reset_password_explanatory_text, "<p>If you have forgotten your password, you can request a password reset:</p>"
37
40
  auth_value_method :reset_password_skip_resend_email_within, 300
38
41
  session_key :reset_password_session_key, :reset_password_key
39
42
 
@@ -105,7 +108,7 @@ module Rodauth
105
108
  reset_password_view
106
109
  else
107
110
  session[reset_password_session_key] = nil
108
- set_redirect_error_flash no_matching_reset_password_key_message
111
+ set_redirect_error_flash no_matching_reset_password_key_error_flash
109
112
  redirect require_login_redirect
110
113
  end
111
114
  end
@@ -5,6 +5,7 @@ module Rodauth
5
5
  error_flash 'This session has been logged out as another session has become active'
6
6
  redirect
7
7
 
8
+ auth_value_method :allow_raw_single_session_key?, false
8
9
  auth_value_method :single_session_id_column, :id
9
10
  auth_value_method :single_session_key_column, :key
10
11
  session_key :single_session_session_key, :single_session_key
@@ -35,7 +36,14 @@ module Rodauth
35
36
  end
36
37
  true
37
38
  elsif current_key
38
- timing_safe_eql?(single_session_key, current_key)
39
+ if hmac_secret
40
+ valid = timing_safe_eql?(single_session_key, compute_hmac(current_key))
41
+ if !valid && !allow_raw_single_session_key?
42
+ return false
43
+ end
44
+ end
45
+
46
+ valid || timing_safe_eql?(single_session_key, current_key)
39
47
  end
40
48
  end
41
49
 
@@ -53,7 +61,7 @@ module Rodauth
53
61
 
54
62
  def update_single_session_key
55
63
  key = random_key
56
- set_session_value(single_session_session_key, key)
64
+ set_single_session_key(key)
57
65
  if single_session_ds.update(single_session_key_column=>key) == 0
58
66
  # Don't handle uniqueness violations here. While we could get the stored key from the
59
67
  # database, it could lead to two sessions sharing the same key, which this feature is
@@ -74,6 +82,11 @@ module Rodauth
74
82
  super if defined?(super)
75
83
  end
76
84
 
85
+ def set_single_session_key(data)
86
+ data = compute_hmac(data) if hmac_secret
87
+ set_session_value(single_session_session_key, data)
88
+ end
89
+
77
90
  def update_session
78
91
  super
79
92
  update_single_session_key
@@ -4,9 +4,16 @@ module Rodauth
4
4
  Feature.define(:verify_account, :VerifyAccount) do
5
5
  depends :login, :create_account, :email_base
6
6
 
7
+ def_deprecated_alias :attempt_to_create_unverified_account_error_flash, :attempt_to_create_unverified_account_notice_message
8
+ def_deprecated_alias :attempt_to_login_to_unverified_account_error_flash, :attempt_to_login_to_unverified_account_notice_message
9
+ def_deprecated_alias :no_matching_verify_account_key_error_flash, :no_matching_verify_account_key_message
10
+
7
11
  error_flash "Unable to verify account"
8
12
  error_flash "Unable to resend verify account email", 'verify_account_resend'
9
13
  error_flash "An email has recently been sent to you with a link to verify your account", 'verify_account_email_recently_sent'
14
+ error_flash "There was an error verifying your account: invalid verify account key", 'no_matching_verify_account_key'
15
+ error_flash "The account you tried to create is currently awaiting verification", 'attempt_to_create_unverified_account'
16
+ error_flash "The account you tried to login with is currently awaiting verification", 'attempt_to_login_to_unverified_account'
10
17
  notice_flash "Your account has been verified"
11
18
  notice_flash "An email has been sent to you with a link to verify your account", 'verify_account_email_sent'
12
19
  loaded_templates %w'verify-account verify-account-resend verify-account-email'
@@ -24,9 +31,6 @@ module Rodauth
24
31
  redirect(:verify_account_email_sent){default_post_email_redirect}
25
32
  redirect(:verify_account_email_recently_sent){default_post_email_redirect}
26
33
 
27
- auth_value_method :no_matching_verify_account_key_message, "invalid verify account key"
28
- auth_value_method :attempt_to_create_unverified_account_notice_message, "The account you tried to create is currently awaiting verification"
29
- auth_value_method :attempt_to_login_to_unverified_account_notice_message, "The account you tried to login with is currently awaiting verification"
30
34
  auth_value_method :verify_account_email_subject, 'Verify Account'
31
35
  auth_value_method :verify_account_key_param, 'key'
32
36
  auth_value_method :verify_account_autologin?, true
@@ -35,6 +39,7 @@ module Rodauth
35
39
  auth_value_method :verify_account_email_last_sent_column, nil
36
40
  auth_value_method :verify_account_skip_resend_email_within, 300
37
41
  auth_value_method :verify_account_key_column, :key
42
+ auth_value_method :verify_account_resend_explanatory_text, "<p>If you no longer have the email to verify the account, you can request that it be resent to you:</p>"
38
43
  session_key :verify_account_session_key, :verify_account_key
39
44
  auth_value_method :verify_account_set_password?, false
40
45
 
@@ -102,7 +107,7 @@ module Rodauth
102
107
  verify_account_view
103
108
  else
104
109
  session[verify_account_session_key] = nil
105
- set_redirect_error_flash no_matching_verify_account_key_message
110
+ set_redirect_error_flash no_matching_verify_account_key_error_flash
106
111
  redirect require_login_redirect
107
112
  end
108
113
  end
@@ -180,7 +185,7 @@ module Rodauth
180
185
  def new_account(login)
181
186
  if account_from_login(login) && allow_resending_verify_account_email?
182
187
  set_redirect_error_status(unopen_account_error_status)
183
- set_error_flash attempt_to_create_unverified_account_notice_message
188
+ set_error_flash attempt_to_create_unverified_account_error_flash
184
189
  response.write resend_verify_account_view
185
190
  request.halt
186
191
  end
@@ -251,7 +256,7 @@ module Rodauth
251
256
  def before_login_attempt
252
257
  unless open_account?
253
258
  set_redirect_error_status(unopen_account_error_status)
254
- set_error_flash attempt_to_login_to_unverified_account_notice_message
259
+ set_error_flash attempt_to_login_to_unverified_account_error_flash
255
260
  response.write resend_verify_account_view
256
261
  request.halt
257
262
  end
@@ -4,8 +4,11 @@ module Rodauth
4
4
  Feature.define(:verify_login_change, :VerifyLoginChange) do
5
5
  depends :change_login, :email_base
6
6
 
7
+ def_deprecated_alias :no_matching_verify_login_change_key_error_flash, :no_matching_verify_login_change_key_message
8
+
7
9
  error_flash "Unable to verify login change"
8
10
  error_flash "Unable to change login as there is already an account with the new login", :verify_login_change_duplicate_account
11
+ error_flash "There was an error verifying your login change: invalid verify login change key", 'no_matching_verify_login_change_key'
9
12
  notice_flash "Your login change has been verified"
10
13
  loaded_templates %w'verify-login-change verify-login-change-email'
11
14
  view 'verify-login-change', 'Verify Login Change'
@@ -18,7 +21,6 @@ module Rodauth
18
21
  redirect
19
22
  redirect(:verify_login_change_duplicate_account){require_login_redirect}
20
23
 
21
- auth_value_method :no_matching_verify_login_change_key_message, "invalid verify login change key"
22
24
  auth_value_method :verify_login_change_autologin?, false
23
25
  auth_value_method :verify_login_change_deadline_column, :deadline
24
26
  auth_value_method :verify_login_change_deadline_interval, {:days=>1}
@@ -64,7 +66,7 @@ module Rodauth
64
66
  verify_login_change_view
65
67
  else
66
68
  session[verify_login_change_session_key] = nil
67
- set_redirect_error_flash no_matching_verify_login_change_key_message
69
+ set_redirect_error_flash no_matching_verify_login_change_key_error_flash
68
70
  redirect require_login_redirect
69
71
  end
70
72
  end
@@ -147,7 +149,7 @@ module Rodauth
147
149
 
148
150
  def update_login(login)
149
151
  if _account_from_login(login)
150
- @login_requirement_message = 'already an account with this login'
152
+ @login_requirement_message = already_an_account_with_this_login_message
151
153
  return false
152
154
  end
153
155
 
@@ -6,11 +6,11 @@ module Rodauth
6
6
  MAJOR = 1
7
7
 
8
8
  # The minor version of Rodauth, updated for new feature releases of Rodauth.
9
- MINOR = 19
9
+ MINOR = 20
10
10
 
11
11
  # The patch version of Rodauth, updated only for bug fixes from the last
12
12
  # feature release.
13
- TINY = 1
13
+ TINY = 0
14
14
 
15
15
  # The full version of Rodauth as a string
16
16
  VERSION = "#{MAJOR}.#{MINOR}.#{TINY}".freeze
@@ -54,39 +54,126 @@ describe 'Rodauth disallow_password_reuse feature' do
54
54
  DB[table].get{count(:id)}.must_equal 0
55
55
  end
56
56
 
57
- it "should handle create account when account_password_hash_column is true" do
58
- rodauth do
59
- enable :login, :create_account, :change_password, :disallow_password_reuse
60
- if ENV['RODAUTH_SEPARATE_SCHEMA']
61
- previous_password_hash_table Sequel[:rodauth_test_password][:account_previous_password_hashes]
57
+ [true, false].each do |ph|
58
+ it "should handle create account when account_password_hash_column is #{ph}" do
59
+ rodauth do
60
+ enable :login, :create_account, :change_password, :disallow_password_reuse
61
+ if ENV['RODAUTH_SEPARATE_SCHEMA']
62
+ previous_password_hash_table Sequel[:rodauth_test_password][:account_previous_password_hashes]
63
+ end
64
+ account_password_hash_column :ph if ph
65
+ change_password_requires_password? false
62
66
  end
63
- account_password_hash_column :ph
64
- change_password_requires_password? false
67
+ roda do |r|
68
+ r.rodauth
69
+ r.root{view :content=>""}
70
+ end
71
+
72
+ visit '/create-account'
73
+ fill_in 'Login', :with=>'bar@example.com'
74
+ fill_in 'Confirm Login', :with=>'bar@example.com'
75
+ fill_in 'Password', :with=>'0123456789'
76
+ fill_in 'Confirm Password', :with=>'0123456789'
77
+ click_button 'Create Account'
78
+ page.current_path.must_equal '/'
79
+ page.find('#notice_flash').text.must_equal "Your account has been created"
80
+
81
+ visit '/change-password'
82
+ fill_in 'New Password', :with=>"012345678"
83
+ fill_in 'Confirm Password', :with=>"012345678"
84
+ click_button 'Change Password'
85
+ page.find('#notice_flash').text.must_equal "Your password has been changed"
86
+
87
+ visit '/change-password'
88
+ fill_in 'New Password', :with=>"0123456789"
89
+ fill_in 'Confirm Password', :with=>"0123456789"
90
+ click_button 'Change Password'
91
+ page.html.must_include("invalid password, does not meet requirements (same as previous password)")
65
92
  end
66
- roda do |r|
67
- r.rodauth
68
- r.root{view :content=>""}
93
+
94
+ it "should handle verify account when account_password_hash_column is #{ph}" do
95
+ rodauth do
96
+ enable :login, :verify_account, :change_password, :disallow_password_reuse
97
+ if ENV['RODAUTH_SEPARATE_SCHEMA']
98
+ previous_password_hash_table Sequel[:rodauth_test_password][:account_previous_password_hashes]
99
+ end
100
+ account_password_hash_column :ph if ph
101
+ change_password_requires_password? false
102
+ end
103
+ roda do |r|
104
+ r.rodauth
105
+ r.root{view :content=>""}
106
+ end
107
+
108
+ visit '/create-account'
109
+ fill_in 'Login', :with=>'bar@example.com'
110
+ fill_in 'Confirm Login', :with=>'bar@example.com'
111
+ fill_in 'Password', :with=>'0123456789'
112
+ fill_in 'Confirm Password', :with=>'0123456789'
113
+ click_button 'Create Account'
114
+ page.current_path.must_equal '/'
115
+ page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your account"
116
+ link = email_link(/(\/verify-account\?key=.+)$/, 'bar@example.com')
117
+
118
+ visit link
119
+ click_button 'Verify Account'
120
+ page.find('#notice_flash').text.must_equal "Your account has been verified"
121
+ page.current_path.must_equal '/'
122
+
123
+ visit '/change-password'
124
+ fill_in 'New Password', :with=>"012345678"
125
+ fill_in 'Confirm Password', :with=>"012345678"
126
+ click_button 'Change Password'
127
+ page.find('#notice_flash').text.must_equal "Your password has been changed"
128
+
129
+ visit '/change-password'
130
+ fill_in 'New Password', :with=>"0123456789"
131
+ fill_in 'Confirm Password', :with=>"0123456789"
132
+ click_button 'Change Password'
133
+ page.html.must_include("invalid password, does not meet requirements (same as previous password)")
69
134
  end
70
135
 
71
- visit '/create-account'
72
- fill_in 'Login', :with=>'bar@example.com'
73
- fill_in 'Confirm Login', :with=>'bar@example.com'
74
- fill_in 'Password', :with=>'0123456789'
75
- fill_in 'Confirm Password', :with=>'0123456789'
76
- click_button 'Create Account'
77
- page.current_path.must_equal '/'
78
- page.find('#notice_flash').text.must_equal "Your account has been created"
136
+ it "should handle verify account when account_password_hash_column is #{ph} and verify_account_set_password? is true" do
137
+ rodauth do
138
+ enable :login, :verify_account, :change_password, :disallow_password_reuse
139
+ if ENV['RODAUTH_SEPARATE_SCHEMA']
140
+ previous_password_hash_table Sequel[:rodauth_test_password][:account_previous_password_hashes]
141
+ end
142
+ account_password_hash_column :ph if ph
143
+ change_password_requires_password? false
144
+ verify_account_set_password? true
145
+ end
146
+ roda do |r|
147
+ r.rodauth
148
+ r.root{view :content=>""}
149
+ end
79
150
 
80
- visit '/change-password'
81
- fill_in 'New Password', :with=>"012345678"
82
- fill_in 'Confirm Password', :with=>"012345678"
83
- click_button 'Change Password'
84
- page.find('#notice_flash').text.must_equal "Your password has been changed"
151
+ visit '/create-account'
152
+ fill_in 'Login', :with=>'bar@example.com'
153
+ fill_in 'Confirm Login', :with=>'bar@example.com'
154
+ click_button 'Create Account'
155
+ page.current_path.must_equal '/'
156
+ page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your account"
157
+ link = email_link(/(\/verify-account\?key=.+)$/, 'bar@example.com')
85
158
 
86
- visit '/change-password'
87
- fill_in 'New Password', :with=>"0123456789"
88
- fill_in 'Confirm Password', :with=>"0123456789"
89
- click_button 'Change Password'
90
- page.html.must_include("invalid password, does not meet requirements (same as previous password)")
159
+ visit link
160
+ fill_in 'Password', :with=>'0123456789'
161
+ fill_in 'Confirm Password', :with=>'0123456789'
162
+ click_button 'Verify Account'
163
+ page.find('#notice_flash').text.must_equal "Your account has been verified"
164
+ page.current_path.must_equal '/'
165
+
166
+ visit '/change-password'
167
+ fill_in 'New Password', :with=>"012345678"
168
+ fill_in 'Confirm Password', :with=>"012345678"
169
+ click_button 'Change Password'
170
+ page.find('#notice_flash').text.must_equal "Your password has been changed"
171
+
172
+ visit '/change-password'
173
+ fill_in 'New Password', :with=>"0123456789"
174
+ fill_in 'Confirm Password', :with=>"0123456789"
175
+ click_button 'Change Password'
176
+ page.html.must_include("invalid password, does not meet requirements (same as previous password)")
177
+ end
91
178
  end
92
179
  end
@@ -26,7 +26,7 @@ describe 'Rodauth email auth feature' do
26
26
  link = email_link(/(\/email-auth\?key=.+)$/)
27
27
 
28
28
  visit link[0...-1]
29
- page.find('#error_flash').text.must_equal "invalid email authentication key"
29
+ page.find('#error_flash').text.must_equal "There was an error logging you in: invalid email authentication key"
30
30
 
31
31
  visit '/login'
32
32
  fill_in 'Login', :with=>'foo@example.com'
@@ -105,7 +105,7 @@ describe 'Rodauth email auth feature' do
105
105
  link = email_link(/(\/email-auth\?key=.+)$/)
106
106
 
107
107
  visit link[0...-1]
108
- page.find('#error_flash').text.must_equal "invalid email authentication key"
108
+ page.find('#error_flash').text.must_equal "There was an error logging you in: invalid email authentication key"
109
109
 
110
110
  visit '/login'
111
111
  fill_in 'Login', :with=>'foo@example.com'
@@ -0,0 +1,256 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe 'Rodauth login feature' do
4
+ it "should not have jwt refresh feature assume JWT token given during Basic/Digest authentication" do
5
+ rodauth do
6
+ enable :login, :logout, :jwt_refresh
7
+ end
8
+ roda(:jwt) do |r|
9
+ rodauth.require_authentication
10
+ '1'
11
+ end
12
+
13
+ res = json_request("/jwt-refresh", :headers=>{'HTTP_AUTHORIZATION'=>'Basic foo'})
14
+ res.must_equal [401, {'error'=>'Please login to continue'}]
15
+
16
+ res = json_request("/", :headers=>{'HTTP_AUTHORIZATION'=>'Digest foo'})
17
+ res.must_equal [401, {'error'=>'Please login to continue'}]
18
+ end
19
+
20
+ it "should require json request content type in only json mode for rodauth endpoints only" do
21
+ oj = false
22
+ rodauth do
23
+ enable :login, :logout, :jwt_refresh
24
+ jwt_secret '1'
25
+ json_response_success_key 'success'
26
+ only_json?{oj}
27
+ end
28
+ roda(:csrf=>false, :json=>true) do |r|
29
+ r.rodauth
30
+ rodauth.require_authentication
31
+ '1'
32
+ end
33
+
34
+ res = json_request("/", :content_type=>'application/x-www-form-urlencoded', :include_headers=>true, :method=>'GET')
35
+ res[1].delete('Set-Cookie')
36
+ res.must_equal [302, {"Content-Type"=>'text/html', "Content-Length"=>'0', "Location"=>"/login",}, []]
37
+
38
+ res = json_request("/", :content_type=>'application/vnd.api+json', :method=>'GET')
39
+ res.must_equal [400, ['{"error":"Please login to continue"}']]
40
+
41
+ oj = true
42
+
43
+ res = json_request("/", :content_type=>'application/x-www-form-urlencoded', :method=>'GET')
44
+ res.must_equal [400, ['{"error":"Please login to continue"}']]
45
+
46
+ res = json_request("/", :method=>'GET')
47
+ res.must_equal [400, {'error'=>'Please login to continue'}]
48
+
49
+ res = json_request("/login", :content_type=>'application/x-www-form-urlencoded', :include_headers=>true, :method=>'GET')
50
+ msg = "Only JSON format requests are allowed"
51
+ res[1].delete('Set-Cookie')
52
+ res.must_equal [400, {"Content-Type"=>'text/html', "Content-Length"=>msg.length.to_s}, [msg]]
53
+
54
+ jwt_refresh_login
55
+
56
+ res = json_request("/", :content_type=>'application/x-www-form-urlencoded', :include_headers=>true, :method=>'GET')
57
+ # res.must_equal [200, {"Content-Type"=>'text/html', "Content-Length"=>'1'}, ['1']]
58
+ end
59
+
60
+ it "should allow non-json requests if only_json? is false" do
61
+ rodauth do
62
+ enable :login, :logout, :jwt_refresh
63
+ jwt_secret '1'
64
+ only_json? false
65
+ end
66
+ roda(:jwt_html) do |r|
67
+ r.rodauth
68
+ rodauth.require_authentication
69
+ view(:content=>'1')
70
+ end
71
+
72
+ login
73
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
74
+ end
75
+
76
+ it "should require POST for json requests" do
77
+ rodauth do
78
+ enable :login, :logout, :jwt_refresh
79
+ jwt_secret '1'
80
+ json_response_success_key 'success'
81
+ end
82
+ roda(:jwt) do |r|
83
+ r.rodauth
84
+ end
85
+
86
+ res = json_request("/login", :method=>'GET')
87
+ res.must_equal [405, {'error'=>'non-POST method used in JSON API'}]
88
+ end
89
+
90
+ it "should require Accept contain application/json if jwt_check_accept? is true and Accept is present" do
91
+ rodauth do
92
+ enable :login, :logout, :jwt_refresh
93
+ jwt_secret '1'
94
+ json_response_success_key 'success'
95
+ jwt_check_accept? true
96
+ end
97
+ roda(:jwt) do |r|
98
+ r.rodauth
99
+ end
100
+
101
+ res = json_request("/login", :headers=>{'HTTP_ACCEPT'=>'text/html'})
102
+ res.must_equal [406, {'error'=>'Unsupported Accept header. Must accept "application/json" or compatible content type'}]
103
+
104
+ jwt_refresh_validate_login(json_request("/login", :login=>'foo@example.com', :password=>'0123456789'))
105
+ jwt_refresh_validate_login(json_request("/login", :headers=>{'HTTP_ACCEPT'=>'*/*'}, :login=>'foo@example.com', :password=>'0123456789'))
106
+ jwt_refresh_validate_login(json_request("/login", :headers=>{'HTTP_ACCEPT'=>'application/*'}, :login=>'foo@example.com', :password=>'0123456789'))
107
+ jwt_refresh_validate_login(json_request("/login", :headers=>{'HTTP_ACCEPT'=>'application/vnd.api+json'}, :login=>'foo@example.com', :password=>'0123456789'))
108
+ end
109
+
110
+ it "should clear jwt refresh token when closing account" do
111
+ rodauth do
112
+ enable :login, :jwt_refresh, :close_account
113
+ jwt_secret '1'
114
+ end
115
+ roda(:jwt) do |r|
116
+ r.rodauth
117
+ rodauth.require_authentication
118
+ response['Content-Type'] = 'application/json'
119
+ {'hello' => 'world'}.to_json
120
+ end
121
+
122
+ jwt_refresh_login
123
+
124
+ DB[:account_jwt_refresh_keys].count.must_equal 1
125
+ res = json_request('/close-account', :password=>'0123456789')
126
+ res[1].delete('access_token').must_be_kind_of(String)
127
+ res.must_equal [200, {'success'=>"Your account has been closed"}]
128
+ DB[:account_jwt_refresh_keys].count.must_equal 0
129
+ end
130
+
131
+
132
+ it "should set refresh tokens when creating accounts when using autologin" do
133
+ rodauth do
134
+ enable :login, :create_account, :jwt_refresh
135
+ after_create_account{json_response[:account_id] = account_id}
136
+ create_account_autologin? true
137
+ end
138
+ roda(:jwt) do |r|
139
+ r.rodauth
140
+ rodauth.require_authentication
141
+ response['Content-Type'] = 'application/json'
142
+ {'hello' => 'world'}.to_json
143
+ end
144
+
145
+ res = json_request('/create-account', :login=>'foo@example2.com', "login-confirm"=>'foo@example2.com', :password=>'0123456789', "password-confirm"=>'0123456789')
146
+ refresh_token = res.last.delete('refresh_token')
147
+ @authorization = res.last.delete('access_token')
148
+ res.must_equal [200, {'success'=>"Your account has been created", 'account_id'=>DB[:accounts].max(:id)}]
149
+
150
+ res = json_request("/")
151
+ res.must_equal [200, {'hello'=>'world'}]
152
+
153
+ # We can refresh our token
154
+ res = json_request("/jwt-refresh", :refresh_token=>refresh_token)
155
+ jwt_refresh_validate(res)
156
+ @authorization = res.last.delete('access_token')
157
+
158
+ # Which we can use to access protected resources
159
+ res = json_request("/")
160
+ res.must_equal [200, {'hello'=>'world'}]
161
+ end
162
+
163
+ [false, true].each do |hs|
164
+ it "generates and refreshes Refresh Tokens #{'with hmac_secret' if hs}" do
165
+ initial_secret = secret = SecureRandom.random_bytes(32) if hs
166
+ rodauth do
167
+ enable :login, :logout, :jwt_refresh
168
+ hmac_secret{secret} if hs
169
+ jwt_secret '1'
170
+ end
171
+ roda(:jwt) do |r|
172
+ r.rodauth
173
+ rodauth.require_authentication
174
+ response['Content-Type'] = 'application/json'
175
+ {'hello' => 'world'}.to_json
176
+ end
177
+ res = json_request("/")
178
+ res.must_equal [401, {'error'=>'Please login to continue'}]
179
+
180
+ # We can login
181
+ res = jwt_refresh_login
182
+ refresh_token = res.last['refresh_token']
183
+
184
+ # Which gives us an access token which grants us access to protected resources
185
+ @authorization = res.last['access_token']
186
+ res = json_request("/")
187
+ res.must_equal [200, {'hello'=>'world'}]
188
+
189
+ # We can refresh our token
190
+ res = json_request("/jwt-refresh", :refresh_token=>refresh_token)
191
+ jwt_refresh_validate(res)
192
+ second_refresh_token = res.last['refresh_token']
193
+
194
+ # Which we can use to access protected resources
195
+ @authorization = res.last['access_token']
196
+ res = json_request("/")
197
+ res.must_equal [200, {'hello'=>'world'}]
198
+
199
+ # Subsequent refresh token is valid
200
+ res = json_request("/jwt-refresh", :refresh_token=>second_refresh_token)
201
+ jwt_refresh_validate(res)
202
+ third_refresh_token = res.last['refresh_token']
203
+
204
+ # First refresh token is now no longer valid
205
+ res = json_request("/jwt-refresh", :refresh_token=>refresh_token)
206
+ res.must_equal [400, {"error"=>"invalid JWT refresh token"}]
207
+
208
+ # Third refresh token is valid
209
+ res = json_request("/jwt-refresh", :refresh_token=>third_refresh_token)
210
+ jwt_refresh_validate(res)
211
+ fourth_refresh_token = res.last['refresh_token']
212
+
213
+ # And still gives us a valid access token
214
+ @authorization = res.last['access_token']
215
+ res = json_request("/")
216
+ res.must_equal [200, {'hello'=>'world'}]
217
+
218
+ if hs
219
+ # Refresh secret doesn't work if hmac_secret changed
220
+ secret = SecureRandom.random_bytes(32)
221
+ res = json_request("/jwt-refresh", :refresh_token=>fourth_refresh_token)
222
+ res.first.must_equal 400
223
+ res.must_equal [400, {'error'=>'invalid JWT refresh token'}]
224
+
225
+ # Refresh secret works if hmac_secret changed back
226
+ secret = initial_secret
227
+ res = json_request("/jwt-refresh", :refresh_token=>fourth_refresh_token)
228
+ jwt_refresh_validate(res)
229
+
230
+ # And still gives us a valid access token
231
+ @authorization = res.last['access_token']
232
+ res = json_request("/")
233
+ res.must_equal [200, {'hello'=>'world'}]
234
+ end
235
+ end
236
+ end
237
+
238
+ it "should not return access_token for failed login attempt" do
239
+ rodauth do
240
+ enable :login, :create_account, :jwt_refresh
241
+ after_create_account{json_response[:account_id] = account_id}
242
+ create_account_autologin? true
243
+ end
244
+ roda(:jwt) do |r|
245
+ r.rodauth
246
+ rodauth.require_authentication
247
+ response['Content-Type'] = 'application/json'
248
+ {'hello' => 'world'}.to_json
249
+ end
250
+
251
+ json_request('/create-account', :login=>'foo@example2.com', "login-confirm"=>'foo@example2.com', :password=>'0123456789', "password-confirm"=>'0123456789')
252
+
253
+ res = json_request('/login', :login=>'foo@example2.com', :password=>'123123')
254
+ res.must_equal [401, {"field-error"=>['password', 'invalid password'], "error"=>"There was an error logging in"}]
255
+ end
256
+ end