rodauth 2.46.0 → 2.47.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rodauth/features/active_sessions.rb +2 -2
  3. data/lib/rodauth/features/audit_logging.rb +2 -1
  4. data/lib/rodauth/features/base.rb +65 -10
  5. data/lib/rodauth/features/change_password.rb +11 -2
  6. data/lib/rodauth/features/confirm_password.rb +1 -1
  7. data/lib/rodauth/features/create_account.rb +2 -1
  8. data/lib/rodauth/features/email_auth.rb +13 -4
  9. data/lib/rodauth/features/email_base.rb +36 -1
  10. data/lib/rodauth/features/internal_request.rb +4 -0
  11. data/lib/rodauth/features/json.rb +5 -1
  12. data/lib/rodauth/features/jwt.rb +7 -1
  13. data/lib/rodauth/features/jwt_cors.rb +2 -0
  14. data/lib/rodauth/features/jwt_refresh.rb +33 -18
  15. data/lib/rodauth/features/lockout.rb +22 -19
  16. data/lib/rodauth/features/login.rb +12 -4
  17. data/lib/rodauth/features/login_password_requirements_base.rb +39 -9
  18. data/lib/rodauth/features/otp.rb +19 -10
  19. data/lib/rodauth/features/password_expiration.rb +0 -1
  20. data/lib/rodauth/features/password_grace_period.rb +7 -0
  21. data/lib/rodauth/features/password_pepper.rb +11 -1
  22. data/lib/rodauth/features/recovery_codes.rb +4 -1
  23. data/lib/rodauth/features/remember.rb +2 -0
  24. data/lib/rodauth/features/require_domain.rb +12 -0
  25. data/lib/rodauth/features/require_hmac_secret.rb +12 -0
  26. data/lib/rodauth/features/reset_password.rb +25 -20
  27. data/lib/rodauth/features/single_session.rb +6 -2
  28. data/lib/rodauth/features/sms_codes.rb +4 -1
  29. data/lib/rodauth/features/two_factor_base.rb +2 -1
  30. data/lib/rodauth/features/update_password_hash.rb +5 -1
  31. data/lib/rodauth/features/verify_account.rb +29 -20
  32. data/lib/rodauth/features/verify_login_change.rb +19 -11
  33. data/lib/rodauth/features/webauthn.rb +10 -5
  34. data/lib/rodauth/features/webauthn_login.rb +1 -10
  35. data/lib/rodauth/features/webauthn_verify_account.rb +0 -4
  36. data/lib/rodauth/version.rb +1 -1
  37. data/lib/rodauth.rb +2 -0
  38. metadata +3 -1
@@ -15,7 +15,7 @@ module Rodauth
15
15
  auth_value_method :login_error_status, 401
16
16
  translatable_method :login_form_footer_links_heading, '<h2 class="rodauth-login-form-footer-links-heading">Other Options</h2>'
17
17
  auth_value_method :login_return_to_requested_location?, false
18
- auth_value_method :login_return_to_requested_location_max_path_size, 2048
18
+ def_deprecated_alias :return_to_path_max_size, :login_return_to_requested_location_max_path_size
19
19
  auth_value_method :use_multi_phase_login?, false
20
20
 
21
21
  session_key :login_redirect_session_key, :login_redirect
@@ -106,14 +106,14 @@ module Rodauth
106
106
  end
107
107
 
108
108
  def login_required
109
- if login_return_to_requested_location? && (path = login_return_to_requested_location_path) && path.bytesize <= login_return_to_requested_location_max_path_size
110
- set_session_value(login_redirect_session_key, path)
109
+ if login_return_to_requested_location?
110
+ set_session_return_to_path(login_redirect_session_key, login_return_to_requested_location_path)
111
111
  end
112
112
  super
113
113
  end
114
114
 
115
115
  def login_return_to_requested_location_path
116
- request.fullpath if request.get?
116
+ request.fullpath
117
117
  end
118
118
 
119
119
  def after_login_entered_during_multi_phase_login
@@ -153,6 +153,14 @@ module Rodauth
153
153
  login_path
154
154
  end
155
155
 
