rodauth 2.46.0 → 2.48.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rodauth/features/active_sessions.rb +2 -2
  3. data/lib/rodauth/features/argon2.rb +6 -6
  4. data/lib/rodauth/features/audit_logging.rb +4 -3
  5. data/lib/rodauth/features/base.rb +84 -30
  6. data/lib/rodauth/features/change_password.rb +11 -2
  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/disallow_password_reuse.rb +2 -2
  10. data/lib/rodauth/features/email_auth.rb +13 -4
  11. data/lib/rodauth/features/email_base.rb +36 -1
  12. data/lib/rodauth/features/http_basic_auth.rb +6 -1
  13. data/lib/rodauth/features/internal_request.rb +4 -0
  14. data/lib/rodauth/features/json.rb +5 -1
  15. data/lib/rodauth/features/jwt.rb +9 -3
  16. data/lib/rodauth/features/jwt_cors.rb +2 -0
  17. data/lib/rodauth/features/jwt_refresh.rb +33 -18
  18. data/lib/rodauth/features/lockout.rb +24 -21
  19. data/lib/rodauth/features/login.rb +12 -4
  20. data/lib/rodauth/features/login_password_requirements_base.rb +43 -13
  21. data/lib/rodauth/features/otp.rb +21 -12
  22. data/lib/rodauth/features/otp_unlock.rb +2 -2
  23. data/lib/rodauth/features/password_complexity.rb +2 -2
  24. data/lib/rodauth/features/password_expiration.rb +0 -1
  25. data/lib/rodauth/features/password_grace_period.rb +8 -0
  26. data/lib/rodauth/features/password_pepper.rb +11 -1
  27. data/lib/rodauth/features/recovery_codes.rb +4 -1
  28. data/lib/rodauth/features/remember.rb +4 -2
  29. data/lib/rodauth/features/require_domain.rb +12 -0
  30. data/lib/rodauth/features/require_hmac_secret.rb +12 -0
  31. data/lib/rodauth/features/reset_password.rb +25 -20
  32. data/lib/rodauth/features/single_session.rb +6 -2
  33. data/lib/rodauth/features/sms_codes.rb +4 -1
  34. data/lib/rodauth/features/two_factor_base.rb +2 -1
  35. data/lib/rodauth/features/update_password_hash.rb +5 -1
  36. data/lib/rodauth/features/verify_account.rb +29 -20
  37. data/lib/rodauth/features/verify_login_change.rb +19 -11
  38. data/lib/rodauth/features/webauthn.rb +16 -11
  39. data/lib/rodauth/features/webauthn_login.rb +1 -10
  40. data/lib/rodauth/features/webauthn_verify_account.rb +0 -4
  41. data/lib/rodauth/version.rb +1 -1
  42. data/lib/rodauth.rb +14 -12
  43. metadata +4 -2
@@ -8,6 +8,7 @@ module Rodauth
8
8
  auth_value_method :jwt_cors_allow_methods, 'POST'
9
9
  auth_value_method :jwt_cors_allow_headers, 'Content-Type, Authorization, Accept'
10
10
  auth_value_method :jwt_cors_expose_headers, 'Authorization'
11
+ auth_value_method :jwt_cors_vary_header, 'Origin'
11
12
  auth_value_method :jwt_cors_max_age, 86400
12
13
 
13
14
  auth_methods(:jwt_cors_allow?)
@@ -33,6 +34,7 @@ module Rodauth
33
34
 
34
35
  def before_rodauth
35
36
  if jwt_cors_allow?
37
+ set_response_header('vary', jwt_cors_vary_header)
36
38
  set_response_header('access-control-allow-origin', request.env['HTTP_ORIGIN'])
37
39
 
38
40
  # Handle CORS preflight request
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:jwt_refresh, :JwtRefresh) do
5
- depends :jwt
5
+ depends :jwt, :require_hmac_secret
6
6
 
7
7
  after 'refresh_token'
8
8
  before 'refresh_token'
@@ -31,28 +31,34 @@ module Rodauth
31
31
  :account_from_refresh_token
32
32
  )
33
33
 
34
- uses_instance_variables(:@jwt_refresh_route)
34
+ uses_instance_variables(
35
+ :@jwt_refresh_route,
36
+ :@jwt_refresh_key_for_update,
37
+ )
35
38
 
36
39
  route do |r|
