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.
- checksums.yaml +4 -4
- data/lib/rodauth/features/active_sessions.rb +2 -2
- data/lib/rodauth/features/audit_logging.rb +2 -1
- data/lib/rodauth/features/base.rb +65 -10
- data/lib/rodauth/features/change_password.rb +11 -2
- data/lib/rodauth/features/confirm_password.rb +1 -1
- data/lib/rodauth/features/create_account.rb +2 -1
- data/lib/rodauth/features/email_auth.rb +13 -4
- data/lib/rodauth/features/email_base.rb +36 -1
- data/lib/rodauth/features/internal_request.rb +4 -0
- data/lib/rodauth/features/json.rb +5 -1
- data/lib/rodauth/features/jwt.rb +7 -1
- data/lib/rodauth/features/jwt_cors.rb +2 -0
- data/lib/rodauth/features/jwt_refresh.rb +33 -18
- data/lib/rodauth/features/lockout.rb +22 -19
- data/lib/rodauth/features/login.rb +12 -4
- data/lib/rodauth/features/login_password_requirements_base.rb +39 -9
- data/lib/rodauth/features/otp.rb +19 -10
- data/lib/rodauth/features/password_expiration.rb +0 -1
- data/lib/rodauth/features/password_grace_period.rb +7 -0
- data/lib/rodauth/features/password_pepper.rb +11 -1
- data/lib/rodauth/features/recovery_codes.rb +4 -1
- data/lib/rodauth/features/remember.rb +2 -0
- data/lib/rodauth/features/require_domain.rb +12 -0
- data/lib/rodauth/features/require_hmac_secret.rb +12 -0
- data/lib/rodauth/features/reset_password.rb +25 -20
- data/lib/rodauth/features/single_session.rb +6 -2
- data/lib/rodauth/features/sms_codes.rb +4 -1
- data/lib/rodauth/features/two_factor_base.rb +2 -1
- data/lib/rodauth/features/update_password_hash.rb +5 -1
- data/lib/rodauth/features/verify_account.rb +29 -20
- data/lib/rodauth/features/verify_login_change.rb +19 -11
- data/lib/rodauth/features/webauthn.rb +10 -5
- data/lib/rodauth/features/webauthn_login.rb +1 -10
- data/lib/rodauth/features/webauthn_verify_account.rb +0 -4
- data/lib/rodauth/version.rb +1 -1
- data/lib/rodauth.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 680f1b9dda9f8dd15399a7a7d1a990ea65299b6d9fb1d42646f9d8661a14b927
|
|
4
|
+
data.tar.gz: e7dd526eb3f3d0cfb1d7b470bd2e9d604e6fe7784657fe6449e1d2b9b128c9f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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=>
|
|
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 =
|
|
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
|
-
|
|
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
|
|
425
|
+
@account_retrieval_type ? @account : (session_value && account_from_session)
|
|
418
426
|
end
|
|
419
427
|
|
|
420
428
|
def account_from_session
|
|
421
|
-
|
|
429
|
+
set_account(_account_from_session, :session)
|
|
422
430
|
end
|
|
423
431
|
|
|
424
432
|
def account_from_id(id, status_id=nil)
|
|
425
|
-
|
|
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
|
|
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),
|
|
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=>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
set_session_return_to_path(confirm_password_redirect_session_key)
|
|
59
59
|
redirect password_authentication_required_redirect
|
|
60
60
|
end
|
|
61
61
|
end
|
|
@@ -89,8 +89,17 @@ module Rodauth
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
r.post do
|
|
92
|
-
key =
|
|
93
|
-
|
|
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
|
-
|
|
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))
|
|
@@ -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, /\
|
|
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
|
data/lib/rodauth/features/jwt.rb
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
47
|
+
else
|
|
48
|
+
@jwt_refresh_key_for_update = true
|
|
45
49
|
transaction do
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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 =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|