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