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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f2f00f0b0ca5043b852e3f544a666c066b5df09445a4d0031351e37d101bdbf
4
- data.tar.gz: 387de7af25a3d6f4823cc09818d374ff99f1a35ffe8a0329efe107810667cdd6
3
+ metadata.gz: 680f1b9dda9f8dd15399a7a7d1a990ea65299b6d9fb1d42646f9d8661a14b927
4
+ data.tar.gz: e7dd526eb3f3d0cfb1d7b470bd2e9d604e6fe7784657fe6449e1d2b9b128c9f3
5
5
  SHA512:
6
- metadata.gz: 26088c4dc223cdc9f72a07a0de27bff3a3678b29264a46554733d103106443988ac524553f8afa4cb7a457d0cf51c25646adbdf8bd158a7488db449dfc09f7ee
7
- data.tar.gz: da69371a181d62e7ebb74274c56503565e64be26a808dede0df3f7b7c1e230efa5442f35d99609c138439703d2f97335968a0ee720c83c9819ae0de363269046
6
+ metadata.gz: 79f9aa12624e42d2520e01b3a760d1b7bbda370aa7e11fa8987cd3f67a62d15a160ceda4c997afbb636caabc8282f5dfb36689d538e86dc8aea3a178902fb0b3
7
+ data.tar.gz: d183b0f9f350b2321c1ee2cb33ad36d789541d77c2a6ba27082f205dd61fabc0566e881a359d9490bfcccfae853d3e2b3acb30eee02884885e3ebe2838fef358
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:active_sessions, :ActiveSessions) do
5
- depends :logout
5
+ depends :logout, :require_hmac_secret
6
6
 
7
7
  error_flash 'This session has been logged out'
8
8
  redirect
@@ -235,7 +235,7 @@ module Rodauth
235
235
 
236
236
  def active_sessions_ds
237
237
  db[active_sessions_table].
238
- where(active_sessions_account_id_column=>session_value || account_id)
238
+ where(active_sessions_account_id_column=>account_id_or_session_value)
239
239
  end
240
240
 
241
241
  def use_date_arithmetic?
@@ -30,8 +30,9 @@ module Rodauth
30
30
 
31
31
  def hook_action(hook_type, action)
32
32
  super
33
+
33
34
  # In after_logout, session is already cleared, so use before_logout in that case
34
- if (hook_type == :after || action == :logout) && (id = account ? account_id : session_value)
35
+ if (hook_type == :after || action == :logout) && (id = account_id_or_session_value)
35
36
  add_audit_log(id, action)
36
37
  end
37
38
  end
@@ -68,6 +68,7 @@ module Rodauth
68
68
  auth_value_method :prefix, ''
69
69
  auth_value_method :session_key_prefix, nil
70
70
  auth_value_method :require_bcrypt?, true
71
+ auth_value_method :return_to_path_max_size, 2048
71
72
  auth_value_method :mark_input_fields_as_required?, true
72
73
  auth_value_method :mark_input_fields_with_autocomplete?, true
73
74
  auth_value_method :mark_input_fields_with_inputmode?, true
@@ -101,6 +102,7 @@ module Rodauth
101
102
 
102
103
  auth_methods(
103
104
  :account_id,
105
+ :account_reload,
104
106
  :account_session_value,
105
107
  :already_logged_in,
106
108
  :authenticated?,
@@ -115,6 +117,7 @@ module Rodauth
115
117
  :inputmode_for_field?,
116
118
  :logged_in?,
117
119
  :login_required,
120
+ :missing_recommended_configuration,
118
121
  :normalize_login,
119
122
  :null_byte_parameter_value,
120
123
  :open_account?,
@@ -130,7 +133,8 @@ module Rodauth
130
133
  :set_error_reason,
131
134
  :set_title,
132
135
  :translate,
133
- :update_session
136
+ :update_session,
137
+ :valid_return_to_path?
134
138
  )
135
139
 
136
140
  auth_private_methods(
@@ -164,7 +168,9 @@ module Rodauth
164
168
  :@current_route,
165
169
  :@field_errors,
166
170
  :@password_field_autocomplete_value,
167
- :@has_password
171
+ :@has_password,
172
+ :@account_retrieval_type,
173
+ :@existing_password_hash_or_salt
168
174
  )
169
175
 
170
176
  attr_reader :scope
@@ -309,7 +315,7 @@ module Rodauth
309
315
  alias logged_in? session_value
310
316
 
311
317
  def account_from_login(login)
312
- @account = _account_from_login(login)
318
+ set_account(_account_from_login(login), :login)
313
319
  end
314
320
 
315
321
  def open_account?