37
- @jwt_refresh_route = true
38
40
  before_jwt_refresh_route
39
41
 
40
42
  r.post do
43
+ @jwt_refresh_route = true
41
44
  if !session_value
42
45
  response.status ||= jwt_refresh_without_access_token_status
43
46
  json_response[json_response_error_key] = jwt_refresh_without_access_token_message
44
- elsif (refresh_token = param_or_nil(jwt_refresh_token_key_param)) && account_from_refresh_token(refresh_token)
47
+ else
48
+ @jwt_refresh_key_for_update = true
45
49
  transaction do
46
- before_refresh_token
47
- formatted_token = generate_refresh_token
48
- remove_jwt_refresh_token_key(refresh_token)
49
- set_jwt_refresh_token_hmac_session_key(formatted_token)
50
- json_response[jwt_refresh_token_key] = formatted_token
51
- json_response[jwt_access_token_key] = session_jwt
52
- after_refresh_token
50
+ if (refresh_token = param_or_nil(jwt_refresh_token_key_param)) && account_from_refresh_token(refresh_token)
51
+ before_refresh_token
52
+ formatted_token = generate_refresh_token
53
+ remove_jwt_refresh_token_key(refresh_token)
54
+ set_jwt_refresh_token_hmac_session_key(formatted_token)
55
+ json_response[jwt_refresh_token_key] = formatted_token
56
+ json_response[jwt_access_token_key] = session_jwt
57
+ after_refresh_token
58
+ else
59
+ json_response[json_response_error_key] = jwt_refresh_invalid_token_message
60
+ end
53
61
  end
54
- else
55
- json_response[json_response_error_key] = jwt_refresh_invalid_token_message
56
62
  end
57
63
  _return_json_response
58
64
  end
@@ -88,7 +94,7 @@ module Rodauth
88
94
  end
89
95
 
90
96
  def account_from_refresh_token(token)
91
- @account = _account_from_refresh_token(token)
97
+ set_account(_account_from_refresh_token(token), :jwt_refresh)
92
98
  end
93
99
 
94
100
  def clear_tokens(reason)
@@ -142,7 +148,7 @@ module Rodauth
142
148
  end
143
149
 
144
150
  def _jwt_decode_opts
145
- if allow_refresh_with_expired_jwt_access_token? && (@jwt_refresh_route || request.path == jwt_refresh_path)
151
+ if allow_refresh_with_expired_jwt_access_token? && (@jwt_refresh_route || (request.path == jwt_refresh_path && request.request_method == "POST"))
146
152
  Hash[super].merge!(:verify_expiration=>false)
147
153
  else
148
154
  super
@@ -165,8 +171,12 @@ module Rodauth
165
171
  where(Sequel::CURRENT_TIMESTAMP > jwt_refresh_token_deadline_column).
166
172
  delete
167
173
 
168
- jwt_refresh_token_account_token_ds(account_id, token_id).
169
- get(jwt_refresh_token_key_column)
174
+ ds = jwt_refresh_token_account_token_ds(account_id, token_id)
175
+ if @jwt_refresh_key_for_update
176
+ ds = ds.for_update
177
+ @jwt_refresh_key_for_update = nil
178
+ end
179
+ ds.get(jwt_refresh_token_key_column)
170
180
  end
171
181
 
172
182
  def jwt_refresh_token_account_ds(account_id)
@@ -214,7 +224,12 @@ module Rodauth
214
224
  else
215
225
  id, token_id, key = _account_refresh_token_split(token)
216
226
 
217
- if id && token_id && key && (actual = get_active_refresh_token(session_value, token_id)) && timing_safe_eql?(key, convert_token_key(actual))
227
+ if id &&
228
+ token_id &&
229
+ key &&
230
+ (actual = get_active_refresh_token(session_value, token_id)) &&
231
+ (timing_safe_eql?(key, convert_token_key(actual)) ||
232
+ (hmac_secret_rotation? && timing_safe_eql?(key, compute_old_hmac(actual))))
218
233
  jwt_refresh_token_account_token_ds(id, token_id).delete
219
234
  end
220
235
  end
@@ -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
@@ -184,11 +187,11 @@ module Rodauth
184
187
  with_sql(:update_sql, account_login_failures_number_column=>Sequel.expr(account_login_failures_number_column)+1).
185
188
  single_value
186
189
  else
