rodauth 2.30.0 → 2.32.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +40 -0
- data/README.rdoc +4 -1
- data/doc/argon2.rdoc +9 -5
- data/doc/base.rdoc +1 -0
- data/doc/change_login.rdoc +1 -0
- data/doc/change_password.rdoc +1 -0
- data/doc/close_account.rdoc +1 -0
- data/doc/confirm_password.rdoc +1 -0
- data/doc/create_account.rdoc +1 -0
- data/doc/email_auth.rdoc +1 -0
- data/doc/error_reasons.rdoc +1 -0
- data/doc/guides/registration_field.rdoc +1 -1
- data/doc/guides/render_confirmation.rdoc +17 -0
- data/doc/internal_request.rdoc +76 -0
- data/doc/json.rdoc +1 -0
- data/doc/jwt.rdoc +7 -1
- data/doc/jwt_refresh.rdoc +1 -1
- data/doc/lockout.rdoc +4 -2
- data/doc/login.rdoc +2 -1
- data/doc/logout.rdoc +1 -0
- data/doc/otp.rdoc +3 -0
- data/doc/release_notes/2.31.0.txt +47 -0
- data/doc/release_notes/2.32.0.txt +65 -0
- data/doc/remember.rdoc +1 -0
- data/doc/reset_password.rdoc +2 -0
- data/doc/sms_codes.rdoc +5 -0
- data/doc/two_factor_base.rdoc +2 -0
- data/doc/verify_account.rdoc +2 -0
- data/doc/verify_login_change.rdoc +1 -0
- data/doc/webauthn.rdoc +2 -0
- data/doc/webauthn_autofill.rdoc +5 -0
- data/doc/webauthn_login.rdoc +1 -0
- data/lib/rodauth/features/active_sessions.rb +10 -4
- data/lib/rodauth/features/argon2.rb +54 -15
- data/lib/rodauth/features/base.rb +39 -4
- data/lib/rodauth/features/change_login.rb +2 -2
- data/lib/rodauth/features/change_password.rb +2 -2
- data/lib/rodauth/features/close_account.rb +2 -2
- data/lib/rodauth/features/confirm_password.rb +2 -2
- data/lib/rodauth/features/create_account.rb +2 -2
- data/lib/rodauth/features/email_auth.rb +7 -12
- data/lib/rodauth/features/email_base.rb +4 -6
- data/lib/rodauth/features/internal_request.rb +47 -1
- data/lib/rodauth/features/json.rb +6 -1
- data/lib/rodauth/features/jwt.rb +17 -1
- data/lib/rodauth/features/jwt_refresh.rb +4 -2
- data/lib/rodauth/features/lockout.rb +6 -6
- data/lib/rodauth/features/login.rb +13 -4
- data/lib/rodauth/features/logout.rb +2 -2
- data/lib/rodauth/features/otp.rb +35 -7
- data/lib/rodauth/features/remember.rb +15 -11
- data/lib/rodauth/features/reset_password.rb +5 -5
- data/lib/rodauth/features/single_session.rb +4 -3
- data/lib/rodauth/features/sms_codes.rb +23 -10
- data/lib/rodauth/features/two_factor_base.rb +8 -6
- data/lib/rodauth/features/update_password_hash.rb +2 -1
- data/lib/rodauth/features/verify_account.rb +7 -12
- data/lib/rodauth/features/verify_login_change.rb +2 -2
- data/lib/rodauth/features/webauthn.rb +12 -6
- data/lib/rodauth/features/webauthn_autofill.rb +7 -1
- data/lib/rodauth/features/webauthn_login.rb +19 -0
- data/lib/rodauth/version.rb +1 -1
- data/lib/rodauth.rb +16 -0
- metadata +7 -2
data/doc/webauthn_autofill.rdoc
CHANGED
@@ -4,10 +4,15 @@ The webauthn_autofill feature enables autofill UI (aka "conditional mediation")
|
|
4
4
|
for WebAuthn credentials, logging the user in on selection. It depends on the
|
5
5
|
webauthn_login feature.
|
6
6
|
|
7
|
+
This feature allows generating WebAuthn credential options and submitting a
|
8
|
+
WebAuthn login request without providing a login, which can be used
|
9
|
+
independently from the autofill UI.
|
10
|
+
|
7
11
|
== Auth Value Methods
|
8
12
|
|
9
13
|
webauthn_autofill_js :: The javascript code to execute on the login page to enable autofill UI.
|
10
14
|
webauthn_autofill_js_route :: The route to the webauthn autofill javascript file.
|
15
|
+
webauthn_invalid_webauthn_id_message :: The error message to show when provided WebAuthn ID wasn't found in the database.
|
11
16
|
|
12
17
|
== Auth Methods
|
13
18
|
|
data/doc/webauthn_login.rdoc
CHANGED
@@ -5,6 +5,7 @@ WebAuthn. It depends on the login and webauthn features.
|
|
5
5
|
|
6
6
|
== Auth Value Methods
|
7
7
|
|
8
|
+
webauthn_login_user_verification_additional_factor? :: Whether passwordless login via WebAuthn should consider user verification as 2nd factor when using multifactor authentication, false by default. Setting this to true means that the app trusts the user verification done by the authenticator is strong enough to be considered an additional factor.
|
8
9
|
webauthn_login_error_flash :: The flash error to show if there is a failure during passwordless login via WebAuthn.
|
9
10
|
webauthn_login_failure_redirect :: Whether to redirect if there is a failure during passwordless login via WebAuthn.
|
10
11
|
webauthn_login_route :: The route to the webauthn login action.
|
@@ -40,7 +40,7 @@ module Rodauth
|
|
40
40
|
|
41
41
|
remove_inactive_sessions
|
42
42
|
ds = active_sessions_ds.
|
43
|
-
where(active_sessions_session_id_column =>
|
43
|
+
where(active_sessions_session_id_column => compute_hmacs(session_id))
|
44
44
|
|
45
45
|
if update_current_session?
|
46
46
|
ds.update(active_sessions_update_hash) == 1
|
@@ -83,7 +83,7 @@ module Rodauth
|
|
83
83
|
|
84
84
|
def remove_current_session
|
85
85
|
if session_id = session[session_id_session_key]
|
86
|
-
remove_active_session(
|
86
|
+
remove_active_session(compute_hmacs(session_id))
|
87
87
|
end
|
88
88
|
end
|
89
89
|
|
@@ -119,7 +119,7 @@ module Rodauth
|
|
119
119
|
key = generate_active_sessions_key
|
120
120
|
set_session_value(session_id_session_key, key)
|
121
121
|
active_sessions_ds.
|
122
|
-
where(active_sessions_session_id_column =>
|
122
|
+
where(active_sessions_session_id_column => compute_hmacs(prev_key)).
|
123
123
|
update(active_sessions_session_id_column => compute_hmac(key))
|
124
124
|
end
|
125
125
|
end
|
@@ -150,7 +150,13 @@ module Rodauth
|
|
150
150
|
end
|
151
151
|
|
152
152
|
def active_sessions_update_hash
|
153
|
-
{active_sessions_last_use_column => Sequel::CURRENT_TIMESTAMP}
|
153
|
+
h = {active_sessions_last_use_column => Sequel::CURRENT_TIMESTAMP}
|
154
|
+
|
155
|
+
if hmac_secret_rotation?
|
156
|
+
h[active_sessions_session_id_column] = compute_hmac(session[session_id_session_key])
|
157
|
+
end
|
158
|
+
|
159
|
+
h
|
154
160
|
end
|
155
161
|
|
156
162
|
def session_inactivity_deadline_condition
|
@@ -12,6 +12,7 @@ module Rodauth
|
|
12
12
|
Feature.define(:argon2, :Argon2) do
|
13
13
|
depends :login_password_requirements_base
|
14
14
|
|
15
|
+
auth_value_method :argon2_old_secret, nil
|
15
16
|
auth_value_method :argon2_secret, nil
|
16
17
|
auth_value_method :use_argon2?, true
|
17
18
|
|
@@ -43,7 +44,7 @@ module Rodauth
|
|
43
44
|
|
44
45
|
def password_hash_cost
|
45
46
|
return super unless use_argon2?
|
46
|
-
argon2_hash_cost
|
47
|
+
argon2_hash_cost
|
47
48
|
end
|
48
49
|
|
49
50
|
def password_hash_match?(hash, password)
|
@@ -53,28 +54,50 @@ module Rodauth
|
|
53
54
|
|
54
55
|
def password_hash_using_salt(password, salt)
|
55
56
|
return super unless argon2_hash_algorithm?(salt)
|
57
|
+
argon2_password_hash_using_salt_and_secret(password, salt, argon2_secret)
|
58
|
+
end
|
56
59
|
|
60
|
+
def argon2_password_hash_using_salt_and_secret(password, salt, secret)
|
57
61
|
argon2_params = Hash[extract_password_hash_cost(salt)]
|
58
|
-
argon2_params[argon2_salt_option] =
|
59
|
-
argon2_params[:secret] =
|
62
|
+
argon2_params[argon2_salt_option] = salt.split('$').last.unpack("m")[0]
|
63
|
+
argon2_params[:secret] = secret
|
60
64
|
::Argon2::Password.new(argon2_params).create(password)
|
61
65
|
end
|
62
66
|
|
63
|
-
|
64
|
-
|
67
|
+
if Argon2::VERSION >= '2.1'
|
68
|
+
def extract_password_hash_cost(hash)
|
69
|
+
return super unless argon2_hash_algorithm?(hash)
|
65
70
|
|
66
|
-
|
67
|
-
|
68
|
-
|
71
|
+
/\A\$argon2id\$v=\d+\$m=(\d+),t=(\d+),p=(\d+)/ =~ hash
|
72
|
+
{ t_cost: $2.to_i, m_cost: Math.log2($1.to_i).to_i, p_cost: $3.to_i }
|
73
|
+
end
|
69
74
|
|
70
|
-
|
71
|
-
|
72
|
-
|
75
|
+
if ENV['RACK_ENV'] == 'test'
|
76
|
+
def argon2_hash_cost
|
77
|
+
{ t_cost: 1, m_cost: 5, p_cost: 1 }
|
78
|
+
end
|
79
|
+
# :nocov:
|
80
|
+
else
|
81
|
+
def argon2_hash_cost
|
82
|
+
{ t_cost: 2, m_cost: 16, p_cost: 1 }
|
83
|
+
end
|
73
84
|
end
|
74
|
-
# :nocov:
|
75
85
|
else
|
76
|
-
def
|
77
|
-
|
86
|
+
def extract_password_hash_cost(hash)
|
87
|
+
return super unless argon2_hash_algorithm?(hash )
|
88
|
+
|
89
|
+
/\A\$argon2id\$v=\d+\$m=(\d+),t=(\d+)/ =~ hash
|
90
|
+
{ t_cost: $2.to_i, m_cost: Math.log2($1.to_i).to_i }
|
91
|
+
end
|
92
|
+
|
93
|
+
if ENV['RACK_ENV'] == 'test'
|
94
|
+
def argon2_hash_cost
|
95
|
+
{ t_cost: 1, m_cost: 5 }
|
96
|
+
end
|
97
|
+
else
|
98
|
+
def argon2_hash_cost
|
99
|
+
{ t_cost: 2, m_cost: 16 }
|
100
|
+
end
|
78
101
|
end
|
79
102
|
end
|
80
103
|
# :nocov:
|
@@ -84,7 +107,23 @@ module Rodauth
|
|
84
107
|
end
|
85
108
|
|
86
109
|
def argon2_password_hash_match?(hash, password)
|
87
|
-
::Argon2::Password.verify_password(password, hash, argon2_secret)
|
110
|
+
ret = ::Argon2::Password.verify_password(password, hash, argon2_secret)
|
111
|
+
|
112
|
+
if ret == false && argon2_old_secret != argon2_secret && (ret = ::Argon2::Password.verify_password(password, hash, argon2_old_secret))
|
113
|
+
@update_password_hash = true
|
114
|
+
end
|
115
|
+
|
116
|
+
ret
|
117
|
+
end
|
118
|
+
|
119
|
+
def database_function_password_match?(name, hash_id, password, salt)
|
120
|
+
return true if super
|
121
|
+
|
122
|
+
if use_argon2? && argon2_hash_algorithm?(salt) && argon2_old_secret != argon2_secret && (ret = db.get(Sequel.function(function_name(name), hash_id, argon2_password_hash_using_salt_and_secret(password, salt, argon2_old_secret))))
|
123
|
+
@update_password_hash = true
|
124
|
+
end
|
125
|
+
|
126
|
+
!!ret
|
88
127
|
end
|
89
128
|
end
|
90
129
|
end
|
@@ -27,6 +27,7 @@ module Rodauth
|
|
27
27
|
auth_value_method :convert_token_id_to_integer?, nil
|
28
28
|
flash_key :flash_error_key, :error
|
29
29
|
flash_key :flash_notice_key, :notice
|
30
|
+
auth_value_method :hmac_old_secret, nil
|
30
31
|
auth_value_method :hmac_secret, nil
|
31
32
|
translatable_method :input_field_label_suffix, ''
|
32
33
|
auth_value_method :input_field_error_class, 'error is-invalid'
|
@@ -242,9 +243,24 @@ module Rodauth
|
|
242
243
|
|
243
244
|
# Return urlsafe base64 HMAC for data, assumes hmac_secret is set.
|
244
245
|
def compute_hmac(data)
|
245
|
-
|
246
|
-
|
247
|
-
|
246
|
+
_process_raw_hmac(compute_raw_hmac(data))
|
247
|
+
end
|
248
|
+
|
249
|
+
# Return urlsafe base64 HMAC for data using hmac_old_secret, assumes hmac_old_secret is set.
|
250
|
+
def compute_old_hmac(data)
|
251
|
+
_process_raw_hmac(compute_raw_hmac_with_secret(data, hmac_old_secret))
|
252
|
+
end
|
253
|
+
|
254
|
+
# Return array of hmacs. Array has two strings if hmac_old_secret
|
255
|
+
# is set, or one string otherwise.
|
256
|
+
def compute_hmacs(data)
|
257
|
+
hmacs = [compute_hmac(data)]
|
258
|
+
|
259
|
+
if hmac_old_secret
|
260
|
+
hmacs << compute_old_hmac(data)
|
261
|
+
end
|
262
|
+
|
263
|
+
hmacs
|
248
264
|
end
|
249
265
|
|
250
266
|
def account_id
|
@@ -515,6 +531,13 @@ module Rodauth
|
|
515
531
|
yield
|
516
532
|
end
|
517
533
|
|
534
|
+
def _process_raw_hmac(hmac)
|
535
|
+
s = [hmac].pack('m')
|
536
|
+
s.chomp!("=\n")
|
537
|
+
s.tr!('+/', '-_')
|
538
|
+
s
|
539
|
+
end
|
540
|
+
|
518
541
|
def database_function_password_match?(name, hash_id, password, salt)
|
519
542
|
db.get(Sequel.function(function_name(name), hash_id, password_hash_using_salt(password, salt)))
|
520
543
|
end
|
@@ -711,10 +734,17 @@ module Rodauth
|
|
711
734
|
ds.first
|
712
735
|
end
|
713
736
|
|
737
|
+
def hmac_secret_rotation?
|
738
|
+
hmac_secret && hmac_old_secret && hmac_secret != hmac_old_secret
|
739
|
+
end
|
740
|
+
|
714
741
|
def compute_raw_hmac(data)
|
715
742
|
raise ArgumentError, "hmac_secret not set" unless hmac_secret
|
743
|
+
compute_raw_hmac_with_secret(data, hmac_secret)
|
744
|
+
end
|
716
745
|
|
717
|
-
|
746
|
+
def compute_raw_hmac_with_secret(data, secret)
|
747
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, data)
|
718
748
|
end
|
719
749
|
|
720
750
|
def _field_attributes(field)
|
@@ -822,6 +852,11 @@ module Rodauth
|
|
822
852
|
false
|
823
853
|
end
|
824
854
|
|
855
|
+
def require_response(meth)
|
856
|
+
send(meth)
|
857
|
+
raise RuntimeError, "#{meth.to_s.sub(/\A_/, '')} overridden without returning a response (should use redirect or request.halt). This is a bug in your Rodauth configuration, not a bug in Rodauth itself."
|
858
|
+
end
|
859
|
+
|
825
860
|
def set_session_value(key, value)
|
826
861
|
session[key] = value
|
827
862
|
end
|
@@ -14,6 +14,7 @@ module Rodauth
|
|
14
14
|
additional_form_tags
|
15
15
|
button 'Change Login'
|
16
16
|
redirect
|
17
|
+
response
|
17
18
|
|
18
19
|
auth_value_methods :change_login_requires_password?
|
19
20
|
|
@@ -51,9 +52,8 @@ module Rodauth
|
|
51
52
|
end
|
52
53
|
|
53
54
|
after_change_login
|
54
|
-
set_notice_flash change_login_notice_flash
|
55
|
-
redirect change_login_redirect
|
56
55
|
end
|
56
|
+
change_login_response
|
57
57
|
end
|
58
58
|
|
59
59
|
set_error_flash change_login_error_flash
|
@@ -13,6 +13,7 @@ module Rodauth
|
|
13
13
|
additional_form_tags
|
14
14
|
button 'Change Password'
|
15
15
|
redirect
|
16
|
+
response
|
16
17
|
|
17
18
|
translatable_method :new_password_label, 'New Password'
|
18
19
|
auth_value_method :new_password_param, 'new-password'
|
@@ -56,8 +57,7 @@ module Rodauth
|
|
56
57
|
set_password(password)
|
57
58
|
after_change_password
|
58
59
|
end
|
59
|
-
|
60
|
-
redirect change_password_redirect
|
60
|
+
change_password_response
|
61
61
|
end
|
62
62
|
|
63
63
|
set_error_flash change_password_error_flash
|
@@ -11,6 +11,7 @@ module Rodauth
|
|
11
11
|
after
|
12
12
|
before
|
13
13
|
redirect
|
14
|
+
response
|
14
15
|
|
15
16
|
auth_value_method :account_closed_status_value, 3
|
16
17
|
|
@@ -50,8 +51,7 @@ module Rodauth
|
|
50
51
|
end
|
51
52
|
clear_session
|
52
53
|
|
53
|
-
|
54
|
-
redirect close_account_redirect
|
54
|
+
close_account_response
|
55
55
|
end
|
56
56
|
|
57
57
|
set_error_flash close_account_error_flash
|
@@ -11,6 +11,7 @@ module Rodauth
|
|
11
11
|
button 'Confirm Password'
|
12
12
|
before
|
13
13
|
after
|
14
|
+
response
|
14
15
|
redirect(:password_authentication_required){confirm_password_path}
|
15
16
|
|
16
17
|
session_key :confirm_password_redirect_session_key, :confirm_password_redirect
|
@@ -37,8 +38,7 @@ module Rodauth
|
|
37
38
|
confirm_password
|
38
39
|
after_confirm_password
|
39
40
|
end
|
40
|
-
|
41
|
-
redirect confirm_password_redirect
|
41
|
+
confirm_password_response
|
42
42
|
else
|
43
43
|
set_response_error_reason_status(:invalid_password, invalid_password_error_status)
|
44
44
|
set_field_error(password_param, invalid_password_message)
|
@@ -13,6 +13,7 @@ module Rodauth
|
|
13
13
|
button 'Create Account'
|
14
14
|
additional_form_tags
|
15
15
|
redirect
|
16
|
+
response
|
16
17
|
|
17
18
|
auth_value_method :create_account_autologin?, true
|
18
19
|
translatable_method :create_account_link_text, "Create a New Account"
|
@@ -79,8 +80,7 @@ module Rodauth
|
|
79
80
|
if create_account_autologin?
|
80
81
|
autologin_session('create_account')
|
81
82
|
end
|
82
|
-
|
83
|
-
redirect create_account_redirect
|
83
|
+
create_account_response
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
@@ -19,6 +19,7 @@ module Rodauth
|
|
19
19
|
button 'Send Login Link Via Email', 'email_auth_request'
|
20
20
|
redirect(:email_auth_email_sent){default_post_email_redirect}
|
21
21
|
redirect(:email_auth_email_recently_sent){default_post_email_redirect}
|
22
|
+
response :email_auth_email_sent
|
22
23
|
email :email_auth, 'Login Link'
|
23
24
|
|
24
25
|
auth_value_method :email_auth_deadline_column, :deadline
|
@@ -57,12 +58,11 @@ module Rodauth
|
|
57
58
|
r.post do
|
58
59
|
if account_from_login(param(login_param)) && open_account?
|
59
60
|
_email_auth_request
|
60
|
-
else
|
61
|
-
set_redirect_error_status(no_matching_login_error_status)
|
62
|
-
set_error_reason :no_matching_login
|
63
|
-
set_redirect_error_flash email_auth_request_error_flash
|
64
61
|
end
|
65
62
|
|
63
|
+
set_redirect_error_status(no_matching_login_error_status)
|
64
|
+
set_error_reason :no_matching_login
|
65
|
+
set_redirect_error_flash email_auth_request_error_flash
|
66
66
|
redirect email_auth_email_sent_redirect
|
67
67
|
end
|
68
68
|
end
|
@@ -150,7 +150,7 @@ module Rodauth
|
|
150
150
|
|
151
151
|
def after_login_entered_during_multi_phase_login
|
152
152
|
# If forcing email auth, just send the email link.
|
153
|
-
|
153
|
+
_email_auth_request if force_email_auth?
|
154
154
|
|
155
155
|
super
|
156
156
|
end
|
@@ -169,7 +169,7 @@ module Rodauth
|
|
169
169
|
|
170
170
|
def _multi_phase_login_forms
|
171
171
|
forms = super
|
172
|
-
forms << [30, email_auth_request_form, :
|
172
|
+
forms << [30, email_auth_request_form, :_email_auth_request] if valid_login_entered? && allow_email_auth?
|
173
173
|
forms
|
174
174
|
end
|
175
175
|
|
@@ -177,11 +177,6 @@ module Rodauth
|
|
177
177
|
(email_last_sent = get_email_auth_email_last_sent) && (Time.now - email_last_sent < email_auth_skip_resend_email_within)
|
178
178
|
end
|
179
179
|
|
180
|
-
def _email_auth_request_and_redirect
|
181
|
-
_email_auth_request
|
182
|
-
redirect email_auth_email_sent_redirect
|
183
|
-
end
|
184
|
-
|
185
180
|
def _email_auth_request
|
186
181
|
if email_auth_email_recently_sent?
|
187
182
|
set_redirect_error_flash email_auth_email_recently_sent_error_flash
|
@@ -196,7 +191,7 @@ module Rodauth
|
|
196
191
|
after_email_auth_request
|
197
192
|
end
|
198
193
|
|
199
|
-
|
194
|
+
email_auth_email_sent_response
|
200
195
|
end
|
201
196
|
|
202
197
|
attr_reader :email_auth_key_value
|
@@ -69,12 +69,10 @@ module Rodauth
|
|
69
69
|
|
70
70
|
return unless actual = yield(id)
|
71
71
|
|
72
|
-
unless timing_safe_eql?(key, convert_email_token_key(actual))
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
return
|
77
|
-
end
|
72
|
+
unless (hmac_secret && timing_safe_eql?(key, convert_email_token_key(actual))) ||
|
73
|
+
(hmac_secret_rotation? && timing_safe_eql?(key, compute_old_hmac(actual))) ||
|
74
|
+
((!hmac_secret || allow_raw_email_token?) && timing_safe_eql?(key, actual))
|
75
|
+
return
|
78
76
|
end
|
79
77
|
ds = account_ds(id)
|
80
78
|
ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
|
@@ -50,6 +50,10 @@ module Rodauth
|
|
50
50
|
@params[k]
|
51
51
|
end
|
52
52
|
|
53
|
+
def clear_session
|
54
|
+
@session.clear
|
55
|
+
end
|
56
|
+
|
53
57
|
def set_error_flash(message)
|
54
58
|
@flash = message
|
55
59
|
_handle_internal_request_error
|
@@ -81,6 +85,24 @@ module Rodauth
|
|
81
85
|
_return_from_internal_request(recovery_codes)
|
82
86
|
end
|
83
87
|
|
88
|
+
def webauthn_setup_view
|
89
|
+
cred = new_webauthn_credential
|
90
|
+
_return_from_internal_request({
|
91
|
+
webauthn_setup: cred.as_json,
|
92
|
+
webauthn_setup_challenge: cred.challenge,
|
93
|
+
webauthn_setup_challenge_hmac: compute_hmac(cred.challenge)
|
94
|
+
})
|
95
|
+
end
|
96
|
+
|
97
|
+
def webauthn_auth_view
|
98
|
+
cred = webauthn_credential_options_for_get
|
99
|
+
_return_from_internal_request({
|
100
|
+
webauthn_auth: cred.as_json,
|
101
|
+
webauthn_auth_challenge: cred.challenge,
|
102
|
+
webauthn_auth_challenge_hmac: compute_hmac(cred.challenge)
|
103
|
+
})
|
104
|
+
end
|
105
|
+
|
84
106
|
def handle_internal_request(meth)
|
85
107
|
catch(:halt) do
|
86
108
|
_around_rodauth do
|
@@ -153,6 +175,11 @@ module Rodauth
|
|
153
175
|
_set_login_param_from_account
|
154
176
|
end
|
155
177
|
|
178
|
+
def before_webauthn_login_route
|
179
|
+
super
|
180
|
+
_set_login_param_from_account
|
181
|
+
end
|
182
|
+
|
156
183
|
def account_from_key(token, status_id=nil)
|
157
184
|
return super unless session_value
|
158
185
|
return unless yield session_value
|
@@ -232,6 +259,25 @@ module Rodauth
|
|
232
259
|
_handle_otp_setup(request)
|
233
260
|
end
|
234
261
|
|
262
|
+
def _handle_webauthn_setup_params(request)
|
263
|
+
request.env['REQUEST_METHOD'] = 'GET'
|
264
|
+
_handle_webauthn_setup(request)
|
265
|
+
end
|
266
|
+
|
267
|
+
def _handle_webauthn_auth_params(request)
|
268
|
+
request.env['REQUEST_METHOD'] = 'GET'
|
269
|
+
_handle_webauthn_auth(request)
|
270
|
+
end
|
271
|
+
|
272
|
+
def _handle_webauthn_login_params(request)
|
273
|
+
_set_login_param_from_account
|
274
|
+
unless webauthn_login_options?
|
275
|
+
raise InternalRequestError, "no login provided" unless param_or_nil(login_param)
|
276
|
+
raise InternalRequestError, "no account for login"
|
277
|
+
end
|
278
|
+
webauthn_auth_view
|
279
|
+
end
|
280
|
+
|
235
281
|
def _predicate_internal_request(meth, request)
|
236
282
|
_return_false_on_error!
|
237
283
|
_set_internal_request_return_value(true)
|
@@ -302,7 +348,7 @@ module Rodauth
|
|
302
348
|
session[rodauth.session_key] = account_id
|
303
349
|
unless authenticated_by = opts.delete(:authenticated_by)
|
304
350
|
authenticated_by = case route
|
305
|
-
when :otp_auth, :sms_request, :sms_auth, :recovery_auth, :valid_otp_auth?, :valid_sms_auth?, :valid_recovery_auth?
|
351
|
+
when :otp_auth, :sms_request, :sms_auth, :recovery_auth, :webauthn_auth, :webauthn_auth_params, :valid_otp_auth?, :valid_sms_auth?, :valid_recovery_auth?
|
306
352
|
['internal1']
|
307
353
|
else
|
308
354
|
['internal1', 'internal2']
|
@@ -22,6 +22,7 @@ module Rodauth
|
|
22
22
|
|
23
23
|
auth_methods(
|
24
24
|
:json_request?,
|
25
|
+
:json_response_error?
|
25
26
|
)
|
26
27
|
|
27
28
|
auth_private_methods :json_response_body
|
@@ -65,6 +66,10 @@ module Rodauth
|
|
65
66
|
return_json_response
|
66
67
|
end
|
67
68
|
|
69
|
+
def json_response_error?
|
70
|
+
!!json_response[json_response_error_key]
|
71
|
+
end
|
72
|
+
|
68
73
|
private
|
69
74
|
|
70
75
|
def before_two_factor_manage_route
|
@@ -172,7 +177,7 @@ module Rodauth
|
|
172
177
|
end
|
173
178
|
|
174
179
|
def _return_json_response
|
175
|
-
response.status ||= json_response_error_status if
|
180
|
+
response.status ||= json_response_error_status if json_response_error?
|
176
181
|
response['Content-Type'] ||= json_response_content_type
|
177
182
|
return_response _json_response_body(json_response)
|
178
183
|
end
|
data/lib/rodauth/features/jwt.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
3
|
require 'jwt'
|
4
|
+
require 'jwt/version'
|
4
5
|
|
5
6
|
module Rodauth
|
6
7
|
Feature.define(:jwt, :Jwt) do
|
@@ -11,6 +12,7 @@ module Rodauth
|
|
11
12
|
auth_value_method :jwt_authorization_ignore, /\A(?:Basic|Digest) /
|
12
13
|
auth_value_method :jwt_authorization_remove, /\ABearer:?\s+/
|
13
14
|
auth_value_method :jwt_decode_opts, {}.freeze
|
15
|
+
auth_value_method :jwt_old_secret, nil
|
14
16
|
auth_value_method :jwt_session_key, nil
|
15
17
|
auth_value_method :jwt_symbolize_deeply?, false
|
16
18
|
|
@@ -111,9 +113,23 @@ module Rodauth
|
|
111
113
|
jwt_decode_opts
|
112
114
|
end
|
113
115
|
|
116
|
+
if JWT::VERSION::MAJOR > 2 || (JWT::VERSION::MAJOR == 2 && JWT::VERSION::MINOR >= 4)
|
117
|
+
def _jwt_decode_secrets
|
118
|
+
secrets = [jwt_secret, jwt_old_secret]
|
119
|
+
secrets.compact!
|
120
|
+
secrets
|
121
|
+
end
|
122
|
+
# :nocov:
|
123
|
+
else
|
124
|
+
def _jwt_decode_secrets
|
125
|
+
jwt_secret
|
126
|
+
end
|
127
|
+
# :nocov:
|
128
|
+
end
|
129
|
+
|
114
130
|
def jwt_payload
|
115
131
|
return @jwt_payload if defined?(@jwt_payload)
|
116
|
-
@jwt_payload = JWT.decode(jwt_token,
|
132
|
+
@jwt_payload = JWT.decode(jwt_token, _jwt_decode_secrets, true, _jwt_decode_opts.merge(:algorithm=>jwt_algorithm))[0]
|
117
133
|
rescue JWT::DecodeError => e
|
118
134
|
rescue_jwt_payload(e)
|
119
135
|
end
|
@@ -114,7 +114,7 @@ module Rodauth
|
|
114
114
|
unless key &&
|
115
115
|
(id.to_s == session_value.to_s) &&
|
116
116
|
(actual = get_active_refresh_token(id, token_id)) &&
|
117
|
-
timing_safe_eql?(key, convert_token_key(actual)) &&
|
117
|
+
(timing_safe_eql?(key, convert_token_key(actual)) || (hmac_secret_rotation? && timing_safe_eql?(key, compute_old_hmac(actual)))) &&
|
118
118
|
jwt_refresh_token_match?(key)
|
119
119
|
return
|
120
120
|
end
|
@@ -150,7 +150,9 @@ module Rodauth
|
|
150
150
|
|
151
151
|
# If allowing with expired jwt access token, check the expired session contains
|
152
152
|
# hmac matching submitted and active refresh token.
|
153
|
-
|
153
|
+
s = session[jwt_refresh_token_hmac_session_key].to_s
|
154
|
+
h = session[jwt_refresh_token_data_session_key].to_s + key
|
155
|
+
timing_safe_eql?(compute_hmac(h), s) || (hmac_secret_rotation? && timing_safe_eql?(compute_old_hmac(h), s))
|
154
156
|
end
|
155
157
|
|
156
158
|
def get_active_refresh_token(account_id, token_id)
|
@@ -23,10 +23,12 @@ module Rodauth
|
|
23
23
|
notice_flash "Your account has been unlocked", 'unlock_account'
|
24
24
|
notice_flash "An email has been sent to you with a link to unlock your account", 'unlock_account_request'
|
25
25
|
redirect :unlock_account
|
26
|
+
response :unlock_account
|
27
|
+
response :unlock_account_request
|
26
28
|
redirect(:unlock_account_request){default_post_email_redirect}
|
27
29
|
redirect(:unlock_account_email_recently_sent){default_post_email_redirect}
|
28
30
|
email :unlock_account, 'Unlock Account'
|
29
|
-
|
31
|
+
|
30
32
|
auth_value_method :unlock_account_autologin?, true
|
31
33
|
auth_value_method :max_invalid_logins, 100
|
32
34
|
auth_value_method :account_login_failures_table, :account_login_failures
|
@@ -82,14 +84,13 @@ module Rodauth
|
|
82
84
|
after_unlock_account_request
|
83
85
|
end
|
84
86
|
|
85
|
-
|
87
|
+
unlock_account_request_response
|
86
88
|
else
|
87
89
|
set_redirect_error_status(no_matching_login_error_status)
|
88
90
|
set_error_reason :no_matching_login
|
89
91
|
set_redirect_error_flash no_matching_login_message.to_s.capitalize
|
92
|
+
redirect unlock_account_request_redirect
|
90
93
|
end
|
91
|
-
|
92
|
-
redirect unlock_account_request_redirect
|
93
94
|
end
|
94
95
|
end
|
95
96
|
|
@@ -134,8 +135,7 @@ module Rodauth
|
|
134
135
|
end
|
135
136
|
|
136
137
|
remove_session_value(unlock_account_session_key)
|
137
|
-
|
138
|
-
redirect unlock_account_redirect
|
138
|
+
unlock_account_response
|
139
139
|
else
|
140
140
|
set_response_error_reason_status(:invalid_password, invalid_password_error_status)
|
141
141
|
set_field_error(password_param, invalid_password_message)
|
@@ -24,7 +24,10 @@ module Rodauth
|
|
24
24
|
|
25
25
|
auth_value_methods :login_return_to_requested_location_path
|
26
26
|
|
27
|
-
auth_private_methods
|
27
|
+
auth_private_methods(
|
28
|
+
:login_form_footer_links,
|
29
|
+
:login_response
|
30
|
+
)
|
28
31
|
|
29
32
|
internal_request_method
|
30
33
|
internal_request_method :valid_login_and_password?
|
@@ -77,17 +80,18 @@ module Rodauth
|
|
77
80
|
end
|
78
81
|
|
79
82
|
attr_reader :login_form_header
|
83
|
+
attr_reader :saved_login_redirect
|
84
|
+
private :saved_login_redirect
|
80
85
|
|
81
86
|
def login(auth_type)
|
82
|
-
saved_login_redirect = remove_session_value(login_redirect_session_key)
|
87
|
+
@saved_login_redirect = remove_session_value(login_redirect_session_key)
|
83
88
|
transaction do
|
84
89
|
before_login
|
85
90
|
login_session(auth_type)
|
86
91
|
yield if block_given?
|
87
92
|
after_login
|
88
93
|
end
|
89
|
-
|
90
|
-
redirect(saved_login_redirect || login_redirect)
|
94
|
+
require_response(:_login_response)
|
91
95
|
end
|
92
96
|
|
93
97
|
def login_required
|
@@ -136,6 +140,11 @@ module Rodauth
|
|
136
140
|
|
137
141
|
private
|
138
142
|
|
143
|
+
def _login_response
|
144
|
+
set_notice_flash login_notice_flash
|
145
|
+
redirect(saved_login_redirect || login_redirect)
|
146
|
+
end
|
147
|
+
|
139
148
|
def _login_form_footer_links
|
140
149
|
[]
|
141
150
|
end
|