@@ -337,10 +343,12 @@ module Rodauth
337
343
  end
338
344
 
339
345
  def check_already_logged_in
346
+ # RODAUTH3: Move to login feature
340
347
  already_logged_in if logged_in?
341
348
  end
342
349
 
343
350
  def already_logged_in
351
+ # RODAUTH3: Move to login feature, halt or redirect by default
344
352
  nil
345
353
  end
346
354
 
@@ -414,15 +422,15 @@ module Rodauth
414
422
  end
415
423
 
416
424
  def account!
417
- account || (session_value && account_from_session)
425
+ @account_retrieval_type ? @account : (session_value && account_from_session)
418
426
  end
419
427
 
420
428
  def account_from_session
421
- @account = _account_from_session
429
+ set_account(_account_from_session, :session)
422
430
  end
423
431
 
424
432
  def account_from_id(id, status_id=nil)
425
- @account = _account_from_id(id, status_id)
433
+ set_account(_account_from_id(id, status_id), :id)
426
434
  end
427
435
 
428
436
  def check_csrf
@@ -486,7 +494,7 @@ module Rodauth
486
494
  end
487
495
 
488
496
  def password_match?(password)
489
- if hash = get_password_hash
497
+ if @existing_password_hash_or_salt = hash = get_password_hash
490
498
  if account_password_hash_column || !use_database_authentication_functions?
491
499
  password_hash_match?(hash, password)
492
500
  else
@@ -579,7 +587,7 @@ module Rodauth
579
587
 
580
588
  def has_password?
581
589
  return @has_password unless @has_password.nil?
582
- return false unless account || session_value
590
+ return false unless account_id_or_session_value
583
591
  @has_password = !!get_password_hash
584
592
  end
585
593
 
@@ -599,6 +607,14 @@ module Rodauth
599
607
  s
600
608
  end
601
609
 
610
+ def account_id_or_session_value
611
+ if @account
612
+ account_id
613
+ elsif !@account_retrieval_type
614
+ session_value
615
+ end
616
+ end
617
+
602
618
  if Rack.release >= '3'
603
619
  def set_response_header(key, value)
604
620
  response.headers[key] = value
@@ -627,6 +643,7 @@ module Rodauth
627
643
  Content-Type
628
644
  Content-Length
629
645
  WWW-Authenticate
646
+ Vary
630
647
  END
631
648
  mixed_case_headers.freeze
632
649
  define_method(:convert_response_header_key) do |key|
@@ -649,6 +666,28 @@ module Rodauth
649
666
  end
650
667
  # :nocov:
651
668
 
669
+ def missing_recommended_configuration(msg)
670
+ # RODAUTH3: switch to raise ConfigurationError
671
+ warn msg
672
+ end
673
+
674
+ def account_reload(current_retrieval_type, new_retrieval_type)
675
+ return unless @current_route
676
+
677
+
678
+ if current_retrieval_type == new_retrieval_type
679
+ warn "account retrieved multiple times during rodauth route with retrieval type #{current_retrieval_type}"
680
+ else
681
+ raise Error, "account retrieved multiple times during rodauth route, original retrieval type: #{current_retrieval_type}, new retrieval type: #{new_retrieval_type}"
682
+ end
683
+ end
684
+
685
+ def set_account(account, retrieval_type)
686
+ account_reload(@account_retrieval_type, retrieval_type) if @account_retrieval_type
687
+ @account_retrieval_type = retrieval_type
688
+ @account = account
689
+ end
690
+
652
691
  def database_function_password_match?(name, hash_id, password, salt)
653
692
  db.get(Sequel.function(function_name(name), hash_id, password_hash_using_salt(password, salt)))
654
693
  end
@@ -823,7 +862,7 @@ module Rodauth
823
862
  if account_password_hash_column
824
863
  account[account_password_hash_column] if account!
825
864
  elsif use_database_authentication_functions?
826
- db.get(Sequel.function(function_name(:rodauth_get_salt), account ? account_id : session_value))
865
+ db.get(Sequel.function(function_name(:rodauth_get_salt), account_id_or_session_value))
827
866
  else
828
867
  # :nocov:
829
868
  password_hash_ds.get(password_hash_column)
@@ -831,6 +870,10 @@ module Rodauth
831
870
  end
832
871
  end
833
872
 
873
+ def new_password_matches_current_password?(password)
874
+ password_match?(password)
875
+ end
876
+
834
877
  def _account_from_login(login)
835
878
  ds = account_table_ds.where(login_column=>login)
836
879
  ds = ds.select(*account_select) if account_select
@@ -895,7 +938,7 @@ module Rodauth
895
938
  end