156
+ def post_configure
157
+ # RODAUTH3: Remove
158
+ super
159
+ if method(:already_logged_in).owner == Rodauth::Base
160
+ missing_recommended_configuration("The Rodauth 'already_logged_in' configuration method was not used. Use the configuration method to define the behavior when accessing a page while logged in that is expected to be accessed when not logged in (safest to halt or redirect).")
161
+ end
162
+ end
163
+
156
164
  private
157
165
 
158
166
  def _login_response
@@ -12,7 +12,7 @@ module Rodauth
12
12
  translatable_method :logins_do_not_match_message, 'logins do not match'
13
13
  auth_value_method :password_confirm_param, 'password-confirm'
14
14
  auth_value_method :password_minimum_length, 6
15
- auth_value_method :password_maximum_bytes, nil
15
+ auth_value_method :password_maximum_bytes, nil # RODAUTH3: 72 if bcrypt is used
16
16
  auth_value_method :password_maximum_length, nil
17
17
  translatable_method :passwords_do_not_match_message, 'passwords do not match'
18
18
  auth_value_method :require_email_address_logins?, true
@@ -67,13 +67,39 @@ module Rodauth
67
67
  def set_password(password)
68
68
  hash = password_hash(password)
69
69
  if account_password_hash_column
70
- update_account(account_password_hash_column=>hash)
71
- elsif password_hash_ds.update(password_hash_column=>hash) == 0
72
- # This shouldn't raise a uniqueness error, as the update should only fail for a new user,
73
- # and an existing user should always have a valid password hash row. If this does
74
- # fail, retrying it will cause problems, it will override a concurrently running update
75
- # with potentially a different password.
76
- db[password_hash_table].insert(password_hash_id_column=>account_id, password_hash_column=>hash)
70
+ ds = account_ds
71
+
72
+ if @existing_password_hash_or_salt
73
+ ds = ds.where(account_password_hash_column => @existing_password_hash_or_salt)
74
+ end
75
+
76
+ unless update_account({account_password_hash_column=>hash}, ds) == 1
77
+ raise SetPasswordFailure, "set_password did not update password, either due to missing account, or concurrent password change"
78
+ end
79
+ else
80
+ ph_ds = ds = password_hash_ds
81
+ if @existing_password_hash_or_salt
82
+ existing_expression = if use_database_authentication_functions?
83
+ Sequel.function(function_name(:rodauth_get_salt), :id)
84
+ # :nocov:
85
+ else
86
+ password_hash_column
87
+ # :nocov:
88
+ end
89
+ ds = ds.where(existing_expression => @existing_password_hash_or_salt)
90
+ end
91
+
92
+ if ds.update(password_hash_column=>hash) == 0
93
+ if ph_ds.empty?
94
+ # This shouldn't raise a uniqueness error, as the update should only fail for a new user,
95
+ # and an existing user should always have a valid password hash row. If this does
96
+ # fail, retrying it will cause problems, it will override a concurrently running update
97
+ # with potentially a different password.
98
+ db[password_hash_table].insert(password_hash_id_column=>account_id, password_hash_column=>hash)
99
+ else
100
+ raise SetPasswordFailure, "set_password did not update password due to concurrent password change"
101
+ end
102
+ end
77
103
  end
78
104
  hash
79
105
  end
@@ -174,7 +200,7 @@ module Rodauth
174
200
  elsif password_maximum_length && password_maximum_length < password.length
175
201
  set_password_requirement_error_message(:password_too_long, password_too_long_message)
176
202
  false
177
- elsif password_maximum_bytes && password_maximum_bytes < password.bytesize
203
+ elsif password_maximum_bytes && password_maximum_bytes < password_bytesize(password)
178
204
  set_password_requirement_error_message(:password_too_many_bytes, password_too_many_bytes_message)
179
205
  false
180
206
  else
@@ -182,6 +208,10 @@ module Rodauth
182
208
  end
183
209
  end
184
210
 
211
+ def password_bytesize(password)
212
+ password.bytesize
213
+ end
214
+
185
215
  def password_does_not_contain_null_byte?(password)
186
216
  return true unless password.include?("\0")
187
217
  set_password_requirement_error_message(:password_contains_null_byte, contains_null_byte_message)
