rodauth 2.35.0 → 2.36.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2b961b8668976f18f46df1c499c1c8f101e75a76527feef460419ad71d7d15b
4
- data.tar.gz: 446184720914538cd207b90cb0d6b9e484eadb12559be84f117532aed5d1cf87
3
+ metadata.gz: 4daf43beb9b2af129683b299f1e455e5b38fa925b1cc020ec79ae951f56f292b
4
+ data.tar.gz: a694fe203f76512691004d1138ad03cab7699d317a0a658ae6ac51732ff22b36
5
5
  SHA512:
6
- metadata.gz: 548c5f50659441297116a0343d2cf2160a8f54ec7c0f7b87867e6377bf9165a53db9b69802f2573343031a26c073ef3e4d0e4358163a97686511b32448f05f5f
7
- data.tar.gz: bf33b84dcb33a6f5cb54f4df2f4561d57d74cc91040032d3eee58e72dbdedad618b726daff2341e91f4350a451b816f719c836f6211acfe20ffaecb879e736d5
6
+ metadata.gz: aef00a1d31a310ea0061f54ebcb4044ccdcc7106d2f07074f4585ac5406776784bef9c698befa050b374d4dd37dd2495d3a3e4dcd6854e387fc8442901f3f227
7
+ data.tar.gz: 85d9837f21b320f2bec1241f07e22ff150b49192017cb1ea20d2863fce93b547d5271cafe30b08baeaa582fd4f40b715ce488d9fb19bd7fe2cc43a2f565b5266
data/CHANGELOG CHANGED
@@ -1,4 +1,20 @@
1
- === 2.35.0 (2025-05-28)
1
+ === 2.36.0 (2024-07-23)
2
+
3
+ * Add webauthn_modify_email feature for emailing when a WebAuthn authenticator is added or removed (jeremyevans)
4
+
5
+ * Add account_from_id method for retrieving an account using the account id and optional status id (janko) (#431)
6
+
7
+ * Add otp_modify_email feature for emailing when TOTP authentication is setup or disabled (jeremyevans)
8
+
9
+ * Add otp_lockout_email feature for emailing when TOTP authentication is locked out or unlocked (jeremyevans)
10
+
11
+ * Add strftime_format configuration method for configuring display of Time values to users (jeremyevans)
12
+
13
+ * Add otp_unlock feature for unlocking TOTP authentication after it has been locked out (jeremyevans)
14
+
15
+ * Make internal_request feature work with Roda path_rewriter plugin (jeremyevans) (#425)
16
+
17
+ === 2.35.0 (2024-05-28)
2
18
 
3
19
  * Handle internal_request_configuration blocks in superclasses (jeremyevans, bjeanes)
4
20
 
data/README.rdoc CHANGED
@@ -38,7 +38,11 @@ HTML and JSON API for all supported features.
38
38
  * WebAuthn Login (Passwordless login via WebAuthn)
39
39
  * WebAuthn Verify Account (Passwordless WebAuthn Setup)
40
40
  * WebAuthn Autofill (Autofill WebAuthn credentials on login)
41
+ * WebAuthn Modify Email (Email when WebAuthn authenticator aded or removed)
41
42
  * OTP (Multifactor authentication via TOTP)
43
+ * OTP Modify Email (Email when TOTP authentication setup or disabled)
44
+ * OTP Unlock (Unlock TOTP authentication after lockout)
45
+ * OTP Lockout Email (Email when TOTP authentication locked out or unlocked)
42
46
  * Recovery Codes (Multifactor authentication via backup codes)
43
47
  * SMS Codes (Multifactor authentication via SMS)
44
48
  * Verify Login Change (Verify new login before changing login)
@@ -328,7 +332,7 @@ PostgreSQL 15 changed default database security so that only the database
328
332
  owner has writable access to the public schema. Rodauth expects the
329
333
  +ph+ account to have writable access to the public schema when setting
330
334
  things up. Temporarily grant that access (it will be revoked after the
331
- migation has run)
335
+ migration has run)
332
336
 
333
337
  psql -U postgres -c "GRANT CREATE ON SCHEMA public TO ${DATABASE_NAME}_password" ${DATABASE_NAME}
334
338
 
@@ -587,6 +591,13 @@ Note that these migrations require Sequel 4.35.0+.
587
591
  Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP
588
592
  end
589
593
 
594
+ # Used by the otp_unlock feature
595
+ create_table(:account_otp_unlocks) do
596
+ foreign_key :id, :accounts, primary_key: true, type: :Bignum
597
+ Integer :num_successes, null: false, default: 1
598
+ Time :next_auth_attempt_after, null: false, default: Sequel::CURRENT_TIMESTAMP
599
+ end
600
+
590
601
  # Used by the recovery codes feature
591
602
  create_table(:account_recovery_codes) do
592
603
  foreign_key :id, :accounts, type: :Bignum
@@ -631,6 +642,7 @@ Note that these migrations require Sequel 4.35.0+.
631
642
  run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_webauthn_user_ids TO #{user}"
632
643
  run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_webauthn_keys TO #{user}"
633
644
  run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_otp_keys TO #{user}"
645
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_otp_unlocks TO #{user}"
634
646
  run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_recovery_codes TO #{user}"
635
647
  run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_sms_codes TO #{user}"
636
648
  end
@@ -639,6 +651,7 @@ Note that these migrations require Sequel 4.35.0+.
639
651
  down do
640
652
  drop_table(:account_sms_codes,
641
653
  :account_recovery_codes,
654
+ :account_otp_unlocks,
642
655
  :account_otp_keys,
643
656
  :account_webauthn_keys,
644
657
  :account_webauthn_user_ids,
@@ -920,6 +933,9 @@ view the appropriate file in the doc directory.
920
933
  * {Login}[rdoc-ref:doc/login.rdoc]
921
934
  * {Logout}[rdoc-ref:doc/logout.rdoc]
922
935
  * {OTP}[rdoc-ref:doc/otp.rdoc]
936
+ * {OTP Lockout Email}[rdoc-ref:doc/otp_lockout_email.rdoc]
937
+ * {OTP Modify Email}[rdoc-ref:doc/otp_modify_email.rdoc]
938
+ * {OTP Unlock}[rdoc-ref:doc/otp_unlock.rdoc]
923
939
  * {Password Complexity}[rdoc-ref:doc/password_complexity.rdoc]
924
940
  * {Password Expiration}[rdoc-ref:doc/password_expiration.rdoc]
925
941
  * {Password Grace Period}[rdoc-ref:doc/password_grace_period.rdoc]
@@ -939,6 +955,7 @@ view the appropriate file in the doc directory.
939
955
  * {WebAuthn}[rdoc-ref:doc/webauthn.rdoc]
940
956
  * {WebAuthn Autofill}[rdoc-ref:doc/webauthn_autofill.rdoc]
941
957
  * {WebAuthn Login}[rdoc-ref:doc/webauthn_login.rdoc]
958
+ * {WebAuthn Modify Email}[rdoc-ref:doc/webauthn_modify_email.rdoc]
942
959
  * {WebAuthn Verify Account}[rdoc-ref:doc/webauthn_verify_account.rdoc]
943
960
 
944
961
  === Calling Rodauth in the Routing Tree
data/doc/base.rdoc CHANGED
@@ -74,6 +74,7 @@ password_param :: The parameter name to use for passwords.
74
74
  require_login_error_flash :: The flash error to display when accessing a page that requires a login, when you are not logged in.
75
75
  require_login_redirect :: A redirect to the login page.
76
76
  set_deadline_values? :: Whether deadline values should be set. True by default on MySQL, as that doesn't support default values that are not constant. Can be set to true on other databases if you want to vary the value based on a request parameter.
77
+ strftime_format :: The format to pass to Time#strftime when formatting timestamps to display to the user, '%F %T' by default.
77
78
  template_opts :: Any template options to pass to view/render. This can be used to set a custom layout, for example.
78
79
  token_separator :: The string used to separate account id from the random key in links.
79
80
  unmatched_field_error_status :: The response status to use when two field values should match but do not, 422 by default.
@@ -84,6 +85,7 @@ use_request_specific_csrf_tokens? :: Whether to use request-specific CSRF tokens
84
85
 
85
86
  == Auth Methods
86
87
 
88
+ account_from_id(id, status_id=nil) :: Retrieve the account hash for the given account id and status.
87
89
  account_from_login(login) :: Retrieve the account hash related to the given login or nil if no login matches.
88
90
  account_from_session :: Retrieve the account hash related to the currently logged in session.
89
91
  account_id :: The primary key value of the current account.
@@ -0,0 +1,30 @@
1
+ = Documentation for OTP Lockout Email Feature
2
+
3
+ The otp_lockout_email feature emails users when:
4
+
5
+ * TOTP authentication is locked out
6
+ * TOTP authentication is unlocked
7
+ * A TOTP unlock attempt has failed
8
+
9
+ The otp_unlock_email feature depends on the otp_lockout and email_base features.
10
+
11
+ == Auth Value Methods
12
+
13
+ otp_locked_out_email_body :: Body to use for the email notifying user that TOTP authentication has been locked out.
14
+ otp_locked_out_email_subject :: Subject to use for the email notifying user that TOTP authentication has been locked out.
15
+ otp_unlock_failed_email_body :: Body to use for the email notifying user that there has been an unsuccessful attempt to unlock TOTP authentication.
16
+ otp_unlock_failed_email_subject :: Subject to use for the email notifying user that there has been an unsuccessful attempt to unlock TOTP authentication.
17
+ otp_unlocked_email_body :: Body to use for the email notifying user that TOTP authentication has been unlocked.
18
+ otp_unlocked_email_subject :: Subject to use for the email notifying user that TOTP authentication has been unlocked.
19
+ send_otp_locked_out_email? :: Whether to send an email when TOTP authentication is locked out.
20
+ send_otp_unlock_failed_email? :: Whether to send an email when there has been an unsuccessful attempt to unlock TOTP authentication.
21
+ send_otp_unlocked_email? :: Whether to send an email when TOTP authentication is unlocked.
22
+
23
+ == Auth Methods
24
+
25
+ create_otp_locked_out_email :: A Mail::Message for the email notifying user that TOTP authentication has been locked out.
26
+ create_otp_unlock_failed_email :: A Mail::Message for the email notifying user that there has been an unsuccessful attempt to unlock TOTP authentication.
27
+ create_otp_unlocked_email :: A Mail::Message for the email notifying user that TOTP authentication has been unlocked.
28
+ send_otp_locked_out_email :: Send the email notifying user that TOTP authentication has been locked out.
29
+ send_otp_unlock_failed_email :: Send the email notifying user that there has been an unsuccessful attempt to unlock TOTP authentication.
30
+ send_otp_unlocked_email :: Send the email notifying user that TOTP authentication has been unlocked.
@@ -0,0 +1,19 @@
1
+ = Documentation for OTP Modify Email Feature
2
+
3
+ The otp_modify_email feature emails users when TOTP authentication is setup or disabled.
4
+
5
+ The otp_modify_email feature depends on the otp and email_base features.
6
+
7
+ == Auth Value Methods
8
+
9
+ otp_disabled_email_body :: Body to use for the email notifying user that TOTP authentication has been disabled.
10
+ otp_disabled_email_subject :: Subject to use for the email notifying user that TOTP authentication has been disabled.
11
+ otp_setup_email_body :: Body to use for the email notifying user that TOTP authentication has been setup.
12
+ otp_setup_email_subject :: Subject to use for the email notifying user that TOTP authentication has been setup.
13
+
14
+ == Auth Methods
15
+
16
+ create_otp_disabled_email :: A Mail::Message for the email notifying user that TOTP authentication has been disabled.
17
+ create_otp_setup_email :: A Mail::Message for the email notifying user that TOTP authentication has been setup.
18
+ send_otp_disabled_email :: Send the email notifying user that TOTP authentication has been disabled.
19
+ send_otp_setup_email :: Send the email notifying user that TOTP authentication has been setup.
@@ -0,0 +1,58 @@
1
+ = Documentation for OTP Unlock Feature
2
+
3
+ The otp_unlock feature implements unlocking of TOTP authentication after
4
+ TOTP authentication. The user must consecutively successfully authenticate
5
+ with TOTP multiple times (default: 3) within a given time period (15 minutes
6
+ per attempt) in order to unlock TOTP authentication. By requiring
7
+ consecutive successful unlocks, with a delay after failure, it is infeasible
8
+ to brute force the TOTP unlock process.
9
+
10
+ The otp_unlock feature depends on the otp feature.
11
+
12
+ == Auth Value Methods
13
+
14
+ otp_unlock_additional_form_tags :: HTML fragment containing additional form tags to use on the OTP unlock form.
15
+ otp_unlock_auth_deadline_passed_error_flash :: The flash error to show if attempting to unlock OTP after the deadline for submittal has passed.
16
+ otp_unlock_auth_deadline_passed_error_status :: The response status to use if attempting to unlock OTP after the deadline for submittal has passed, 403 by default.
17
+ otp_unlock_auth_failure_cooldown_seconds :: The number of seconds the user must wait to attempt OTP unlock again after a failed OTP unlock attempt.
18
+ otp_unlock_auth_failure_error_flash :: The flash error to show if attempting to unlock OTP using an incorrect authentication code.
19
+ otp_unlock_auth_failure_error_status :: The response status to use if attempting to unlock OTP using an incorrect authentication code, 403 by default.
20
+ otp_unlock_auth_not_yet_available_error_flash :: The flash error to show if attempting to unlock OTP when doing so is not yet available due to a recent attempt.
21
+ otp_unlock_auth_not_yet_available_error_status :: The response status to use if attempting to unlock OTP when doing so is not yet available due to a recent attempt, 403 by default.
22
+ otp_unlock_auth_success_notice_flash :: The flash notice to show upon successful unlock authentication, when additional unlock authentication is still needed.
23
+ otp_unlock_auths_required :: The number of consecutive successful authentication attempts needed to unlock OTP authentication, 3 by default.
24
+ otp_unlock_button :: Text to use for button on OTP unlock form.
25
+ otp_unlock_consecutive_successes_label :: Text to show next to the number of consecutive successful authentication attempts the user has already made.
26
+ otp_unlock_deadline_seconds :: The number of seconds between a previously successful authentication attempt and the next successful authentication attempt. This defaults to twice the amount of time of the OTP interval (30 seconds) plus twice the amount of allowed drift (30 seconds), for a total of 120 seconds. This is to make sure the same OTP code cannot be used more than one when unlocking.
27
+ otp_unlock_form_footer :: A footer to display at the bottom of the OTP unlock form.
28
+ otp_unlock_id_column :: The column in the +otp_unlock_table+ containing the account id.
29
+ otp_unlock_next_auth_attempt_after_column :: The column in the +otp_unlock_table+ containing a timestamp for when the user can next try an authentication attempt.
30
+ otp_unlock_next_auth_attempt_label :: Text to show next to the time when the next unlock authentication attempt will be allowed.
31
+ otp_unlock_next_auth_attempt_refresh_label :: Text to show explaining that the page will refresh when the next unlock authentication attempt will be allowed.
32
+ otp_unlock_next_auth_deadline_label :: Text to show next to the deadline for unlock authentication.
33
+ otp_unlock_not_available_page_title :: The page title to use on the page letting users know they need to wait to unlock OTP authentication.
34
+ otp_unlock_not_locked_out_error_flash :: The flash error to show if attempting to access the OTP unlock page when OTP authentication is not locked out.
35
+ otp_unlock_not_locked_out_error_status :: The response status to use if attempting to access the OTP unlock page when OTP authentication is not locked out, 403 by default.
36
+ otp_unlock_not_locked_out_redirect :: Where to redirect if attempting to access the OTP unlock page when OTP authentication is not locked out.
37
+ otp_unlock_num_successes_column :: The column in the +otp_unlock_table+ containing the number of consecutive successful authentications.
38
+ otp_unlock_page_title :: The page title to use on the OTP unlock form.
39
+ otp_unlock_refresh_tag :: The meta refresh tag HTML to use to force a refresh of the page. This can be overridden to use a different refresh approach.
40
+ otp_unlock_required_consecutive_successes_label :: Text to show next to the number of consecutive successful authentication attempts the user is required to make to unlock OTP authentication.
41
+ otp_unlock_route :: The route to the OTP unlock action. Defaults to +otp-unlock+.
42
+ otp_unlock_table :: The table name containing the OTP unlock information.
43
+ otp_unlocked_notice_flash :: The flash notice to show when OTP authentication is successfully fully unlocked.
44
+ otp_unlocked_redirect :: Where to redirect when OTP authentication is successfully fully unlocked.
45
+
46
+ == Auth Methods
47
+
48
+ after_otp_unlock_auth_failure :: Run arbitrary code after OTP unlock authentication failure.
49
+ after_otp_unlock_auth_success :: Run arbitrary code after OTP unlock authentication success.
50
+ after_otp_unlock_not_yet_available :: Run arbitrary code when attempting OTP unlock when it is not yet available.
51
+ before_otp_unlock_attempt :: Run arbitrary code before checking whether OTP unlock authentication code is valid.
52
+ before_otp_unlock_route :: Run arbitrary code before handling an OTP unlock route.
53
+ otp_unlock_auth_failure :: Handle a authentication failure when trying to unlock. By default, this sets the number of consecutive successful authentication attempts to 0, and forces a significant delay before the next unlock authentication attempt can be made.
54
+ otp_unlock_auth_success :: Handle a authentication failure when trying to unlock. By default, this increments the number of consecutive successful authentication attempts, and imposes a short delay before the next unlock authentication attempt can be made (to ensure the code cannot be reused).
55
+ otp_unlock_available? :: Returns whether it is possible to unlock OTP authentication. This assumes that OTP is already locked out.
56
+ otp_unlock_deadline_passed? :: Returns whether the deadline to submit an OTP unlock authentication code has passed.
57
+ otp_unlock_not_available_view :: The HTML to use for the page when the OTP unlock form is not yet available due to a recent unlock authentication attempt.
58
+ otp_unlock_view :: The HTML to use for the OTP unlock form.
@@ -0,0 +1,35 @@
1
+ = New Features
2
+
3
+ * An otp_unlock feature has been added, allowing a user to unlock
4
+ TOTP authentication with 3 consecutive successful TOTP
5
+ authentications. Previously, once TOTP authentication was locked
6
+ out, there was no way for the user to unlock it.
7
+
8
+ Any unsuccessful TOTP authentication during the unlock process
9
+ prevents unlocks attempts for a configurable amount of time (15
10
+ minutes by default). By default, this limits brute force attempts
11
+ to unlock TOTP authentication to less than 10^2 per day, with the
12
+ odds of a successful unlock in each attempt being 1 in 10^18.
13
+
14
+ * An otp_lockout_email feature has been added for emailing the user
15
+ when their TOTP authentication has been locked out or unlocked, and
16
+ when there has been a failed unlock attempt.
17
+
18
+ * An otp_modify_email feature has been added for emailing the user
19
+ when TOTP authentication has been setup or disabled for their
20
+ account.
21
+
22
+ * A webauthn_modify_email feature has been added for emailing the
23
+ user when a WebAuthn authenticator has been added or removed from
24
+ their account.
25
+
26
+ * An account_from_id configuration method has been added for loading
27
+ the account with the given account id.
28
+
29
+ * A strftime_format configuration method has been added for
30
+ configuring how Time values are formatted for display to the user.
31
+
32
+ = Improvements
33
+
34
+ * The internal_request feature now works with Roda's path_rewriter
35
+ plugin.
@@ -0,0 +1,19 @@
1
+ = Documentation for WebAuthn Modify Email Feature
2
+
3
+ The webauthn_modify_email feature emails users when a WebAuthn authenticator is added to or removed from their account.
4
+
5
+ The webauthn_modify_email feature depends on the webauthn and email_base features.
6
+
7
+ == Auth Value Methods
8
+
9
+ webauthn_authenticator_added_email_body :: Body to use for the email notifying user that a WebAuthn authenticator has been added to their account.
10
+ webauthn_authenticator_added_email_subject :: Subject to use for the email notifying user that a WebAuthn authenticator has been added to their account.
11
+ webauthn_authenticator_removed_email_body :: Body to use for the email notifying user that a WebAuthn authenticator has been removed from their account.
12
+ webauthn_authenticator_removed_email_subject :: Subject to use for the email notifying user that a WebAuthn authenticator has been removed from their account.
13
+
14
+ == Auth Methods
15
+
16
+ create_webauthn_authenticator_added_email :: A Mail::Message for the email notifying user that a WebAuthn authenticator has been added to their account.
17
+ create_webauthn_authenticator_removed_email :: A Mail::Message for the email notifying user that a WebAuthn authenticator has been removed from their account.
18
+ send_webauthn_authenticator_added_email :: Send the email notifying user that a WebAuthn authenticator has been added to their account.
19
+ send_webauthn_authenticator_removed_email :: Send the email notifying user that a WebAuthn authenticator has been removed from their account.
@@ -59,6 +59,7 @@ module Rodauth
59
59
  auth_value_method :mark_input_fields_with_autocomplete?, true
60
60
  auth_value_method :mark_input_fields_with_inputmode?, true
61
61
  auth_value_method :skip_status_checks?, true
62
+ translatable_method :strftime_format, '%F %T'
62
63
  auth_value_method :template_opts, {}.freeze
63
64
  auth_value_method :title_instance_variable, nil
64
65
  auth_value_method :token_separator, "_"
@@ -115,6 +116,7 @@ module Rodauth
115
116
  )
116
117
 
117
118
  auth_private_methods(
119
+ :account_from_id,
118
120
  :account_from_login,
119
121
  :account_from_session,
120
122
  :convert_token_id,
@@ -384,6 +386,10 @@ module Rodauth
384
386
  @account = _account_from_session
385
387
  end
386
388
 
389
+ def account_from_id(id, status_id=nil)
390
+ @account = _account_from_id(id, status_id)
391
+ end
392
+
387
393
  def check_csrf
388
394
  scope.check_csrf!(check_csrf_opts, &check_csrf_block)
389
395
  end
@@ -739,6 +745,12 @@ module Rodauth
739
745
  ds.first
740
746
  end
741
747
 
748
+ def _account_from_id(id, status_id=nil)
749
+ ds = account_ds(id)
750
+ ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
751
+ ds.first
752
+ end
753
+
742
754
  def hmac_secret_rotation?
743
755
  hmac_secret && hmac_old_secret && hmac_secret != hmac_old_secret
744
756
  end
@@ -74,9 +74,7 @@ module Rodauth
74
74
  ((!hmac_secret || allow_raw_email_token?) && timing_safe_eql?(key, actual))
75
75
  return
76
76
  end
77
- ds = account_ds(id)
78
- ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
79
- ds.first
77
+ _account_from_id(id, status_id)
80
78
  end
81
79
  end
82
80
  end
@@ -183,9 +183,7 @@ module Rodauth
183
183
  def account_from_key(token, status_id=nil)
184
184
  return super unless session_value
185
185
  return unless yield session_value
186
- ds = account_ds(session_value)
187
- ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
188
- ds.first
186
+ _account_from_id(session_value, status_id)
189
187
  end
190
188
 
191
189
  def _set_internal_request_return_value(value)
@@ -210,7 +208,7 @@ module Rodauth
210
208
  end
211
209
 
212
210
  def _set_login_param_from_account
213
- if session_value && !params[login_param] && (account = account_ds(session_value).first)
211
+ if session_value && !params[login_param] && (account = _account_from_id(session_value))
214
212
  params[login_param] = account[login_column]
215
213
  end
216
214
  end
@@ -311,7 +309,7 @@ module Rodauth
311
309
 
312
310
  env = {
313
311
  'REQUEST_METHOD'=>'POST',
314
- 'PATH_INFO'=>'/',
312
+ 'PATH_INFO'=>'/'.dup,
315
313
  "SCRIPT_NAME" => "",
316
314
  "HTTP_HOST" => INVALID_DOMAIN,
317
315
  "SERVER_NAME" => INVALID_DOMAIN,
@@ -72,6 +72,32 @@ module Rodauth
72
72
 
73
73
  private
74
74
 
75
+ def _set_otp_unlock_info
76
+ if use_json?
77
+ json_response[:num_successes] = otp_unlock_num_successes
78
+ json_response[:required_successes] = otp_unlock_auths_required
79
+ json_response[:next_attempt_after] = otp_unlock_next_auth_attempt_after.to_i
80
+ end
81
+ end
82
+
83
+ def after_otp_unlock_auth_success
84
+ super if defined?(super)
85
+ if otp_locked_out?
86
+ _set_otp_unlock_info
87
+ json_response[:deadline] = otp_unlock_deadline.to_i
88
+ end
89
+ end
90
+
91
+ def after_otp_unlock_auth_failure
92
+ super if defined?(super)
93
+ _set_otp_unlock_info
94
+ end
95
+
96
+ def after_otp_unlock_not_yet_available
97
+ super if defined?(super)
98
+ _set_otp_unlock_info
99
+ end
100
+
75
101
  def before_two_factor_manage_route
76
102
  super if defined?(super)
77
103
  if use_json?
@@ -287,7 +287,7 @@ module Rodauth
287
287
 
288
288
  def otp_update_last_use
289
289
  otp_key_ds.
290
- where(Sequel.date_add(otp_keys_last_use_column, :seconds=>(otp_interval||30)) < Sequel::CURRENT_TIMESTAMP).
290
+ where(Sequel.date_add(otp_keys_last_use_column, :seconds=>_otp_interval) < Sequel::CURRENT_TIMESTAMP).
291
291
  update(otp_keys_last_use_column=>Sequel::CURRENT_TIMESTAMP) == 1
292
292
  end
293
293
 
@@ -346,7 +346,7 @@ module Rodauth
346
346
 
347
347
  def _two_factor_auth_links
348
348
  links = super
349
- links << [20, otp_auth_path, otp_auth_link_text] if otp_available?
349
+ links << [20, otp_auth_path, otp_auth_link_text] if show_otp_auth_link?
350
350
  links
351
351
  end
352
352
 
@@ -420,6 +420,10 @@ module Rodauth
420
420
  @otp_key = secret
421
421
  end
422
422
 
423
+ def _otp_interval
424
+ otp_interval || 30
425
+ end
426
+
423
427
  # Called for valid OTP codes for old secrets
424
428
  def _otp_valid_code_for_old_secret
425
429
  end
@@ -447,6 +451,10 @@ module Rodauth
447
451
  db[otp_keys_table].where(otp_keys_id_column=>session_value)
448
452
  end
449
453
 
454
+ def show_otp_auth_link?
455
+ otp_available?
456
+ end
457
+
450
458
  def use_date_arithmetic?
451
459
  true
452
460
  end
@@ -0,0 +1,42 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:otp_lockout_email, :OtpLockoutEmail) do
5
+ depends :otp_unlock, :email_base
6
+
7
+ loaded_templates %w'otp-locked-out-email otp-unlocked-email otp-unlock-failed-email'
8
+ email :otp_locked_out, 'TOTP Authentication Locked Out', :translatable=>true
9
+ email :otp_unlocked, 'TOTP Authentication Unlocked', :translatable=>true
10
+ email :otp_unlock_failed, 'TOTP Authentication Unlocking Failed', :translatable=>true
11
+
12
+ auth_value_method :send_otp_locked_out_email?, true
13
+ auth_value_method :send_otp_unlocked_email?, true
14
+ auth_value_method :send_otp_unlock_failed_email?, true
15
+
16
+ private
17
+
18
+ def after_otp_authentication_failure
19
+ super
20
+
21
+ if otp_locked_out? && send_otp_locked_out_email?
22
+ send_otp_locked_out_email
23
+ end
24
+ end
25
+
26
+ def after_otp_unlock_auth_success
27
+ super
28
+
29
+ if !otp_locked_out? && send_otp_unlocked_email?
30
+ send_otp_unlocked_email
31
+ end
32
+ end
33
+
34
+ def after_otp_unlock_auth_failure
35
+ super
36
+
37
+ if send_otp_unlock_failed_email?
38
+ send_otp_unlock_failed_email
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:otp_modify_email, :OtpModifyEmail) do
5
+ depends :otp, :email_base
6
+
7
+ loaded_templates %w'otp-setup-email otp-disabled-email'
8
+ email :otp_setup, 'TOTP Authentication Setup', :translatable=>true
9
+ email :otp_disabled, 'TOTP Authentication Disabled', :translatable=>true
10
+
11
+ private
12
+
13
+ def after_otp_setup
14
+ super
15
+ send_otp_setup_email
16
+ end
17
+
18
+ def after_otp_disable
19
+ super
20
+ send_otp_disabled_email
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,250 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:otp_unlock, :OtpUnlock) do
5
+ depends :otp
6
+
7
+ before 'otp_unlock_attempt'
8
+ after 'otp_unlock_auth_success'
9
+ after 'otp_unlock_auth_failure'
10
+ after 'otp_unlock_not_yet_available'
11
+
12
+ error_flash "TOTP authentication is not currently locked out", 'otp_unlock_not_locked_out'
13
+ error_flash "TOTP invalid authentication", 'otp_unlock_auth_failure'
14
+ error_flash "Deadline past for unlocking TOTP authentication", 'otp_unlock_auth_deadline_passed'
15
+ error_flash "TOTP unlock attempt not yet available", 'otp_unlock_auth_not_yet_available'
16
+
17
+ notice_flash "TOTP authentication unlocked", 'otp_unlocked'
18
+ notice_flash "TOTP successful authentication, more successful authentication needed to unlock", 'otp_unlock_auth_success'
19
+
20
+ redirect :otp_unlock_not_locked_out
21
+ redirect :otp_unlocked
22
+
23
+ additional_form_tags
24
+
25
+ button 'Authenticate Using TOTP to Unlock', 'otp_unlock'
26
+
27
+ auth_value_method :otp_unlock_auth_deadline_passed_error_status, 403
28
+ auth_value_method :otp_unlock_auth_failure_cooldown_seconds, 900
29
+ auth_value_method :otp_unlock_auth_failure_error_status, 403
30
+ auth_value_method :otp_unlock_auth_not_yet_available_error_status, 403
31
+ auth_value_method :otp_unlock_auths_required, 3
32
+ auth_value_method :otp_unlock_deadline_seconds, 900
33
+ auth_value_method :otp_unlock_id_column, :id
34
+ auth_value_method :otp_unlock_next_auth_attempt_after_column, :next_auth_attempt_after
35
+ auth_value_method :otp_unlock_not_locked_out_error_status, 403
36
+ auth_value_method :otp_unlock_num_successes_column, :num_successes
37
+ auth_value_method :otp_unlock_table, :account_otp_unlocks
38
+
39
+ translatable_method :otp_unlock_consecutive_successes_label, 'Consecutive successful authentications'
40
+ translatable_method :otp_unlock_form_footer, ''
41
+ translatable_method :otp_unlock_next_auth_attempt_label, 'Can attempt next authentication after'
42
+ translatable_method :otp_unlock_next_auth_attempt_refresh_label, 'Page will automatically refresh when authentication is possible.'
43
+ translatable_method :otp_unlock_next_auth_deadline_label, 'Deadline for next authentication'
44
+ translatable_method :otp_unlock_required_consecutive_successes_label, 'Required consecutive successful authentications to unlock'
45
+
46
+ loaded_templates %w'otp-unlock otp-unlock-not-available'
47
+ view 'otp-unlock', 'Unlock TOTP Authentication', 'otp_unlock'
48
+ view 'otp-unlock-not-available', 'Must Wait to Unlock TOTP Authentication', 'otp_unlock_not_available'
49
+
50
+ auth_methods(
51
+ :otp_unlock_auth_failure,
52
+ :otp_unlock_auth_success,
53
+ :otp_unlock_available?,
54
+ :otp_unlock_deadline_passed?,
55
+ :otp_unlock_refresh_tag,
56
+ )
57
+
58
+ route(:otp_unlock) do |r|
59
+ require_login
60
+ require_account_session
61
+ require_otp_setup
62
+
63
+ unless otp_locked_out?
64
+ set_response_error_reason_status(:otp_not_locked_out, otp_unlock_not_locked_out_error_status)
65
+ set_redirect_error_flash otp_unlock_not_locked_out_error_flash
66
+ redirect otp_unlock_not_locked_out_redirect
67
+ end
68
+
69
+ before_otp_unlock_route
70
+
71
+ r.get do
72
+ if otp_unlock_available?
73
+ otp_unlock_view
74
+ else
75
+ otp_unlock_not_available_view
76
+ end
77
+ end
78
+
79
+ r.post do
80
+ db.transaction do
81
+ if otp_unlock_deadline_passed?
82
+ set_response_error_reason_status(:otp_unlock_deadline_passed, otp_unlock_auth_deadline_passed_error_status)
83
+ set_redirect_error_flash otp_unlock_auth_deadline_passed_error_flash
84
+ elsif !otp_unlock_available?
85
+ after_otp_unlock_not_yet_available
86
+ set_response_error_reason_status(:otp_unlock_not_yet_available, otp_unlock_auth_not_yet_available_error_status)
87
+ set_redirect_error_flash otp_unlock_auth_not_yet_available_error_flash
88
+ else
89
+ before_otp_unlock_attempt
90
+ if otp_valid_code?(param(otp_auth_param))
91
+ otp_unlock_auth_success
92
+ after_otp_unlock_auth_success
93
+
94
+ unless otp_locked_out?
95
+ set_notice_flash otp_unlocked_notice_flash
96
+ redirect otp_unlocked_redirect
97
+ end
98
+
99
+ set_notice_flash otp_unlock_auth_success_notice_flash
100
+ else
101
+ otp_unlock_auth_failure
102
+ after_otp_unlock_auth_failure
103
+ set_response_error_reason_status(:otp_unlock_auth_failure, otp_unlock_auth_failure_error_status)
104
+ set_redirect_error_flash otp_unlock_auth_failure_error_flash
105
+ end
106
+ end
107
+ end
108
+
109
+ redirect request.path
110
+ end
111
+ end
112
+
113
+ def otp_unlock_available?
114
+ if otp_unlock_data
115
+ next_auth_attempt_after = otp_unlock_next_auth_attempt_after
116
+ current_timestamp = Time.now
117
+
118
+ if (next_auth_attempt_after < current_timestamp - otp_unlock_deadline_seconds)
119
+ # Unlock process not fully completed within deadline, reset process
120
+ otp_unlock_reset
121
+ true
122
+ else
123
+ if next_auth_attempt_after > current_timestamp
124
+ # If next auth attempt after timestamp is in the future, that means the next
125
+ # unlock attempt cannot happen until then.
126
+ false
127
+ else
128
+ if otp_unlock_num_successes == 0
129
+ # 0 value indicates previous attempt was a failure. Since failure cooldown
130
+ # period has passed, reset process so user gets full deadline period
131
+ otp_unlock_reset
132
+ end
133
+ true
134
+ end
135
+ end
136
+ else
137
+ # No row means no unlock attempts yet (or previous attempt was more than the
138
+ # deadline account, so unlocking is available
139
+ true
140
+ end
141
+ end
142
+
143
+ def otp_unlock_auth_failure
144
+ h = {
145
+ otp_unlock_num_successes_column=>0,
146
+ otp_unlock_next_auth_attempt_after_column=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, :seconds=>otp_unlock_auth_failure_cooldown_seconds)
147
+ }
148
+
149
+ if otp_unlock_ds.update(h) == 0
150
+ h[otp_unlock_id_column] = session_value
151
+
152
+ # If row already exists when inserting, no need to do anything
153
+ raises_uniqueness_violation?{otp_unlock_ds.insert(h)}
154
+ end
155
+ end
156
+
157
+ def otp_unlock_auth_success
158
+ deadline = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, :seconds=>otp_unlock_success_cooldown_seconds)
159
+
160
+ # Add WHERE to avoid possible race condition when multiple unlock auth requests
161
+ # are sent at the same time (only the first should increment num successes).
162
+ if otp_unlock_ds.
163
+ where(Sequel[otp_unlock_next_auth_attempt_after_column] < Sequel::CURRENT_TIMESTAMP).
164
+ update(
165
+ otp_unlock_num_successes_column=>Sequel[otp_unlock_num_successes_column]+1,
166
+ otp_unlock_next_auth_attempt_after_column=>deadline
167
+ ) == 0
168
+
169
+ # Ignore uniqueness errors when inserting after a failed update,
170
+ # which could be caused due to the race condition mentioned above.
171
+ raises_uniqueness_violation? do
172
+ otp_unlock_ds.insert(
173
+ otp_unlock_id_column=>session_value,
174
+ otp_unlock_next_auth_attempt_after_column=>deadline
175
+ )
176
+ end
177
+ end
178
+
179
+ @otp_unlock_data = nil
180
+ # :nocov:
181
+ if otp_unlock_data
182
+ # :nocov:
183
+ if otp_unlock_num_successes >= otp_unlock_auths_required
184
+ # At least the requisite number of consecutive successful unlock
185
+ # authentications. Unlock OTP authentication.
186
+ otp_key_ds.update(otp_keys_failures_column => 0)
187
+
188
+ # Remove OTP unlock metadata when unlocking OTP authentication
189
+ otp_unlock_reset
190
+ # else
191
+ # # Still need additional consecutive successful unlock attempts.
192
+ end
193
+ # else
194
+ # # if row isn't available, probably the process was reset during this,
195
+ # # and it's safe to do nothing in that case.
196
+ end
197
+ end
198
+
199
+ def otp_unlock_deadline_passed?
200
+ otp_unlock_data ? (otp_unlock_next_auth_attempt_after < Time.now - otp_unlock_deadline_seconds) : false
201
+ end
202
+
203
+ def otp_unlock_refresh_tag
204
+ "<meta http-equiv=\"refresh\" content=\"#{(otp_unlock_next_auth_attempt_after - Time.now).to_i + 1}\">"
205
+ end
206
+
207
+ def otp_lockout_redirect
208
+ otp_unlock_path
209
+ end
210
+
211
+ def otp_unlock_next_auth_attempt_after
212
+ if otp_unlock_data
213
+ convert_timestamp(otp_unlock_data[otp_unlock_next_auth_attempt_after_column])
214
+ else
215
+ Time.now
216
+ end
217
+ end
218
+
219
+ def otp_unlock_deadline
220
+ otp_unlock_next_auth_attempt_after + otp_unlock_deadline_seconds
221
+ end
222
+
223
+ def otp_unlock_num_successes
224
+ otp_unlock_data ? otp_unlock_data[otp_unlock_num_successes_column] : 0
225
+ end
226
+
227
+ private
228
+
229
+ def show_otp_auth_link?
230
+ super || (otp_exists? && otp_locked_out?)
231
+ end
232
+
233
+ def otp_unlock_data
234
+ @otp_unlock_data ||= otp_unlock_ds.first
235
+ end
236
+
237
+ def otp_unlock_success_cooldown_seconds
238
+ (_otp_interval+(otp_drift||0))*2
239
+ end
240
+
241
+ def otp_unlock_reset
242
+ otp_unlock_ds.delete
243
+ @otp_unlock_data = nil
244
+ end
245
+
246
+ def otp_unlock_ds
247
+ db[otp_unlock_table].where(otp_unlock_id_column=>session_value)
248
+ end
249
+ end
250
+ end
@@ -53,7 +53,7 @@ module Rodauth
53
53
  throw_error_reason(:invalid_webauthn_id, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_webauthn_id_message)