896
939
 
897
940
  def password_hash_ds
898
- db[password_hash_table].where(password_hash_id_column=>account ? account_id : session_value)
941
+ db[password_hash_table].where(password_hash_id_column=>account_id_or_session_value)
899
942
  end
900
943
 
901
944
  # This is needed for jdbc/sqlite, which returns timestamp columns as strings
@@ -968,6 +1011,18 @@ module Rodauth
968
1011
  false
969
1012
  end
970
1013
 
1014
+ def valid_return_to_path?(path)
1015
+ path[0] == "/" &&
1016
+ path[1] != "/" &&
1017
+ path.bytesize <= return_to_path_max_size
1018
+ end
1019
+
1020
+ def set_session_return_to_path(session_key, path=request.fullpath)
1021
+ if request.get? && valid_return_to_path?(path)
1022
+ set_session_value(session_key, path)
1023
+ end
1024
+ end
1025
+
971
1026
  # :nocov:
972
1027
  if RUBY_VERSION >= '4.0'
973
1028
  # :nocov:
@@ -17,6 +17,7 @@ module Rodauth
17
17
 
18
18
  translatable_method :new_password_label, 'New Password'
19
19
  auth_value_method :new_password_param, 'new-password'
20
+ translatable_method :concurrent_password_update_message, 'a concurrent password update prevented the password change'
20
21
 
21
22
  auth_value_methods(
22
23
  :change_password_requires_password?,
@@ -38,15 +39,19 @@ module Rodauth
38
39
  if change_password_requires_password? && !password_match?(param(password_param))
39
40
  throw_error_reason(:invalid_previous_password, invalid_password_error_status, password_param, invalid_previous_password_message)
40
41
  end
42
+ existing_hash = @existing_password_hash_or_salt || get_password_hash
41
43
 
42
44
  password = param(new_password_param)
43
45
  if require_password_confirmation? && password != param(password_confirm_param)
44
46
  throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, new_password_param, passwords_do_not_match_message)
45
47
  end
46
48
 
47
- if password_match?(password)
49
+ if new_password_matches_current_password?(password)
48
50
  throw_error_reason(:same_as_existing_password, invalid_field_error_status, new_password_param, same_as_existing_password_message)
49
51
  end
52
+ # Don't allow same as existing password check for new password to override
53
+ # known existing password hash/salt
54
+ @existing_password_hash_or_salt = existing_hash
50
55
 
51
56
  unless password_meets_requirements?(password)
52
57
  throw_error_status(invalid_field_error_status, new_password_param, password_does_not_meet_requirements_message)
@@ -54,7 +59,11 @@ module Rodauth
54
59
 
55
60
  transaction do
56
61
  before_change_password
57
- set_password(password)
62
+ begin
63
+ set_password(password)
64
+ rescue Rodauth::SetPasswordFailure
65
+ throw_error_reason(:concurrent_password_update, invalid_password_error_status, new_password_param, concurrent_password_update_message)
66
+ end
58
67
  clear_tokens(:change_password)
59
68
  after_change_password
60
69
  end
@@ -55,7 +55,7 @@ module Rodauth
55
55
  set_redirect_error_status(password_authentication_required_error_status)
56
56
  set_error_reason :password_authentication_required
57
57
  set_redirect_error_flash password_authentication_required_error_flash
58
- set_session_value(confirm_password_redirect_session_key, request.fullpath)
58
+ set_session_return_to_path(confirm_password_redirect_session_key)
59
59
  redirect password_authentication_required_redirect
60
60
  end
61
61
  end
@@ -94,7 +94,8 @@ module Rodauth
94
94
  end
95
95
 
96
96
  def new_account(login)
97
- @account = _new_account(login)
97
+ @account_retrieval_type = nil
98
+ set_account(_new_account(login), :new)
98
99
  end
99
100
 
100
101
  def save_account
@@ -89,8 +89,17 @@ module Rodauth
89
89
  end
90
90
 
91
91
  r.post do
92
- key = session[email_auth_session_key] || param(email_auth_key_param)
93
- unless account_from_email_auth_key(key)
92
+ key = session_param(email_auth_session_key, email_auth_key_param)
93
+ remove_session_value(email_auth_session_key)
94
+ select_key_for_update!
95
+
96
+ valid = transaction do
97
+ if key && account_from_email_auth_key(key)
98
+ remove_email_auth_key == 1
99
+ end
100
+ end
101
+
102
+ unless valid
94
103
  set_redirect_error_status(invalid_key_error_status)
95
104
  set_error_reason :invalid_email_auth_key
96
105
  set_redirect_error_flash email_auth_error_flash
