rodauth 2.30.0 → 2.32.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/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
@@ -10,6 +10,7 @@ module Rodauth
|
|
10
10
|
after
|
11
11
|
button 'Logout'
|
12
12
|
redirect{require_login_redirect}
|
13
|
+
response
|
13
14
|
|
14
15
|
auth_methods :logout
|
15
16
|
|
@@ -26,8 +27,7 @@ module Rodauth
|
|
26
27
|
logout
|
27
28
|
after_logout
|
28
29
|
end
|
29
|
-
|
30
|
-
redirect logout_redirect
|
30
|
+
logout_response
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
data/lib/rodauth/features/otp.rb
CHANGED
@@ -35,6 +35,8 @@ module Rodauth
|
|
35
35
|
redirect :otp_disable
|
36
36
|
redirect :otp_already_setup
|
37
37
|
redirect :otp_setup
|
38
|
+
response :otp_disable
|
39
|
+
response :otp_setup
|
38
40
|
redirect(:otp_lockout){two_factor_auth_required_redirect}
|
39
41
|
|
40
42
|
loaded_templates %w'otp-disable otp-auth otp-setup otp-auth-code-field password-field'
|
@@ -94,7 +96,8 @@ module Rodauth
|
|
94
96
|
|
95
97
|
auth_private_methods(
|
96
98
|
:otp_add_key,
|
97
|
-
:otp_tmp_key
|
99
|
+
:otp_tmp_key,
|
100
|
+
:otp_valid_code_for_old_secret
|
98
101
|
)
|
99
102
|
|
100
103
|
internal_request_method :otp_setup_params
|
@@ -181,8 +184,7 @@ module Rodauth
|
|
181
184
|
end
|
182
185
|
after_otp_setup
|
183
186
|
end
|
184
|
-
|
185
|
-
redirect otp_setup_redirect
|
187
|
+
otp_setup_response
|
186
188
|
end
|
187
189
|
|
188
190
|
set_error_flash otp_setup_error_flash
|
@@ -209,8 +211,7 @@ module Rodauth
|
|
209
211
|
end
|
210
212
|
after_otp_disable
|
211
213
|
end
|
212
|
-
|
213
|
-
redirect otp_disable_redirect
|
214
|
+
otp_disable_response
|
214
215
|
end
|
215
216
|
|
216
217
|
set_response_error_reason_status(:invalid_password, invalid_password_error_status)
|
@@ -246,8 +247,19 @@ module Rodauth
|
|
246
247
|
def otp_exists?
|
247
248
|
!otp_key.nil?
|
248
249
|
end
|
249
|
-
|
250
|
+
|
250
251
|
def otp_valid_code?(ot_pass)
|
252
|
+
if _otp_valid_code?(ot_pass, otp)
|
253
|
+
true
|
254
|
+
elsif hmac_secret_rotation? && _otp_valid_code?(ot_pass, _otp_for_key(otp_hmac_old_secret(otp_key)))
|
255
|
+
_otp_valid_code_for_old_secret
|
256
|
+
true
|
257
|
+
else
|
258
|
+
false
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def _otp_valid_code?(ot_pass, otp)
|
251
263
|
return false unless otp_exists?
|
252
264
|
ot_pass = ot_pass.gsub(/\s+/, '')
|
253
265
|
if drift = otp_drift
|
@@ -368,9 +380,17 @@ module Rodauth
|
|
368
380
|
base32_encode(compute_raw_hmac(ROTP::Base32.decode(key)), key.bytesize)
|
369
381
|
end
|
370
382
|
|
383
|
+
def otp_hmac_old_secret(key)
|
384
|
+
base32_encode(compute_raw_hmac_with_secret(ROTP::Base32.decode(key), hmac_old_secret), key.bytesize)
|
385
|
+
end
|
386
|
+
|
371
387
|
def otp_valid_key?(secret)
|
372
388
|
return false unless secret =~ /\A([a-z2-7]{16}|[a-z2-7]{32})\z/
|
373
389
|
if otp_keys_use_hmac?
|
390
|
+
# Purposely do not allow creating new OTPs with old secrets,
|
391
|
+
# since OTP rotation is difficult. The user will get shown
|
392
|
+
# the same page with an updated secret, which they can submit
|
393
|
+
# to setup OTP.
|
374
394
|
timing_safe_eql?(otp_hmac_secret(param(otp_setup_raw_param)), secret)
|
375
395
|
else
|
376
396
|
true
|
@@ -400,6 +420,10 @@ module Rodauth
|
|
400
420
|
@otp_key = secret
|
401
421
|
end
|
402
422
|
|
423
|
+
# Called for valid OTP codes for old secrets
|
424
|
+
def _otp_valid_code_for_old_secret
|
425
|
+
end
|
426
|
+
|
403
427
|
def _otp_add_key(secret)
|
404
428
|
# Uniqueness errors can't be handled here, as we can't be sure the secret provided
|
405
429
|
# is the same as the current secret.
|
@@ -411,8 +435,12 @@ module Rodauth
|
|
411
435
|
otp_key_ds.get(otp_keys_column)
|
412
436
|
end
|
413
437
|
|
438
|
+
def _otp_for_key(key)
|
439
|
+
otp_class.new(key, :issuer=>otp_issuer, :digits=>otp_digits, :interval=>otp_interval)
|
440
|
+
end
|
441
|
+
|
414
442
|
def _otp
|
415
|
-
|
443
|
+
_otp_for_key(otp_user_key)
|
416
444
|
end
|
417
445
|
|
418
446
|
def otp_key_ds
|
@@ -13,6 +13,7 @@ module Rodauth
|
|
13
13
|
after
|
14
14
|
after 'load_memory'
|
15
15
|
redirect
|
16
|
+
response
|
16
17
|
|
17
18
|
auth_value_method :raw_remember_token_deadline, nil
|
18
19
|
auth_value_method :remember_cookie_options, {}.freeze
|
@@ -71,15 +72,14 @@ module Rodauth
|
|
71
72
|
when remember_remember_param_value
|
72
73
|
remember_login
|
73
74
|
when remember_forget_param_value
|
74
|
-
forget_login
|
75
|
+
forget_login
|
75
76
|
when remember_disable_param_value
|
76
|
-
disable_remember_login
|
77
|
+
disable_remember_login
|
77
78
|
end
|
78
79
|
after_remember
|
79
80
|
end
|
80
81
|
|
81
|
-
|
82
|
-
redirect remember_redirect
|
82
|
+
remember_response
|
83
83
|
else
|
84
84
|
set_response_error_reason_status(:invalid_remember_param, invalid_field_error_status)
|
85
85
|
set_error_flash remember_error_flash
|
@@ -96,11 +96,11 @@ module Rodauth
|
|
96
96
|
actual, deadline = active_remember_key_ds(id).get([remember_key_column, remember_deadline_column])
|
97
97
|
return unless actual
|
98
98
|
|
99
|
-
if hmac_secret
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
99
|
+
if hmac_secret && !(valid = timing_safe_eql?(key, compute_hmac(actual)))
|
100
|
+
if hmac_secret_rotation? && (valid = timing_safe_eql?(key, compute_old_hmac(actual)))
|
101
|
+
_set_remember_cookie(id, actual, deadline)
|
102
|
+
elsif !(raw_remember_token_deadline && raw_remember_token_deadline > convert_timestamp(deadline))
|
103
|
+
return
|
104
104
|
end
|
105
105
|
end
|
106
106
|
|
@@ -177,16 +177,20 @@ module Rodauth
|
|
177
177
|
|
178
178
|
private
|
179
179
|
|
180
|
-
def
|
180
|
+
def _set_remember_cookie(account_id, remember_key_value, deadline)
|
181
181
|
opts = Hash[remember_cookie_options]
|
182
182
|
opts[:value] = "#{account_id}_#{convert_token_key(remember_key_value)}"
|
183
|
-
opts[:expires] = convert_timestamp(
|
183
|
+
opts[:expires] = convert_timestamp(deadline)
|
184
184
|
opts[:path] = "/" unless opts.key?(:path)
|
185
185
|
opts[:httponly] = true unless opts.key?(:httponly) || opts.key?(:http_only)
|
186
186
|
opts[:secure] = true unless opts.key?(:secure) || !request.ssl?
|
187
187
|
::Rack::Utils.set_cookie_header!(response.headers, remember_cookie_key, opts)
|
188
188
|
end
|
189
189
|
|
190
|
+
def set_remember_cookie
|
191
|
+
_set_remember_cookie(account_id, remember_key_value, active_remember_key_ds.get(remember_deadline_column))
|
192
|
+
end
|
193
|
+
|
190
194
|
def extend_remember_deadline_while_logged_in?
|
191
195
|
return false unless extend_remember_deadline?
|
192
196
|
|
@@ -24,8 +24,10 @@ module Rodauth
|
|
24
24
|
redirect
|
25
25
|
redirect(:reset_password_email_sent){default_post_email_redirect}
|
26
26
|
redirect(:reset_password_email_recently_sent){default_post_email_redirect}
|
27
|
+
response
|
28
|
+
response :reset_password_email_sent
|
27
29
|
email :reset_password, 'Reset Password'
|
28
|
-
|
30
|
+
|
29
31
|
auth_value_method :reset_password_deadline_column, :deadline
|
30
32
|
auth_value_method :reset_password_deadline_interval, {:days=>1}.freeze
|
31
33
|
auth_value_method :reset_password_key_param, 'key'
|
@@ -88,8 +90,7 @@ module Rodauth
|
|
88
90
|
after_reset_password_request
|
89
91
|
end
|
90
92
|
|
91
|
-
|
92
|
-
redirect reset_password_email_sent_redirect
|
93
|
+
reset_password_email_sent_response
|
93
94
|
end
|
94
95
|
|
95
96
|
set_error_flash reset_password_request_error_flash
|
@@ -154,8 +155,7 @@ module Rodauth
|
|
154
155
|
end
|
155
156
|
|
156
157
|
remove_session_value(reset_password_session_key)
|
157
|
-
|
158
|
-
redirect reset_password_redirect
|
158
|
+
reset_password_response
|
159
159
|
end
|
160
160
|
|
161
161
|
set_error_flash reset_password_error_flash
|
@@ -37,9 +37,10 @@ module Rodauth
|
|
37
37
|
end
|
38
38
|
true
|
39
39
|
elsif current_key
|
40
|
-
if hmac_secret
|
41
|
-
valid = timing_safe_eql?(single_session_key,
|
42
|
-
|
40
|
+
if hmac_secret && !(valid = timing_safe_eql?(single_session_key, hmac = compute_hmac(current_key)))
|
41
|
+
if hmac_secret_rotation? && (valid = timing_safe_eql?(single_session_key, compute_old_hmac(current_key)))
|
42
|
+
session[single_session_session_key] = hmac
|
43
|
+
elsif !allow_raw_single_session_key?
|
43
44
|
return false
|
44
45
|
end
|
45
46
|
end
|
@@ -55,6 +55,10 @@ module Rodauth
|
|
55
55
|
redirect(:sms_request){sms_request_path}
|
56
56
|
redirect(:sms_lockout){two_factor_auth_required_redirect}
|
57
57
|
|
58
|
+
response :sms_confirm
|
59
|
+
response :sms_disable
|
60
|
+
response :sms_needs_confirmation
|
61
|
+
|
58
62
|
loaded_templates %w'sms-auth sms-confirm sms-disable sms-request sms-setup sms-code-field password-field'
|
59
63
|
view 'sms-auth', 'Authenticate via SMS Code', 'sms_auth'
|
60
64
|
view 'sms-confirm', 'Confirm SMS Backup Number', 'sms_confirm'
|
@@ -86,7 +90,11 @@ module Rodauth
|
|
86
90
|
|
87
91
|
auth_cached_method :sms
|
88
92
|
|
89
|
-
auth_value_methods
|
93
|
+
auth_value_methods(
|
94
|
+
:sms_codes_primary?,
|
95
|
+
:sms_needs_confirmation_notice_flash,
|
96
|
+
:sms_request_response
|
97
|
+
)
|
90
98
|
|
91
99
|
auth_methods(
|
92
100
|
:sms_auth_message,
|
@@ -136,9 +144,8 @@ module Rodauth
|
|
136
144
|
sms_send_auth_code
|
137
145
|
after_sms_request
|
138
146
|
end
|
139
|
-
|
140
|
-
|
141
|
-
redirect sms_auth_redirect
|
147
|
+
|
148
|
+
require_response(:_sms_request_response)
|
142
149
|
end
|
143
150
|
end
|
144
151
|
|
@@ -223,8 +230,7 @@ module Rodauth
|
|
223
230
|
after_sms_setup
|
224
231
|
end
|
225
232
|
|
226
|
-
|
227
|
-
redirect sms_needs_confirmation_redirect
|
233
|
+
sms_needs_confirmation_response
|
228
234
|
end
|
229
235
|
|
230
236
|
set_error_flash sms_setup_error_flash
|
@@ -256,8 +262,7 @@ module Rodauth
|
|
256
262
|
end
|
257
263
|
end
|
258
264
|
|
259
|
-
|
260
|
-
redirect sms_confirm_redirect
|
265
|
+
sms_confirm_response
|
261
266
|
end
|
262
267
|
|
263
268
|
sms_confirm_failure
|
@@ -287,8 +292,7 @@ module Rodauth
|
|
287
292
|
end
|
288
293
|
after_sms_disable
|
289
294
|
end
|
290
|
-
|
291
|
-
redirect sms_disable_redirect
|
295
|
+
sms_disable_response
|
292
296
|
end
|
293
297
|
|
294
298
|
set_response_error_reason_status(:invalid_password, invalid_password_error_status)
|
@@ -395,6 +399,10 @@ module Rodauth
|
|
395
399
|
"SMS confirmation code for #{domain} is #{code}"
|
396
400
|
end
|
397
401
|
|
402
|
+
def sms_needs_confirmation_notice_flash
|
403
|
+
sms_needs_confirmation_error_flash
|
404
|
+
end
|
405
|
+
|
398
406
|
def sms_set_code(code)
|
399
407
|
update_sms(sms_code_column=>code, sms_issued_at_column=>Sequel::CURRENT_TIMESTAMP)
|
400
408
|
end
|
@@ -449,6 +457,11 @@ module Rodauth
|
|
449
457
|
|
450
458
|
private
|
451
459
|
|
460
|
+
def _sms_request_response
|
461
|
+
set_notice_flash sms_request_notice_flash
|
462
|
+
redirect sms_auth_redirect
|
463
|
+
end
|
464
|
+
|
452
465
|
def _two_factor_auth_links
|
453
466
|
links = super
|
454
467
|
links << [30, sms_request_path, sms_auth_link_text] if sms_available?
|
@@ -23,6 +23,8 @@ module Rodauth
|
|
23
23
|
redirect(:two_factor_need_setup){two_factor_manage_path}
|
24
24
|
redirect(:two_factor_auth_required){two_factor_auth_path}
|
25
25
|
|
26
|
+
response :two_factor_disable
|
27
|
+
|
26
28
|
notice_flash "You have been multifactor authenticated", "two_factor_auth"
|
27
29
|
notice_flash "All multifactor authentication methods have been disabled", "two_factor_disable"
|
28
30
|
|
@@ -55,6 +57,7 @@ module Rodauth
|
|
55
57
|
|
56
58
|
auth_private_methods(
|
57
59
|
:two_factor_auth_links,
|
60
|
+
:two_factor_auth_response,
|
58
61
|
:two_factor_setup_links,
|
59
62
|
:two_factor_remove_links
|
60
63
|
)
|
@@ -106,8 +109,7 @@ module Rodauth
|
|
106
109
|
_two_factor_remove_all_from_session
|
107
110
|
after_two_factor_disable
|
108
111
|
end
|
109
|
-
|
110
|
-
redirect two_factor_disable_redirect
|
112
|
+
two_factor_disable_response
|
111
113
|
end
|
112
114
|
|
113
115
|
set_response_error_reason_status(:invalid_password, invalid_password_error_status)
|
@@ -247,13 +249,13 @@ module Rodauth
|
|
247
249
|
two_factor_update_session(type)
|
248
250
|
two_factor_remove_auth_failures
|
249
251
|
after_two_factor_authentication
|
250
|
-
|
251
|
-
redirect_two_factor_authenticated
|
252
|
+
require_response(:_two_factor_auth_response)
|
252
253
|
end
|
253
254
|
|
254
|
-
def
|
255
|
+
def _two_factor_auth_response
|
255
256
|
saved_two_factor_auth_redirect = remove_session_value(two_factor_auth_redirect_session_key)
|
256
|
-
|
257
|
+
set_notice_flash two_factor_auth_notice_flash
|
258
|
+
redirect(saved_two_factor_auth_redirect || two_factor_auth_redirect)
|
257
259
|
end
|
258
260
|
|
259
261
|
def two_factor_remove_session(type)
|
@@ -6,6 +6,7 @@ module Rodauth
|
|
6
6
|
|
7
7
|
def password_match?(password)
|
8
8
|
if (result = super) && update_password_hash?
|
9
|
+
@update_password_hash = false
|
9
10
|
set_password(password)
|
10
11
|
end
|
11
12
|
|
@@ -15,7 +16,7 @@ module Rodauth
|
|
15
16
|
private
|
16
17
|
|
17
18
|
def update_password_hash?
|
18
|
-
password_hash_cost != @current_password_hash_cost
|
19
|
+
password_hash_cost != @current_password_hash_cost || @update_password_hash
|
19
20
|
end
|
20
21
|
|
21
22
|
def get_password_hash
|
@@ -24,6 +24,8 @@ module Rodauth
|
|
24
24
|
button 'Verify Account'
|
25
25
|
button 'Send Verification Email Again', 'verify_account_resend'
|
26
26
|
redirect
|
27
|
+
response
|
28
|
+
response :verify_account_email_sent
|
27
29
|
redirect(:verify_account_email_sent){default_post_email_redirect}
|
28
30
|
redirect(:verify_account_email_recently_sent){default_post_email_redirect}
|
29
31
|
email :verify_account, 'Verify Account'
|
@@ -69,7 +71,6 @@ module Rodauth
|
|
69
71
|
end
|
70
72
|
|
71
73
|
r.post do
|
72
|
-
verified = false
|
73
74
|
if account_from_login(param(login_param)) && allow_resending_verify_account_email?
|
74
75
|
if verify_account_email_recently_sent?
|
75
76
|
set_redirect_error_flash verify_account_email_recently_sent_error_flash
|
@@ -79,18 +80,13 @@ module Rodauth
|
|
79
80
|
before_verify_account_email_resend
|
80
81
|
if verify_account_email_resend
|
81
82
|
after_verify_account_email_resend
|
82
|
-
|
83
|
+
verify_account_email_sent_response
|
83
84
|
end
|
84
85
|
end
|
85
86
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
set_redirect_error_status(no_matching_login_error_status)
|
90
|
-
set_error_reason :no_matching_login
|
91
|
-
set_redirect_error_flash verify_account_resend_error_flash
|
92
|
-
end
|
93
|
-
|
87
|
+
set_redirect_error_status(no_matching_login_error_status)
|
88
|
+
set_error_reason :no_matching_login
|
89
|
+
set_redirect_error_flash verify_account_resend_error_flash
|
94
90
|
redirect verify_account_email_sent_redirect
|
95
91
|
end
|
96
92
|
end
|
@@ -154,8 +150,7 @@ module Rodauth
|
|
154
150
|
end
|
155
151
|
|
156
152
|
remove_session_value(verify_account_session_key)
|
157
|
-
|
158
|
-
redirect verify_account_redirect
|
153
|
+
verify_account_response
|
159
154
|
end
|
160
155
|
|
161
156
|
set_error_flash verify_account_error_flash
|
@@ -18,6 +18,7 @@ module Rodauth
|
|
18
18
|
before 'verify_login_change_email'
|
19
19
|
button 'Verify Login Change'
|
20
20
|
redirect
|
21
|
+
response
|
21
22
|
redirect(:verify_login_change_duplicate_account){require_login_redirect}
|
22
23
|
|
23
24
|
auth_value_method :verify_login_change_autologin?, false
|
@@ -98,8 +99,7 @@ module Rodauth
|
|
98
99
|
end
|
99
100
|
|
100
101
|
remove_session_value(verify_login_change_session_key)
|
101
|
-
|
102
|
-
redirect verify_login_change_redirect
|
102
|
+
verify_login_change_response
|
103
103
|
end
|
104
104
|
end
|
105
105
|
|
@@ -30,6 +30,8 @@ module Rodauth
|
|
30
30
|
|
31
31
|
redirect :webauthn_setup
|
32
32
|
redirect :webauthn_remove
|
33
|
+
response :webauthn_setup
|
34
|
+
response :webauthn_remove
|
33
35
|
|
34
36
|
notice_flash "WebAuthn authentication is now setup", 'webauthn_setup'
|
35
37
|
notice_flash "WebAuthn authenticator has been removed", 'webauthn_remove'
|
@@ -112,6 +114,12 @@ module Rodauth
|
|
112
114
|
|
113
115
|
def_deprecated_alias :webauthn_credential_options_for_get, :webauth_credential_options_for_get
|
114
116
|
|
117
|
+
internal_request_method :webauthn_setup_params
|
118
|
+
internal_request_method :webauthn_setup
|
119
|
+
internal_request_method :webauthn_auth_params
|
120
|
+
internal_request_method :webauthn_auth
|
121
|
+
internal_request_method :webauthn_remove
|
122
|
+
|
115
123
|
route(:webauthn_auth_js) do |r|
|
116
124
|
before_webauthn_auth_js_route
|
117
125
|
r.get do
|
@@ -188,8 +196,7 @@ module Rodauth
|
|
188
196
|
throw_error_reason(:duplicate_webauthn_id, invalid_field_error_status, webauthn_setup_param, webauthn_duplicate_webauthn_id_message)
|
189
197
|
end
|
190
198
|
|
191
|
-
|
192
|
-
redirect webauthn_setup_redirect
|
199
|
+
webauthn_setup_response
|
193
200
|
end
|
194
201
|
|
195
202
|
set_error_flash webauthn_setup_error_flash
|
@@ -229,8 +236,7 @@ module Rodauth
|
|
229
236
|
after_webauthn_remove
|
230
237
|
end
|
231
238
|
|
232
|
-
|
233
|
-
redirect webauthn_remove_redirect
|
239
|
+
webauthn_remove_response
|
234
240
|
end
|
235
241
|
|
236
242
|
set_error_flash webauthn_remove_error_flash
|
@@ -314,7 +320,7 @@ module Rodauth
|
|
314
320
|
|
315
321
|
(challenge = param_or_nil(webauthn_setup_challenge_param)) &&
|
316
322
|
(hmac = param_or_nil(webauthn_setup_challenge_hmac_param)) &&
|
317
|
-
timing_safe_eql?(compute_hmac(challenge), hmac) &&
|
323
|
+
(timing_safe_eql?(compute_hmac(challenge), hmac) || (hmac_secret_rotation? && timing_safe_eql?(compute_old_hmac(challenge), hmac))) &&
|
318
324
|
webauthn_credential.verify(challenge)
|
319
325
|
end
|
320
326
|
|
@@ -370,7 +376,7 @@ module Rodauth
|
|
370
376
|
|
371
377
|
(challenge = param_or_nil(webauthn_auth_challenge_param)) &&
|
372
378
|
(hmac = param_or_nil(webauthn_auth_challenge_hmac_param)) &&
|
373
|
-
timing_safe_eql?(compute_hmac(challenge), hmac) &&
|
379
|
+
(timing_safe_eql?(compute_hmac(challenge), hmac) || (hmac_secret_rotation? && timing_safe_eql?(compute_old_hmac(challenge), hmac))) &&
|
374
380
|
webauthn_credential.verify(challenge, public_key: pub_key, sign_count: sign_count) &&
|
375
381
|
ds.update(
|
376
382
|
webauthn_keys_sign_count_column => Integer(webauthn_credential.sign_count),
|
@@ -6,6 +6,8 @@ module Rodauth
|
|
6
6
|
|
7
7
|
auth_value_method :webauthn_autofill_js, File.binread(File.expand_path('../../../../javascript/webauthn_autofill.js', __FILE__)).freeze
|
8
8
|
|
9
|
+
translatable_method :webauthn_invalid_webauthn_id_message, "no webauthn key with given id found"
|
10
|
+
|
9
11
|
route(:webauthn_autofill_js) do |r|
|
10
12
|
before_webauthn_autofill_js_route
|
11
13
|
r.get do
|
@@ -47,7 +49,11 @@ module Rodauth
|
|
47
49
|
.where(webauthn_keys_webauthn_id_column => credential_id)
|
48
50
|
.get(webauthn_keys_account_id_column)
|
49
51
|
|
50
|
-
|
52
|
+
unless account_id
|
53
|
+
throw_error_reason(:invalid_webauthn_id, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_webauthn_id_message)
|
54
|
+
end
|
55
|
+
|
56
|
+
@account = account_ds(account_id).first
|
51
57
|
end
|
52
58
|
|
53
59
|
def webauthn_login_options?
|
@@ -10,6 +10,11 @@ module Rodauth
|
|
10
10
|
|
11
11
|
error_flash "There was an error authenticating via WebAuthn"
|
12
12
|
|
13
|
+
auth_value_method :webauthn_login_user_verification_additional_factor?, false
|
14
|
+
|
15
|
+
internal_request_method :webauthn_login_params
|
16
|
+
internal_request_method :webauthn_login
|
17
|
+
|
13
18
|
route(:webauthn_login) do |r|
|
14
19
|
check_already_logged_in
|
15
20
|
before_webauthn_login_route
|
@@ -24,6 +29,9 @@ module Rodauth
|
|
24
29
|
before_webauthn_login
|
25
30
|
login('webauthn') do
|
26
31
|
webauthn_update_session(webauthn_credential.id)
|
32
|
+
if webauthn_login_verification_factor?(webauthn_credential)
|
33
|
+
two_factor_update_session('webauthn-verification')
|
34
|
+
end
|
27
35
|
end
|
28
36
|
end
|
29
37
|
|
@@ -48,12 +56,23 @@ module Rodauth
|
|
48
56
|
end
|
49
57
|
end
|
50
58
|
|
59
|
+
def webauthn_user_verification
|
60
|
+
return 'preferred' if webauthn_login_user_verification_additional_factor?
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
51
64
|
def use_multi_phase_login?
|
52
65
|
true
|
53
66
|
end
|
54
67
|
|
55
68
|
private
|
56
69
|
|
70
|
+
def webauthn_login_verification_factor?(webauthn_credential)
|
71
|
+
webauthn_login_user_verification_additional_factor? &&
|
72
|
+
webauthn_credential.response.authenticator_data.user_verified? &&
|
73
|
+
uses_two_factor_authentication?
|
74
|
+
end
|
75
|
+
|
57
76
|
def account_from_webauthn_login
|
58
77
|
account_from_login(param(login_param))
|
59
78
|
end
|
data/lib/rodauth/version.rb
CHANGED
data/lib/rodauth.rb
CHANGED
@@ -214,6 +214,22 @@ module Rodauth
|
|
214
214
|
auth_methods meth
|
215
215
|
end
|
216
216
|
|
217
|
+
def response(name=feature_name)
|
218
|
+
meth = :"#{name}_response"
|
219
|
+
overridable_meth = :"_#{meth}"
|
220
|
+
notice_flash_meth = :"#{name}_notice_flash"
|
221
|
+
redirect_meth = :"#{name}_redirect"
|
222
|
+
define_method(overridable_meth) do
|
223
|
+
set_notice_flash send(notice_flash_meth)
|
224
|
+
redirect send(redirect_meth)
|
225
|
+
end
|
226
|
+
define_method(meth) do
|
227
|
+
require_response(overridable_meth)
|
228
|
+
end
|
229
|
+
private overridable_meth, meth
|
230
|
+
auth_private_methods meth
|
231
|
+
end
|
232
|
+
|
217
233
|
def loaded_templates(v)
|
218
234
|
define_method(:loaded_templates) do
|
219
235
|
super().concat(v)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rodauth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.32.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-10-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -348,6 +348,8 @@ extra_rdoc_files:
|
|
348
348
|
- doc/release_notes/2.29.0.txt
|
349
349
|
- doc/release_notes/2.3.0.txt
|
350
350
|
- doc/release_notes/2.30.0.txt
|
351
|
+
- doc/release_notes/2.31.0.txt
|
352
|
+
- doc/release_notes/2.32.0.txt
|
351
353
|
- doc/release_notes/2.4.0.txt
|
352
354
|
- doc/release_notes/2.5.0.txt
|
353
355
|
- doc/release_notes/2.6.0.txt
|
@@ -394,6 +396,7 @@ files:
|
|
394
396
|
- doc/guides/query_params.rdoc
|
395
397
|
- doc/guides/redirects.rdoc
|
396
398
|
- doc/guides/registration_field.rdoc
|
399
|
+
- doc/guides/render_confirmation.rdoc
|
397
400
|
- doc/guides/require_mfa.rdoc
|
398
401
|
- doc/guides/reset_password_autologin.rdoc
|
399
402
|
- doc/guides/share_configuration.rdoc
|
@@ -465,6 +468,8 @@ files:
|
|
465
468
|
- doc/release_notes/2.29.0.txt
|
466
469
|
- doc/release_notes/2.3.0.txt
|
467
470
|
- doc/release_notes/2.30.0.txt
|
471
|
+
- doc/release_notes/2.31.0.txt
|
472
|
+
- doc/release_notes/2.32.0.txt
|
468
473
|
- doc/release_notes/2.4.0.txt
|
469
474
|
- doc/release_notes/2.5.0.txt
|
470
475
|
- doc/release_notes/2.6.0.txt
|