54
54
  end
55
55
 
56
- @account = account_ds(account_id).first
56
+ account_from_id(account_id)
57
57
  end
58
58
 
59
59
  def webauthn_login_options?
@@ -0,0 +1,23 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:webauthn_modify_email, :WebauthnModifyEmail) do
5
+ depends :webauthn, :email_base
6
+
7
+ loaded_templates %w'webauthn-authenticator-added-email webauthn-authenticator-removed-email'
8
+ email :webauthn_authenticator_added, 'WebAuthn Authenticator Added', :translatable=>true
9
+ email :webauthn_authenticator_removed, 'WebAuthn Authenticator Removed', :translatable=>true
10
+
11
+ private
12
+
13
+ def after_webauthn_setup
14
+ super
15
+ send_webauthn_authenticator_added_email
16
+ end
17
+
18
+ def after_webauthn_remove
19
+ super
20
+ send_webauthn_authenticator_removed_email
21
+ end
22
+ end
23
+ 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 = 35
9
+ MINOR = 36
10
10
 
11
11
  # The patch version of Rodauth, updated only for bug fixes from the last
12
12
  # feature release.
@@ -0,0 +1,2 @@
1
+ Someone (hopefully you) has disabled TOTP authentication for the account
2
+ associated to this email address.
@@ -0,0 +1,9 @@
1
+ TOTP authentication has been locked out on your account due to too many
2
+ consecutive authentication failures. You can attempt to unlock TOTP
3
+ authentication for your account by consecutively authenticating via
4
+ TOTP multiple times.
5
+
6
+ If you did not initiate the TOTP authentication failures that
7
+ caused TOTP authentication to be locked out, that means someone already
8
+ has partial access to your account, but is unable to use TOTP
9
+ authentication to fully authenticate themselves.
@@ -0,0 +1,2 @@
1
+ Someone (hopefully you) has setup TOTP authentication for the account
2
+ associated to this email address.
@@ -0,0 +1,8 @@
1
+ Someone (hopefully you) attempted to unlock TOTP authentication for the
2
+ account associated to this email address, but failed as the
3
+ authentication code submitted was not correct.
4
+
5
+ If you did not initiate the TOTP authentication failure that generated
6
+ this email, that means someone already has partial access to your
7
+ account, but is unable to use TOTP authentication to fully authenticate
8
+ themselves.
@@ -0,0 +1,5 @@
1
+ <p>#{rodauth.otp_unlock_consecutive_successes_label}: #{rodauth.otp_unlock_num_successes}</p>
2
+ <p>#{rodauth.otp_unlock_required_consecutive_successes_label}: #{rodauth.otp_unlock_auths_required}</p>
3
+ <p>#{rodauth.otp_unlock_next_auth_attempt_label}: #{rodauth.otp_unlock_next_auth_attempt_after.strftime(rodauth.strftime_format)}</p>
4
+ <p>#{rodauth.otp_unlock_next_auth_attempt_refresh_label}</p>
5
+ #{rodauth.otp_unlock_refresh_tag}
@@ -0,0 +1,11 @@
1
+ <form method="post" class="rodauth" role="form" id="otp-unlock-form">
2
+ #{rodauth.otp_unlock_additional_form_tags}
3
+ #{rodauth.csrf_tag}
4
+ <p>#{rodauth.otp_unlock_consecutive_successes_label}: #{rodauth.otp_unlock_num_successes}</p>
5
+ <p>#{rodauth.otp_unlock_required_consecutive_successes_label}: #{rodauth.otp_unlock_auths_required}</p>
6
+ <p>#{rodauth.otp_unlock_next_auth_deadline_label}: #{rodauth.otp_unlock_deadline.strftime(rodauth.strftime_format)}</p>
7
+ #{rodauth.render('otp-auth-code-field')}
8
+ #{rodauth.button(rodauth.otp_unlock_button)}
9
+ </form>
10
+
11
+ #{rodauth.otp_unlock_form_footer}
@@ -0,0 +1,2 @@
1
+ Someone (hopefully you) has unlocked TOTP authentication for the account
2
+ associated to this email address.
@@ -0,0 +1,3 @@
1
+ Someone (hopefully you) has added a WebAuthn authenticator to the
2
+ account associated to this email address. There are now #{rodauth.account_webauthn_ids.length} WebAuthn
3
+ authenticator(s) with access to the account.
@@ -0,0 +1,3 @@
1
+ Someone (hopefully you) has removed a WebAuthn authenticator from the
2
+ account associated to this email address. There are now #{rodauth.account_webauthn_ids.length} WebAuthn
3
+ authenticator(s) with access to the account.
@@ -4,7 +4,7 @@
4
4
  #{rodauth.render('password-field') if rodauth.two_factor_modifications_require_password?}