@@ -131,7 +140,7 @@ module Rodauth
131
140
  end
132
141
 
133
142
  def account_from_email_auth_key(key)
134
- @account = _account_from_email_auth_key(key)
143
+ set_account(_account_from_email_auth_key(key), :email_auth)
135
144
  end
136
145
 
137
146
  def email_auth_email_link
@@ -141,7 +150,7 @@ module Rodauth
141
150
  def get_email_auth_key(id)
142
151
  ds = email_auth_ds(id)
143
152
  ds.where(Sequel::CURRENT_TIMESTAMP > email_auth_deadline_column).delete
144
- ds.get(email_auth_key_column)
153
+ apply_key_for_update(ds).get(email_auth_key_column)
145
154
  end
146
155
 
147
156
  def email_auth_request_form
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:email_base, :EmailBase) do
5
+ depends :require_domain, :require_hmac_secret
6
+
5
7
  translatable_method :email_subject_prefix, ''
6
8
  auth_value_method :require_mail?, true
7
9
  auth_value_method :allow_raw_email_token?, false
@@ -9,7 +11,8 @@ module Rodauth
9
11
  redirect :default_post_email
10
12
 
11
13
  auth_value_methods(
12
- :email_from
14
+ :email_from,
15
+ :allow_param_fallback_for_session_param?
13
16
  )
14
17
 
15
18
  auth_methods(
@@ -18,6 +21,10 @@ module Rodauth
18
21
  :send_email
19
22
  )
20
23
 
24
+ uses_instance_variables(
25
+ :@key_for_update
26
+ )
27
+
21
28
  def post_configure
22
29
  super
23
30
  require 'mail' if require_mail?
@@ -58,10 +65,38 @@ module Rodauth
58
65
  "#{account_id}#{token_separator}#{convert_email_token_key(key)}"
59
66
  end
60
67
 
68
+ def session_param(session_key, param_name)
69
+ unless value = session[session_key]
70
+ if allow_param_fallback_for_session_param?
71
+ value = param(param_name)
72
+ end
73
+ end
74
+
75
+ value
76
+ end
77
+
78
+ def allow_param_fallback_for_session_param?
79
+ return super if defined?(super)
80
+ false
81
+ end
82
+
61
83
  def convert_email_token_key(key)
62
84
  convert_token_key(key)
63
85
  end
64
86
 
87
+ def select_key_for_update!
88
+ @key_for_update = true
89
+ end
90
+
91
+ def apply_key_for_update(ds)
92
+ if @key_for_update
93
+ @key_for_update = nil
94
+ ds.for_update
95
+ else
96
+ ds
97
+ end
98
+ end
99
+
65
100
  def account_from_key(token, status_id=nil)
66
101
  id, key = split_token(token)
67
102
  return unless key && (id = convert_token_id(id))
@@ -120,6 +120,10 @@ module Rodauth
120
120
 
121
121
  private
122
122
 
123
+ def allow_param_fallback_for_session_param?
124
+ true
125
+ end
126
+
123
127
  def internal_request?
124
128
  true
125
129
  end
@@ -8,7 +8,7 @@ module Rodauth
8
8
  translatable_method :json_non_post_error_message, 'non-POST method used in JSON API'
9
9
  auth_value_method :json_accept_regexp, /(?:(?:\*|\bapplication)\/\*|\bapplication\/(?:vnd\.api\+)?json\b)/i
10
10
  auth_value_method :json_check_accept?, true
11
- auth_value_method :json_request_content_type_regexp, /\bapplication\/(?:vnd\.api\+)?json\b/i
11
+ auth_value_method :json_request_content_type_regexp, /\Aapplication\/(?:vnd\.api\+)?json\b/i
12
12
  auth_value_method :json_response_content_type, 'application/json'
13
13
  auth_value_method :json_response_custom_error_status?, true
14
14
  auth_value_method :json_response_error_status, 400
@@ -81,6 +81,10 @@ module Rodauth
81
81
  super
82
82
  end
83
83
 
84
+ def allow_param_fallback_for_session_param?
85
+ use_json?
86
+ end
87
+
84
88
  def _set_otp_unlock_info
85
89
  if use_json?
86
90
  json_response[:num_successes] = otp_unlock_num_successes
@@ -145,7 +145,13 @@ module Rodauth
145
145
  end
146
146
 
147
147
  def return_json_response
148
- set_jwt
148
+ case response.status
149
+ when 405, 406
150
+ response.headers.delete('authorization')
151
+ else
152
+ set_jwt
153
+ end
154
+
149
155
  super
150
156
  end
151
157
 
@@ -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
@@ -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