@@ -5,7 +5,7 @@ require 'rqrcode'
5
5
 
6
6
  module Rodauth
7
7
  Feature.define(:otp, :Otp) do
8
- depends :two_factor_base
8
+ depends :two_factor_base, :require_domain, :require_hmac_secret
9
9
 
10
10
  additional_form_tags 'otp_disable'
11
11
  additional_form_tags 'otp_auth'
@@ -127,13 +127,18 @@ module Rodauth
127
127
  end
128
128
 
129
129
  r.post do
130
- if otp_valid_code?(param(otp_auth_param)) && otp_update_last_use
131
- before_otp_authentication
132
- two_factor_authenticate('totp')
130
+ transaction do
131
+ otp_key_ds.for_update.first
132
+
133
+ if otp_valid_code?(param(otp_auth_param)) && otp_update_last_use
134
+ before_otp_authentication
135
+ two_factor_authenticate('totp')
136
+ end
137
+
138
+ otp_record_authentication_failure
139
+ after_otp_authentication_failure
133
140
  end
134
141
 
135
- otp_record_authentication_failure
136
- after_otp_authentication_failure
137
142
  set_response_error_reason_status(:invalid_otp_auth_code, invalid_key_error_status)
138
143
  set_field_error(otp_auth_param, otp_invalid_auth_code_message)
139
144
  set_error_flash otp_auth_error_flash
@@ -265,15 +270,19 @@ module Rodauth
265
270
  return false unless otp_exists?
266
271
  ot_pass = ot_pass.gsub(/\s+/, '')
267
272
  if drift = otp_drift
273
+ # :nocov:
268
274
  if otp.respond_to?(:verify_with_drift)
269
- # :nocov:
270
275
  otp.verify_with_drift(ot_pass, drift)
271
- # :nocov:
276
+ # :nocov:
272
277
  else
273
- otp.verify(ot_pass, :drift_behind=>drift, :drift_ahead=>drift)
278
+ otp.verify(ot_pass, :drift_behind=>drift, :drift_ahead=>drift, :after=>otp_last_use)
274
279
  end
275
- else
280
+ # :nocov:
281
+ elsif otp.respond_to?(:verify_with_drift)
276
282
  otp.verify(ot_pass)
283
+ # :nocov:
284
+ else
285
+ otp.verify(ot_pass, :after=>otp_last_use)
277
286
  end
278
287
  end
279
288
 
@@ -70,7 +70,6 @@ module Rodauth
70
70
  return password_changed_at + require_password_change_after < Time.now.to_i
71
71
  end
72
72
 
73
- account_from_session
74
73
  if password_changed_at = get_password_changed_at
75
74
  set_session_value(password_changed_at_session_key, password_changed_at.to_i)
76
75
  password_changed_at + require_password_change_after < Time.now
@@ -33,6 +33,13 @@ module Rodauth
33
33
 
34
34
  private
35
35
 
36
+ def new_password_matches_current_password?(_)
37
+ if v = super
38
+ session.delete(last_password_entry_session_key)
39
+ end
40
+ v
41
+ end
42
+
36
43
  def after_create_account
37
44
  super if defined?(super)
38
45
  @last_password_entry = Time.now.to_i
@@ -12,7 +12,11 @@ module Rodauth
12
12
 
13
13
  def password_match?(password)
14
14
  if (result = super) && @previous_pepper_matched && password_pepper_update?
15
- set_password(password)
15
+ begin
16
+ set_password(password)
17
+ rescue Rodauth::SetPasswordFailure
18
+ return false
19
+ end
16
20
  end
17
21
 
18
22
  result
@@ -24,6 +28,12 @@ module Rodauth
24
28
 
25
29
  private
26
30
 
31
+ def password_bytesize(password)
32
+ n = super
33
+ n += password_pepper.bytesize if password_pepper
34
+ n
35
+ end
36
+
27
37
  def password_hash_match?(hash, password)
28
38
  return super if password_pepper.nil?
29
39
 
@@ -162,7 +162,10 @@ module Rodauth
162
162
  def recovery_code_match?(code)
163
163
  recovery_codes.each do |s|
164
164
  if timing_safe_eql?(code, s)