187
- # :nocov:
190
+ # simplecov:disable
188
191
  if ds.update(account_login_failures_number_column=>Sequel.expr(account_login_failures_number_column)+1) > 0
189
192
  ds.get(account_login_failures_number_column)
190
193
  end
191
- # :nocov:
194
+ # simplecov:enable
192
195
  end
193
196
 
194
197
  unless number
@@ -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
+ # simplecov:disable
85
+ else
86
+ password_hash_column
87
+ # simplecov:enable
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
@@ -133,12 +159,12 @@ module Rodauth
133
159
  def login_confirmation_matches?(login, login_confirmation)
134
160
  login.casecmp?(login_confirmation)
135
161
  end
136
- # :nocov:
162
+ # simplecov:disable
137
163
  else
138
164
  def login_confirmation_matches?(login, login_confirmation)
139
165
  login.casecmp(login_confirmation) == 0
140
166
  end
141
- # :nocov:
167
+ # simplecov:enable
142
168
  end
143
169
 
144
170
  def login_meets_length_requirements?(login)
@@ -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)
@@ -193,11 +223,11 @@ module Rodauth
193
223
  BCrypt::Engine::MIN_COST
194
224
  end
195
225
  else
196
- # :nocov:
226
+ # simplecov:disable
197
227
  def password_hash_cost
198
228
  BCrypt::Engine::DEFAULT_COST
199
229
  end
200
- # :nocov:
230
+ # simplecov:enable
201
231
  end
202
232
 
203
233
  def extract_password_hash_cost(hash)
@@ -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
+ # simplecov:disable
268
274
  if otp.respond_to?(:verify_with_drift)
269
- # :nocov:
270
275
  otp.verify_with_drift(ot_pass, drift)
271
- # :nocov:
276
+ # simplecov:enable
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
+ # simplecov:disable
281
+ elsif otp.respond_to?(:verify_with_drift)
276
282
  otp.verify(ot_pass)
283
+ # simplecov:enable
284
+ else
285
+ otp.verify(ot_pass, :after=>otp_last_use)
277
286
  end
278
287
  end
279
288
 
@@ -404,11 +413,11 @@ module Rodauth
404
413
  ROTP::Base32.random_base32.downcase
405
414
  end
406
415
  else
407
- # :nocov:
416
+ # simplecov:disable
408
417
  def otp_new_secret
409
418
  ROTP::Base32.random.downcase
410
419
  end
411
- # :nocov:
420
+ # simplecov:enable
412
421
  end
413
422
 
414
423
  def base32_encode(data, length)
@@ -181,9 +181,9 @@ module Rodauth
181
181
  end
182
182
 
183
183
  @otp_unlock_data = nil
184
- # :nocov:
184
+ # simplecov:disable
185
185
  if otp_unlock_data
186
- # :nocov:
186
+ # simplecov:enable
187
187
  if otp_unlock_num_successes >= otp_unlock_auths_required
188
188
  # At least the requisite number of consecutive successful unlock
189
189
  # authentications. Unlock OTP authentication.
@@ -33,9 +33,9 @@ module Rodauth
33
33
  # nothing
34
34
  when nil
35
35
  default_dictionary_file = '/usr/share/dict/words'
36
- # :nocov:
36
+ # simplecov:disable
37
37
  if File.file?(default_dictionary_file)
38
- # :nocov:
38
+ # simplecov:enable
39
39
  words = File.read(default_dictionary_file)
40
40
  end
41
41
  else
@@ -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,14 @@ module Rodauth
33
33
 
34
34
  private
35
35
 
36
+ def new_password_matches_current_password?(_)
37
+ last_password_entry = session[last_password_entry_session_key]
38
+ if v = super
39
+ set_session_value(last_password_entry_session_key, last_password_entry)
40
+ end
41
+ v
42
+ end
43
+
36
44
  def after_create_account
37
45
  super if defined?(super)
38
46
  @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'
@@ -68,9 +70,9 @@ module Rodauth
68
70
  if [remember_remember_param_value, remember_forget_param_value, remember_disable_param_value].include?(remember)
69
71
  transaction do
70
72
  before_remember
71
- # :nocov:
73
+ # simplecov:disable
72
74
  case remember
73
- # :nocov:
75
+ # simplecov:enable
74
76
  when remember_remember_param_value
75
77
  remember_login
76
78
  when remember_forget_param_value
@@ -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)))