5
5
  <fieldset class="form-group mb-3">
6
6
  #{(usage = rodauth.account_webauthn_usage; last_id = usage.keys.last; usage;).map do |id, last_use|
7
- last_use = last_use.strftime("%F %T") if last_use.is_a?(Time)
7
+ last_use = last_use.strftime(rodauth.strftime_format) if last_use.is_a?(Time)
8
8
  input = rodauth.input_field_string(rodauth.webauthn_remove_param, "webauthn-remove-#{h id}", :type=>'radio', :class=>"form-check-input", :skip_error_message=>true, :value=>id, :required=>false)
9
9
  label = "<label class=\"rodauth-webauthn-id form-check-label\" for=\"webauthn-remove-#{h id}\">Last Use: #{last_use}</label>"
10
10
  error = rodauth.formatted_field_error(rodauth.webauthn_remove_param) if id == last_id
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.35.0
4
+ version: 2.36.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: 2024-05-28 00:00:00.000000000 Z
11
+ date: 2024-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -278,6 +278,9 @@ extra_rdoc_files:
278
278
  - doc/login_password_requirements_base.rdoc
279
279
  - doc/logout.rdoc
280
280
  - doc/otp.rdoc
281
+ - doc/otp_lockout_email.rdoc
282
+ - doc/otp_modify_email.rdoc
283
+ - doc/otp_unlock.rdoc
281
284
  - doc/password_complexity.rdoc