165
- recovery_codes_ds.where(recovery_codes_column=>code).delete
165
+ unless recovery_codes_ds.where(recovery_codes_column=>code).delete == 1
166
+ return false
167
+ end
168
+
166
169
  if recovery_codes_primary?
167
170
  add_recovery_code
168
171
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:remember, :Remember) do
5
+ depends :require_hmac_secret
6
+
5
7
  notice_flash "Your remember setting has been updated"
6
8
  error_flash "There was an error updating your remember setting"
7
9
  loaded_templates %w'remember'
@@ -0,0 +1,12 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:require_domain, :RequireDomain) do
5
+ def post_configure
6
+ super
7
+ if method(:domain).owner == Rodauth::Base
8
+ missing_recommended_configuration("The Rodauth 'domain' configuration method was not used. This can potentially leave the application vulnerable to requests that use malicious or invalid domains.")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:require_hmac_secret, :RequireHmacSecret) do
5
+ def post_configure
6
+ super
7
+ if method(:hmac_secret).owner == Rodauth::Base
8
+ missing_recommended_configuration("The Rodauth 'hmac_secret' configuration method was not used. This significantly reduces Rodauth's security. Consult the Rodauth README for what this method does, and set an appropriate value.")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -120,29 +120,26 @@ module Rodauth
120
120
  end
121
121
 
122
122
  r.post do
123
- key = session[reset_password_session_key] || param(reset_password_key_param)
124
- unless account_from_reset_password_key(key)
125
- set_redirect_error_status(invalid_key_error_status)
126
- set_error_reason :invalid_reset_password_key
127
- set_redirect_error_flash reset_password_error_flash
128
- redirect reset_password_email_sent_redirect
129
- end
130
-
123
+ key = session_param(reset_password_session_key, reset_password_key_param)
124
+ select_key_for_update!
131
125
  password = param(password_param)
126
+
132
127
  catch_error do
133
- unless password_meets_requirements?(password)
134
- throw_error_status(invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
135
- end
128
+ transaction do
129
+ handle_invalid_reset_password_key unless key && account_from_reset_password_key(key)
136
130
 
137
- if password_match?(password)
138
- throw_error_reason(:same_as_existing_password, invalid_field_error_status, password_param, same_as_existing_password_message)
139
- end
131
+ unless password_meets_requirements?(password)
132
+ throw_error_status(invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
133
+ end
140
134
 
141
- if require_password_confirmation? && password != param(password_confirm_param)
142
- throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, password_param, passwords_do_not_match_message)
143
- end
135
+ if password_match?(password)
136
+ throw_error_reason(:same_as_existing_password, invalid_field_error_status, password_param, same_as_existing_password_message)
137
+ end
138
+
139
+ if require_password_confirmation? && password != param(password_confirm_param)
140
+ throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, password_param, passwords_do_not_match_message)
141
+ end
144
142
 
145
- transaction do
146
143
  before_reset_password
147
144
  set_password(password)
148
145
  clear_tokens(:reset_password)
@@ -184,7 +181,7 @@ module Rodauth
184
181
  end
185
182
 
186
183
  def account_from_reset_password_key(key)
187
- @account = _account_from_reset_password_key(key)
184
+ set_account(_account_from_reset_password_key(key), :reset_password)
188
185
  end
189
186
 
190
187
  def reset_password_email_link
@@ -199,7 +196,7 @@ module Rodauth
199
196
  def get_reset_password_key(id)
200
197
  ds = password_reset_ds(id)
201
198
  ds.where(Sequel::CURRENT_TIMESTAMP > reset_password_deadline_column).delete
202
- ds.get(reset_password_key_column)
199
+ apply_key_for_update(ds).get(reset_password_key_column)
203
200
  end
204
201
 
205
202
  def set_reset_password_email_last_sent
@@ -267,5 +264,13 @@ module Rodauth
267
264
  def reset_password_account_status_value
268
265
  account_open_status_value
269
266
  end
