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
@@ -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
|