282
285
  - doc/password_expiration.rdoc
283
286
  - doc/password_grace_period.rdoc
@@ -298,6 +301,7 @@ extra_rdoc_files:
298
301
  - doc/webauthn.rdoc
299
302
  - doc/webauthn_autofill.rdoc
300
303
  - doc/webauthn_login.rdoc
304
+ - doc/webauthn_modify_email.rdoc
301
305
  - doc/webauthn_verify_account.rdoc
302
306
  - doc/release_notes/1.0.0.txt
303
307
  - doc/release_notes/1.1.0.txt
@@ -353,6 +357,7 @@ extra_rdoc_files:
353
357
  - doc/release_notes/2.33.0.txt
354
358
  - doc/release_notes/2.34.0.txt
355
359
  - doc/release_notes/2.35.0.txt
360
+ - doc/release_notes/2.36.0.txt
356
361
  - doc/release_notes/2.4.0.txt
357
362
  - doc/release_notes/2.5.0.txt
358
363
  - doc/release_notes/2.6.0.txt
@@ -416,6 +421,9 @@ files:
416
421
  - doc/login_password_requirements_base.rdoc
417
422
  - doc/logout.rdoc
418
423
  - doc/otp.rdoc
424
+ - doc/otp_lockout_email.rdoc
425
+ - doc/otp_modify_email.rdoc
426
+ - doc/otp_unlock.rdoc
419
427
  - doc/password_complexity.rdoc