267
+
268
+ def handle_invalid_reset_password_key
269
+ remove_session_value(reset_password_session_key)
270
+ set_redirect_error_status(invalid_key_error_status)
271
+ set_error_reason :invalid_reset_password_key
272
+ set_redirect_error_flash reset_password_error_flash
273
+ redirect reset_password_email_sent_redirect
274
+ end
270
275
  end
271
276
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:single_session, :SingleSession) do
5
+ depends :require_hmac_secret
6
+
5
7
  error_flash 'This session has been logged out as another session has become active'
6
8
  redirect
7
9
 
@@ -29,13 +31,15 @@ module Rodauth
29
31
  single_session_key = session[single_session_session_key]
30
32
  current_key = single_session_ds.get(single_session_key_column)
31
33
  if single_session_key.nil?
32
- unless current_key
34
+ if current_key
35
+ false
36
+ else
33
37
  # No row exists for this user, indicating the feature has never
34
38
  # been used, so it is OK to treat the current session as a new
35
39
  # session.
36
40
  update_single_session_key
41
+ true
37
42
  end
38
- true
39
43
  elsif current_key
40
44
  if hmac_secret && !(valid = timing_safe_eql?(single_session_key, hmac = compute_hmac(current_key)))
41
45
  if hmac_secret_rotation? && (valid = timing_safe_eql?(single_session_key, compute_old_hmac(current_key)))
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:sms_codes, :SmsCodes) do
5
- depends :two_factor_base
5
+ depends :two_factor_base, :require_domain
6
6
 
7
7
  additional_form_tags 'sms_auth'
8
8
  additional_form_tags 'sms_confirm'
@@ -176,6 +176,9 @@ module Rodauth
176
176
 
177
177
  r.post do
178
178
  transaction do
179
+ @sms = sms_ds.for_update.first
180
+ require_sms_available
181
+
179
182
  if sms_code_match?(param(sms_code_param))
180
183
  before_sms_auth
181
184
  sms_remove_failures
@@ -158,7 +158,7 @@ module Rodauth
158
158
  def require_two_factor_authenticated
159
159
  unless two_factor_authenticated?
160
160
  if two_factor_auth_return_to_requested_location?
161
- set_session_value(two_factor_auth_redirect_session_key, request.fullpath)
161
+ set_session_return_to_path(two_factor_auth_redirect_session_key)
162
162
  end
163
163
  set_redirect_error_status(two_factor_need_authentication_error_status)
164
164
  set_error_reason :two_factor_need_authentication
@@ -262,6 +262,7 @@ module Rodauth
262
262
  end
263
263
 
264
264
  def two_factor_update_session(auth_type)
265
+ raise Error, "attempt to have two factor authentication using same factor" if authenticated_by.include?(auth_type)
265
266
  authenticated_by << auth_type
266
267
  set_session_value(two_factor_setup_session_key, true)
267
268
  end
@@ -9,7 +9,11 @@ module Rodauth
9
9
  def password_match?(password)
10
10
  if (result = super) && update_password_hash?
11
11
  @update_password_hash = false
12
- set_password(password)
12
+ begin
13
+ set_password(password)
14
+ rescue Rodauth::SetPasswordFailure
15
+ return false
16
+ end
13
17
  end
14
18
 
15
19
  result
@@ -114,30 +114,30 @@ module Rodauth
114
114
  end
115
115
 
116
116
  r.post do
117
- key = session[verify_account_session_key] || param(verify_account_key_param)
118
- unless account_from_verify_account_key(key)
119
- set_redirect_error_status(invalid_key_error_status)
120
- set_error_reason :invalid_verify_account_key
121
- set_redirect_error_flash verify_account_error_flash
122
- redirect verify_account_redirect
123
- end
117
+ key = session_param(verify_account_session_key, verify_account_key_param)
118
+ select_key_for_update!
124
119
 
125
120
  catch_error do
126
- if verify_account_set_password?
127
- password = param(password_param)
121
+ transaction do
122
+ handle_invalid_verify_account_key unless key && account_from_verify_account_key(key)
128
123
 
129
- if require_password_confirmation? && password != param(password_confirm_param)
130
- throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, password_param, passwords_do_not_match_message)
131
- end
124
+ if verify_account_set_password?
125
+ password = param(password_param)
126
+
127
+ if require_password_confirmation? && password != param(password_confirm_param)
128
+ throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, password_param, passwords_do_not_match_message)
129
+ end
132
130
 
