rodauth 1.19.1 → 1.20.0

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