rodauth 2.30.0 → 2.32.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +40 -0
  3. data/README.rdoc +4 -1
  4. data/doc/argon2.rdoc +9 -5
  5. data/doc/base.rdoc +1 -0
  6. data/doc/change_login.rdoc +1 -0
  7. data/doc/change_password.rdoc +1 -0
  8. data/doc/close_account.rdoc +1 -0
  9. data/doc/confirm_password.rdoc +1 -0
  10. data/doc/create_account.rdoc +1 -0
  11. data/doc/email_auth.rdoc +1 -0
  12. data/doc/error_reasons.rdoc +1 -0
  13. data/doc/guides/registration_field.rdoc +1 -1
  14. data/doc/guides/render_confirmation.rdoc +17 -0
  15. data/doc/internal_request.rdoc +76 -0
  16. data/doc/json.rdoc +1 -0
  17. data/doc/jwt.rdoc +7 -1
  18. data/doc/jwt_refresh.rdoc +1 -1
  19. data/doc/lockout.rdoc +4 -2
  20. data/doc/login.rdoc +2 -1
  21. data/doc/logout.rdoc +1 -0
  22. data/doc/otp.rdoc +3 -0
  23. data/doc/release_notes/2.31.0.txt +47 -0
  24. data/doc/release_notes/2.32.0.txt +65 -0
  25. data/doc/remember.rdoc +1 -0
  26. data/doc/reset_password.rdoc +2 -0
  27. data/doc/sms_codes.rdoc +5 -0
  28. data/doc/two_factor_base.rdoc +2 -0
  29. data/doc/verify_account.rdoc +2 -0
  30. data/doc/verify_login_change.rdoc +1 -0
  31. data/doc/webauthn.rdoc +2 -0
  32. data/doc/webauthn_autofill.rdoc +5 -0
  33. data/doc/webauthn_login.rdoc +1 -0
  34. data/lib/rodauth/features/active_sessions.rb +10 -4
  35. data/lib/rodauth/features/argon2.rb +54 -15
  36. data/lib/rodauth/features/base.rb +39 -4
  37. data/lib/rodauth/features/change_login.rb +2 -2
  38. data/lib/rodauth/features/change_password.rb +2 -2
  39. data/lib/rodauth/features/close_account.rb +2 -2
  40. data/lib/rodauth/features/confirm_password.rb +2 -2
  41. data/lib/rodauth/features/create_account.rb +2 -2
  42. data/lib/rodauth/features/email_auth.rb +7 -12
  43. data/lib/rodauth/features/email_base.rb +4 -6
  44. data/lib/rodauth/features/internal_request.rb +47 -1
  45. data/lib/rodauth/features/json.rb +6 -1
  46. data/lib/rodauth/features/jwt.rb +17 -1
  47. data/lib/rodauth/features/jwt_refresh.rb +4 -2
  48. data/lib/rodauth/features/lockout.rb +6 -6
  49. data/lib/rodauth/features/login.rb +13 -4
  50. data/lib/rodauth/features/logout.rb +2 -2
  51. data/lib/rodauth/features/otp.rb +35 -7
  52. data/lib/rodauth/features/remember.rb +15 -11
  53. data/lib/rodauth/features/reset_password.rb +5 -5
  54. data/lib/rodauth/features/single_session.rb +4 -3
  55. data/lib/rodauth/features/sms_codes.rb +23 -10
  56. data/lib/rodauth/features/two_factor_base.rb +8 -6
  57. data/lib/rodauth/features/update_password_hash.rb +2 -1
  58. data/lib/rodauth/features/verify_account.rb +7 -12
  59. data/lib/rodauth/features/verify_login_change.rb +2 -2
  60. data/lib/rodauth/features/webauthn.rb +12 -6
  61. data/lib/rodauth/features/webauthn_autofill.rb +7 -1
  62. data/lib/rodauth/features/webauthn_login.rb +19 -0
  63. data/lib/rodauth/version.rb +1 -1
  64. data/lib/rodauth.rb +16 -0
  65. 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
- set_notice_flash logout_notice_flash
30
- redirect logout_redirect
30
+ logout_response
31
31
  end
32
32
  end
33
33
 
@@ -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
- set_notice_flash otp_setup_notice_flash
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
- set_notice_flash otp_disable_notice_flash
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
- otp_class.new(otp_user_key, :issuer=>otp_issuer, :digits=>otp_digits, :interval=>otp_interval)
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
- set_notice_flash remember_notice_flash
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
- unless valid = timing_safe_eql?(key, compute_hmac(actual))
101
- unless raw_remember_token_deadline && raw_remember_token_deadline > convert_timestamp(deadline)
102
- return
103
- end
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 set_remember_cookie
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(active_remember_key_ds.get(remember_deadline_column))
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
- set_notice_flash reset_password_email_sent_notice_flash
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
- set_notice_flash reset_password_notice_flash
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, compute_hmac(current_key))
42
- if !valid && !allow_raw_single_session_key?
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 :sms_codes_primary?
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
- set_notice_flash sms_request_notice_flash
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
- set_notice_flash sms_needs_confirmation_error_flash
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
- set_notice_flash sms_confirm_notice_flash
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
- set_notice_flash sms_disable_notice_flash
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
- set_notice_flash two_factor_disable_notice_flash
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
- set_notice_flash two_factor_auth_notice_flash
251
- redirect_two_factor_authenticated
252
+ require_response(:_two_factor_auth_response)
252
253
  end
253
254
 
254
- def redirect_two_factor_authenticated
255
+ def _two_factor_auth_response
255
256
  saved_two_factor_auth_redirect = remove_session_value(two_factor_auth_redirect_session_key)
256
- redirect saved_two_factor_auth_redirect || two_factor_auth_redirect
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
- verified = true
83
+ verify_account_email_sent_response
83
84
  end
84
85
  end
85
86
 
86
- if verified
87
- set_notice_flash verify_account_email_sent_notice_flash
88
- else
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
- set_notice_flash verify_account_notice_flash
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
- set_notice_flash verify_login_change_notice_flash
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
- set_notice_flash webauthn_setup_notice_flash
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
- set_notice_flash webauthn_remove_notice_flash
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
- @account = account_ds(account_id).first if account_id
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
@@ -6,7 +6,7 @@ module Rodauth
6
6
  MAJOR = 2
7
7
 
8
8
  # The minor version of Rodauth, updated for new feature releases of Rodauth.
9
- MINOR = 30
9
+ MINOR = 32
10
10
 
11
11
  # The patch version of Rodauth, updated only for bug fixes from the last
12
12
  # feature release.
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.30.0
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-05-22 00:00:00.000000000 Z
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