133
- unless password_meets_requirements?(password)
134
- throw_error_status(invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
131
+ unless password_meets_requirements?(password)
132
+ throw_error_status(invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
133
+ end
135
134
  end
136
- end
137
135
 
138
- transaction do
139
136
  before_verify_account
140
- verify_account
137
+ unless verify_account
138
+ remove_verify_account_key
139
+ handle_invalid_verify_account_key
140
+ end
141
141
  if verify_account_set_password?
142
142
  set_password(password)
143
143
  end
@@ -171,7 +171,8 @@ module Rodauth
171
171
  end
172
172
 
173
173
  def verify_account
174
- update_account(account_status_column=>account_open_status_value) == 1
174
+ ds = account_ds.where(account_status_column=>account_unverified_status_value)
175
+ update_account({account_status_column=>account_open_status_value}, ds) == 1
175
176
  end
176
177
 
177
178
  def verify_account_email_resend
@@ -196,7 +197,7 @@ module Rodauth
196
197
  end
197
198
 
198
199
  def account_from_verify_account_key(key)
199
- @account = _account_from_verify_account_key(key)
200
+ set_account(_account_from_verify_account_key(key), :verify_account)
200
201
  end
201
202
 
202
203
  def account_initial_status_value
@@ -208,7 +209,7 @@ module Rodauth
208
209
  end
209
210
 
210
211
  def get_verify_account_key(id)
211
- verify_account_ds(id).get(verify_account_key_column)
212
+ apply_key_for_update(verify_account_ds(id)).get(verify_account_key_column)
212
213
  end
213
214
 
214
215
  def skip_status_checks?
@@ -318,5 +319,13 @@ module Rodauth
318
319
  def _account_from_verify_account_key(token)
319
320
  account_from_key(token, account_unverified_status_value){|id| get_verify_account_key(id)}
320
321
  end
322
+
323
+ def handle_invalid_verify_account_key
324
+ remove_session_value(verify_account_session_key)
325
+ set_redirect_error_status(invalid_key_error_status)
326
+ set_error_reason :invalid_verify_account_key
327
+ set_redirect_error_flash verify_account_error_flash
328
+ redirect verify_account_redirect
329
+ end
321
330
  end
322
331
  end
@@ -74,15 +74,18 @@ module Rodauth
74
74
  end
75
75
 
76
76
  r.post do
77
- key = session[verify_login_change_session_key] || param(verify_login_change_key_param)
78
- unless account_from_verify_login_change_key(key)
79
- set_redirect_error_status(invalid_key_error_status)
80
- set_error_reason :invalid_verify_login_change_key
81
- set_redirect_error_flash verify_login_change_error_flash
82
- redirect verify_login_change_redirect
83
- end
77
+ key = session_param(verify_login_change_session_key, verify_login_change_key_param)
78
+ remove_session_value(verify_login_change_session_key)
79
+ select_key_for_update!
84
80
 
85
81
  transaction do
82
+ unless key && account_from_verify_login_change_key(key)
83
+ set_redirect_error_status(invalid_key_error_status)
84
+ set_error_reason :invalid_verify_login_change_key
85
+ set_redirect_error_flash verify_login_change_error_flash
86
+ redirect verify_login_change_redirect
87
+ end
88
+
86
89
  before_verify_login_change
87
90
  unless verify_login_change
88
91
  set_redirect_error_status(invalid_key_error_status)
@@ -98,7 +101,6 @@ module Rodauth
98
101
  autologin_session('verify_login_change')
99
102
  end
100
103
 
101
- remove_session_value(verify_login_change_session_key)
102
104
  verify_login_change_response
103
105
  end
104
106
  end
@@ -120,7 +122,7 @@ module Rodauth
120
122
  end
121
123
 
122
124
  def account_from_verify_login_change_key(key)
123
- @account = _account_from_verify_login_change_key(key)
125
+ set_account(_account_from_verify_login_change_key(key), :verify_login_change)
124
126
  end
125
127
 
126
128
  def send_verify_login_change_email(login)
@@ -132,7 +134,9 @@ module Rodauth
132
134
  end
133
135
 
134
136
  def get_verify_login_change_login_and_key(id)
135
- verify_login_change_ds(id).get([verify_login_change_login_column, verify_login_change_key_column])
137
+ ds = verify_login_change_ds(id)
138
+ ds.where(Sequel::CURRENT_TIMESTAMP > verify_login_change_deadline_column).delete
139
+ apply_key_for_update(ds).get([verify_login_change_login_column, verify_login_change_key_column])
136
140
  end
137
141
 
138
142
  def change_login_notice_flash
@@ -210,10 +214,14 @@ module Rodauth
210
214
  end
211
215
 
212
216
  def _account_from_verify_login_change_key(token)
213
- account_from_key(token) do |id|
217
+ account_from_key(token, verify_login_change_account_status_value) do |id|
214
218
  @verify_login_change_new_login, key = get_verify_login_change_login_and_key(id)
215
219
  key
216
220
  end
217
221
  end
222
+
223
+ def verify_login_change_account_status_value
224
+ account_open_status_value
225
+ end
218
226
  end
219
227
  end
@@ -4,7 +4,7 @@ require 'webauthn'
4
4
 
5
5
  module Rodauth
6
6
  Feature.define(:webauthn, :Webauthn) do
7
- depends :two_factor_base
7
+ depends :two_factor_base, :require_domain, :require_hmac_secret
8
8
 
9
9
  loaded_templates %w'webauthn-setup webauthn-auth webauthn-remove'
10
10
 
@@ -164,7 +164,7 @@ module Rodauth
164
164
  end
165
165
 
166
166
  route(:webauthn_setup) do |r|
167
- require_authentication unless two_factor_login_type_match?('webauthn')
167
+ require_authentication unless webauthn_modification_authenticated?
168
168
  require_account_session
169
169
  before_webauthn_setup_route
170
170
 
@@ -185,7 +185,7 @@ module Rodauth
185
185
  raise Sequel::Rollback
186
186
  end
187
187
 
188
- unless two_factor_authenticated?
188
+ if !two_factor_authenticated? && !authenticated_by.include?('webauthn')
189
189
  webauthn_update_session(webauthn_credential.id)
190
190
  two_factor_update_session('webauthn')
191
191
  end
@@ -205,7 +205,7 @@ module Rodauth
205
205
  end
206
206
 
207
207
  route(:webauthn_remove) do |r|
208
- require_authentication unless two_factor_login_type_match?('webauthn')
208
+ require_authentication unless webauthn_modification_authenticated?
209
209
  require_account_session
210
210
  require_webauthn_setup
211
211
  before_webauthn_remove_route
@@ -469,6 +469,11 @@ module Rodauth
469
469
  # :nocov:
470
470
  end
471
471
 
472
+ def webauthn_modification_authenticated?
473
+ two_factor_login_type_match?('webauthn') &&
474
+ (!uses_two_factor_authentication? || two_factor_modifications_require_password?)
475
+ end
476
+
472
477
  def _two_factor_auth_links
473
478
  links = super
474
479
  links << [10, webauthn_auth_path, webauthn_auth_link_text] if webauthn_setup? && !two_factor_login_type_match?('webauthn')
@@ -501,7 +506,7 @@ module Rodauth
501
506
  end
502
507
 
503
508
  def webauthn_account_id
504
- session_value
509
+ account_id_or_session_value
505
510
  end
506
511
 
507
512
  def webauthn_user_ids_ds
@@ -76,7 +76,7 @@ module Rodauth
76
76
  end
77
77
 
78
78
  def account_from_webauthn_login
79
- account_from_login(login_param_value)
79
+ @account_retrieval_type == :login ? @account : account_from_login(login_param_value)
80
80
  end
81
81
 
82
82
  def webauthn_login_options?
@@ -91,14 +91,5 @@ module Rodauth
91
91
  end
92
92
  forms
93
93
  end
94
-
95
- def webauthn_account_id
96
- case @current_route
97
- when :login, :webauthn_login, nil # nil for internal request
98
- account_id
99
- else
100
- super
101
- end
102
- end
103
94
  end
104
95
  end