420
428
  - doc/password_expiration.rdoc
421
429
  - doc/password_grace_period.rdoc
@@ -476,6 +484,7 @@ files:
476
484
  - doc/release_notes/2.33.0.txt
477
485
  - doc/release_notes/2.34.0.txt
478
486
  - doc/release_notes/2.35.0.txt
487
+ - doc/release_notes/2.36.0.txt
479
488
  - doc/release_notes/2.4.0.txt
480
489
  - doc/release_notes/2.5.0.txt
481
490
  - doc/release_notes/2.6.0.txt
@@ -496,6 +505,7 @@ files:
496
505
  - doc/webauthn.rdoc
497
506
  - doc/webauthn_autofill.rdoc
498
507
  - doc/webauthn_login.rdoc
508
+ - doc/webauthn_modify_email.rdoc
499
509
  - doc/webauthn_verify_account.rdoc
500
510
  - javascript/webauthn_auth.js
501
511
  - javascript/webauthn_autofill.js
@@ -528,6 +538,9 @@ files:
528
538
  - lib/rodauth/features/login_password_requirements_base.rb
529
539
  - lib/rodauth/features/logout.rb
530
540
  - lib/rodauth/features/otp.rb
541
+ - lib/rodauth/features/otp_lockout_email.rb
542
+ - lib/rodauth/features/otp_modify_email.rb
543
+ - lib/rodauth/features/otp_unlock.rb
531
544
  - lib/rodauth/features/password_complexity.rb
