rodauth 2.45.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 (40) 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 +12 -2
  6. data/lib/rodauth/features/close_account_email.rb +17 -0
  7. data/lib/rodauth/features/confirm_password.rb +1 -1
  8. data/lib/rodauth/features/create_account.rb +2 -1
  9. data/lib/rodauth/features/email_auth.rb +13 -4
  10. data/lib/rodauth/features/email_base.rb +37 -3
  11. data/lib/rodauth/features/internal_request.rb +4 -0
  12. data/lib/rodauth/features/json.rb +5 -1
  13. data/lib/rodauth/features/jwt.rb +7 -1
  14. data/lib/rodauth/features/jwt_cors.rb +2 -0
  15. data/lib/rodauth/features/jwt_refresh.rb +35 -22
  16. data/lib/rodauth/features/lockout.rb +22 -19
  17. data/lib/rodauth/features/login.rb +12 -4
  18. data/lib/rodauth/features/login_password_requirements_base.rb +39 -9
  19. data/lib/rodauth/features/otp.rb +19 -10
  20. data/lib/rodauth/features/password_expiration.rb +0 -1
  21. data/lib/rodauth/features/password_grace_period.rb +7 -0
  22. data/lib/rodauth/features/password_pepper.rb +11 -1
  23. data/lib/rodauth/features/recovery_codes.rb +4 -1
  24. data/lib/rodauth/features/remember.rb +2 -0
  25. data/lib/rodauth/features/require_domain.rb +12 -0
  26. data/lib/rodauth/features/require_hmac_secret.rb +12 -0
  27. data/lib/rodauth/features/reset_password.rb +25 -20
  28. data/lib/rodauth/features/single_session.rb +6 -2
  29. data/lib/rodauth/features/sms_codes.rb +4 -1
  30. data/lib/rodauth/features/two_factor_base.rb +2 -1
  31. data/lib/rodauth/features/update_password_hash.rb +5 -1
  32. data/lib/rodauth/features/verify_account.rb +40 -22
  33. data/lib/rodauth/features/verify_login_change.rb +19 -11
  34. data/lib/rodauth/features/webauthn.rb +10 -5
  35. data/lib/rodauth/features/webauthn_login.rb +1 -5
  36. data/lib/rodauth/features/webauthn_verify_account.rb +0 -4
  37. data/lib/rodauth/version.rb +1 -1
  38. data/lib/rodauth.rb +2 -0
  39. data/templates/close-account-email.str +2 -0
  40. metadata +6 -2
@@ -116,16 +116,19 @@ module Rodauth
116
116
  end
117
117
 
118
118
  r.post do
119
- key = session[unlock_account_session_key] || param(unlock_account_key_param)
120
- unless account_from_unlock_key(key)
121
- set_redirect_error_status invalid_key_error_status
122
- set_error_reason :invalid_unlock_account_key
123
- set_redirect_error_flash no_matching_unlock_account_key_error_flash
124
- redirect unlock_account_request_redirect
125
- end
119
+ key = session_param(unlock_account_session_key, unlock_account_key_param)
120
+ select_key_for_update!
121
+
122
+ transaction do
123
+ unless key && account_from_unlock_key(key)
124
+ remove_session_value(unlock_account_session_key)
125
+ set_redirect_error_status invalid_key_error_status
126
+ set_error_reason :invalid_unlock_account_key
127
+ set_redirect_error_flash no_matching_unlock_account_key_error_flash
128
+ redirect unlock_account_request_redirect
129
+ end
126
130
 
127
- if !unlock_account_requires_password? || password_match?(param(password_param))
128
- transaction do
131
+ if !unlock_account_requires_password? || password_match?(param(password_param))
129
132
  before_unlock_account
130
133
  unlock_account
131
134
  clear_tokens(:unlock_account)
@@ -133,15 +136,15 @@ module Rodauth
133
136
  if unlock_account_autologin?
134
137
  autologin_session('unlock_account')
135
138
  end
136
- end
137
139
 
138
- remove_session_value(unlock_account_session_key)
139
- unlock_account_response
140
- else
141
- set_response_error_reason_status(:invalid_password, invalid_password_error_status)
142
- set_field_error(password_param, invalid_password_message)
143
- set_error_flash unlock_account_error_flash
144
- unlock_account_view
140
+ remove_session_value(unlock_account_session_key)
141
+ unlock_account_response
142
+ else
143
+ set_response_error_reason_status(:invalid_password, invalid_password_error_status)
144
+ set_field_error(password_param, invalid_password_message)
145
+ set_error_flash unlock_account_error_flash
146
+ unlock_account_view
147
+ end
145
148
  end
146
149
  end
147
150
  end
@@ -221,7 +224,7 @@ module Rodauth
221
224
  end
222
225
 
223
226
  def account_from_unlock_key(key)
224
- @account = _account_from_unlock_key(key)
227
+ set_account(_account_from_unlock_key(key), :unlock)
225
228
  end
226
229
 
227
230
  def unlock_account_email_link
@@ -310,7 +313,7 @@ module Rodauth
310
313
  end
311
314
 
312
315
  def _account_from_unlock_key(token)
313
- account_from_key(token){|id| account_lockouts_ds(id).get(account_lockouts_key_column)}
316
+ account_from_key(token){|id| apply_key_for_update(account_lockouts_ds(id)).get(account_lockouts_key_column)}
314
317
  end
315
318
  end
316
319
  end
@@ -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?
@@ -225,7 +226,7 @@ module Rodauth
225
226
  end
226
227
 
227
228
  def set_verify_account_email_last_sent
228
- verify_account_ds.update(verify_account_email_last_sent_column=>Sequel::CURRENT_TIMESTAMP) if verify_account_email_last_sent_column
229
+ verify_account_ds.update(verify_account_email_last_sent_column=>Sequel::CURRENT_TIMESTAMP) if verify_account_email_last_sent_column
229
230
  end
230
231
 
231
232
  def get_verify_account_email_last_sent
@@ -248,7 +249,16 @@ module Rodauth
248
249
 
249
250
  def clear_tokens(reason)
250
251
  super
251
- remove_verify_account_key
252
+ if reason == :close_account
253
+ remove_verify_account_key
254
+ else
255
+ generate_verify_account_key_value
256
+ hash = {verify_account_key_column=>verify_account_key_value}
257
+ if verify_account_email_last_sent_column
258
+ hash[verify_account_email_last_sent_column] = Time.now - verify_account_skip_resend_email_within - 10
259
+ end
260
+ verify_account_ds.update(hash)
261
+ end
252
262
  end
253
263
 
254
264
  private
@@ -309,5 +319,13 @@ module Rodauth
309
319
  def _account_from_verify_account_key(token)
310
320
  account_from_key(token, account_unverified_status_value){|id| get_verify_account_key(id)}
311
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
312
330
  end
313
331
  end