532
545
  - lib/rodauth/features/password_expiration.rb
533
546
  - lib/rodauth/features/password_grace_period.rb
@@ -548,6 +561,7 @@ files:
548
561
  - lib/rodauth/features/webauthn.rb
549
562
  - lib/rodauth/features/webauthn_autofill.rb
550
563
  - lib/rodauth/features/webauthn_login.rb
564
+ - lib/rodauth/features/webauthn_modify_email.rb
551
565
  - lib/rodauth/features/webauthn_verify_account.rb
552
566
  - lib/rodauth/migrations.rb
553
567
  - lib/rodauth/version.rb
@@ -573,7 +587,14 @@ files:
573
587
  - templates/otp-auth-code-field.str
574
588
  - templates/otp-auth.str
575
589
  - templates/otp-disable.str
590
+ - templates/otp-disabled-email.str
591
+ - templates/otp-locked-out-email.str
592
+ - templates/otp-setup-email.str
576
593
  - templates/otp-setup.str
594
+ - templates/otp-unlock-failed-email.str
595
+ - templates/otp-unlock-not-available.str
596
+ - templates/otp-unlock.str
597
+ - templates/otp-unlocked-email.str
577
598
  - templates/password-changed-email.str
578
599
  - templates/password-confirm-field.str
579
600
  - templates/password-field.str
@@ -602,6 +623,8 @@ files:
602
623
  - templates/verify-login-change-email.str
603
624
  - templates/verify-login-change.str
604
625
  - templates/webauthn-auth.str
626
+ - templates/webauthn-authenticator-added-email.str
627
+ - templates/webauthn-authenticator-removed-email.str
605
628
  - templates/webauthn-autofill.str
606
629
  - templates/webauthn-remove.str
607
630
  - templates/webauthn-setup.str
@@ -636,7 +659,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
636
659
  - !ruby/object:Gem::Version
637
660
  version: '0'
638
661
  requirements: []
639
- rubygems_version: 3.5.9
662
+ rubygems_version: 3.5.11
640
663
  signing_key:
641
664
  specification_version: 4
642
665
  summary: Authentication and Account Management Framework for Rack Applications