rodauth 2.34.0 → 2.36.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +26 -0
- data/README.rdoc +22 -5
- data/doc/base.rdoc +2 -0
- data/doc/guides/render_confirmation.rdoc +1 -1
- data/doc/login.rdoc +2 -2
- data/doc/otp_lockout_email.rdoc +30 -0
- data/doc/otp_modify_email.rdoc +19 -0
- data/doc/otp_unlock.rdoc +58 -0
- data/doc/release_notes/2.35.0.txt +22 -0
- data/doc/release_notes/2.36.0.txt +35 -0
- data/doc/webauthn_modify_email.rdoc +19 -0
- data/lib/rodauth/features/active_sessions.rb +1 -1
- data/lib/rodauth/features/base.rb +17 -1
- data/lib/rodauth/features/email_base.rb +1 -3
- data/lib/rodauth/features/internal_request.rb +17 -9
- data/lib/rodauth/features/json.rb +26 -0
- data/lib/rodauth/features/otp.rb +10 -2
- data/lib/rodauth/features/otp_lockout_email.rb +42 -0
- data/lib/rodauth/features/otp_modify_email.rb +23 -0
- data/lib/rodauth/features/otp_unlock.rb +250 -0
- data/lib/rodauth/features/webauthn_autofill.rb +1 -1
- data/lib/rodauth/features/webauthn_modify_email.rb +23 -0
- data/lib/rodauth/version.rb +1 -1
- data/lib/rodauth.rb +1 -1
- data/templates/otp-disabled-email.str +2 -0
- data/templates/otp-locked-out-email.str +9 -0
- data/templates/otp-setup-email.str +2 -0
- data/templates/otp-unlock-failed-email.str +8 -0
- data/templates/otp-unlock-not-available.str +5 -0
- data/templates/otp-unlock.str +11 -0
- data/templates/otp-unlocked-email.str +2 -0
- data/templates/webauthn-authenticator-added-email.str +3 -0
- data/templates/webauthn-authenticator-removed-email.str +3 -0
- data/templates/webauthn-remove.str +1 -1
- metadata +28 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4daf43beb9b2af129683b299f1e455e5b38fa925b1cc020ec79ae951f56f292b
|
4
|
+
data.tar.gz: a694fe203f76512691004d1138ad03cab7699d317a0a658ae6ac51732ff22b36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aef00a1d31a310ea0061f54ebcb4044ccdcc7106d2f07074f4585ac5406776784bef9c698befa050b374d4dd37dd2495d3a3e4dcd6854e387fc8442901f3f227
|
7
|
+
data.tar.gz: 85d9837f21b320f2bec1241f07e22ff150b49192017cb1ea20d2863fce93b547d5271cafe30b08baeaa582fd4f40b715ce488d9fb19bd7fe2cc43a2f565b5266
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,29 @@
|
|
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)
|
18
|
+
|
19
|
+
* Handle internal_request_configuration blocks in superclasses (jeremyevans, bjeanes)
|
20
|
+
|
21
|
+
* Avoid unused block warning on Ruby 3.4 (jeremyevans)
|
22
|
+
|
23
|
+
* Add throw_rodauth_error method to make it possible to throw without setting a field error (jf) (#418)
|
24
|
+
|
25
|
+
* Support logging out all active sessions for a loaded account that is not logged in (such as after resetting password) (enescakir) (#401)
|
26
|
+
|
1
27
|
=== 2.34.0 (2024-03-22)
|
2
28
|
|
3
29
|
* Add remove_all_active_sessions_except_current method for removing current active session (jeremyevans) (#395)
|
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
|
-
|
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
|
|
@@ -338,10 +342,10 @@ PostgreSQL sets up new tables in the public schema by default.
|
|
338
342
|
If you would like to use separate schemas per user, you can do:
|
339
343
|
|
340
344
|
psql -U postgres -c "DROP SCHEMA public;" ${DATABASE_NAME}
|
341
|
-
psql -U postgres -c "CREATE SCHEMA
|
342
|
-
psql -U postgres -c "CREATE SCHEMA
|
343
|
-
psql -U postgres -c "GRANT USAGE ON SCHEMA ${DATABASE_NAME}
|
344
|
-
psql -U postgres -c "GRANT USAGE ON SCHEMA ${DATABASE_NAME}_password TO ${DATABASE_NAME};"
|
345
|
+
psql -U postgres -c "CREATE SCHEMA AUTHORIZATION ${DATABASE_NAME};" ${DATABASE_NAME}
|
346
|
+
psql -U postgres -c "CREATE SCHEMA AUTHORIZATION ${DATABASE_NAME}_password;" ${DATABASE_NAME}
|
347
|
+
psql -U postgres -c "GRANT USAGE ON SCHEMA ${DATABASE_NAME} TO ${DATABASE_NAME}_password;" ${DATABASE_NAME}
|
348
|
+
psql -U postgres -c "GRANT USAGE ON SCHEMA ${DATABASE_NAME}_password TO ${DATABASE_NAME};" ${DATABASE_NAME}
|
345
349
|
|
346
350
|
You'll need to modify the code to load the extension to specify the schema:
|
347
351
|
|
@@ -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.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
= Render confirmation view
|
2
2
|
|
3
|
-
Most Rodauth actions redirect and display a flash notice after they're
|
3
|
+
Most Rodauth actions redirect and display a flash notice after they're successfully performed. However, in some cases you may wish to render a view confirming that the action was successful, for nicer user experience.
|
4
4
|
|
5
5
|
For example, when the user creates an account, you might render a page with a call to action to verify their account. Assuming you've created an +account_created+ view template alongside your other Rodauth templates, you can configure the following:
|
6
6
|
|
data/doc/login.rdoc
CHANGED
@@ -14,9 +14,8 @@ location.
|
|
14
14
|
|
15
15
|
login_additional_form_tags :: HTML fragment containing additional form tags to use on the login form.
|
16
16
|
login_button :: The text to use for the login button.
|
17
|
-
login_error_flash :: The flash error to show for an
|
17
|
+
login_error_flash :: The flash error to show for an unsuccessful login.
|
18
18
|
login_error_status :: The response status to use when using an invalid login or password to login, 401 by default.
|
19
|
-
login_form_footer :: A message to display after the login form.
|
20
19
|
login_form_footer_links :: An array of entries for links to show on the login page. Each entry is an array of three elements, sort order (integer), link href, and link text.
|
21
20
|
login_form_footer_links_heading :: A heading to show before the login form footer links.
|
22
21
|
login_notice_flash :: The flash notice to show after successful login.
|
@@ -33,6 +32,7 @@ use_multi_phase_login? :: Whether to ask for login first, and only ask for passw
|
|
33
32
|
== Auth Methods
|
34
33
|
|
35
34
|
before_login_route :: Run arbitrary code before handling a login route.
|
35
|
+
login_form_footer :: A message to display after the login form.
|
36
36
|
login_response :: Return a response after a successful login. By default, redirects to +login_redirect+ (or the requested location if +login_return_to_requested_location?+ is true).
|
37
37
|
login_return_to_requested_location_path :: If +login_return_to_requested_location?+ is true, the path to use as the requested location. By default, uses the full path of the request for GET requests, and is nil for non-GET requests (in which case the default +login_redirect+ will be used).
|
38
38
|
login_view :: The HTML to use for the login form.
|
@@ -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.
|
data/doc/otp_unlock.rdoc
ADDED
@@ -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,22 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* A throw_rodauth_error method has been added to make it easier
|
4
|
+
for external extensions to throw the expected error value without
|
5
|
+
setting a field error.
|
6
|
+
|
7
|
+
= Improvements
|
8
|
+
|
9
|
+
* If an account is not currently logged in, but Rodauth knows the
|
10
|
+
related account id, remove_all_active_sessions and related
|
11
|
+
methods in the active_sessions plugin will now remove sessions
|
12
|
+
for the related account.
|
13
|
+
|
14
|
+
* When using the internal_request feature and subclasses,
|
15
|
+
internal_request_configuration blocks in superclasses are now
|
16
|
+
respected when creating the internal request class for a
|
17
|
+
subclass. When creating the internal request in the subclass,
|
18
|
+
this behaves as if all internal_request_configuration blocks
|
19
|
+
were specified directly in the subclass.
|
20
|
+
|
21
|
+
* An ignored block warning on Ruby 3.4 is now avoided by having
|
22
|
+
Rodauth.load_dependencies accept a block.
|
@@ -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
|
@@ -643,9 +649,13 @@ module Rodauth
|
|
643
649
|
set_response_error_status(status)
|
644
650
|
end
|
645
651
|
|
652
|
+
def throw_rodauth_error
|
653
|
+
throw :rodauth_error
|
654
|
+
end
|
655
|
+
|
646
656
|
def throw_error(field, error)
|
647
657
|
set_field_error(field, error)
|
648
|
-
|
658
|
+
throw_rodauth_error
|
649
659
|
end
|
650
660
|
|
651
661
|
def throw_error_status(status, field, error)
|
@@ -735,6 +745,12 @@ module Rodauth
|
|
735
745
|
ds.first
|
736
746
|
end
|
737
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
|
+
|
738
754
|
def hmac_secret_rotation?
|
739
755
|
hmac_secret && hmac_old_secret && hmac_secret != hmac_old_secret
|
740
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
|
-
|
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
|
-
|
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 =
|
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,
|
@@ -384,16 +382,26 @@ module Rodauth
|
|
384
382
|
|
385
383
|
return if is_a?(InternalRequestMethods)
|
386
384
|
|
385
|
+
superklasses = []
|
386
|
+
superklass = self.class
|
387
|
+
until superklass == Rodauth::Auth
|
388
|
+
superklasses << superklass
|
389
|
+
superklass = superklass.superclass
|
390
|
+
end
|
391
|
+
|
387
392
|
klass = self.class
|
388
393
|
internal_class = Class.new(klass)
|
389
394
|
internal_class.instance_variable_set(:@configuration_name, klass.configuration_name)
|
395
|
+
configuration = internal_class.configuration
|
390
396
|
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
397
|
+
superklasses.reverse_each do |superklass|
|
398
|
+
if blocks = superklass.instance_variable_get(:@internal_request_configuration_blocks)
|
399
|
+
blocks.each do |block|
|
400
|
+
configuration.instance_exec(&block)
|
401
|
+
end
|
395
402
|
end
|
396
403
|
end
|
404
|
+
|
397
405
|
internal_class.send(:extend, InternalRequestClassMethods)
|
398
406
|
internal_class.send(:include, InternalRequestMethods)
|
399
407
|
internal_class.allocate.post_configure
|
@@ -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?
|
data/lib/rodauth/features/otp.rb
CHANGED
@@ -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=>
|
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
|
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
|
-
|
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
|
data/lib/rodauth/version.rb
CHANGED
data/lib/rodauth.rb
CHANGED
@@ -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,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}
|
@@ -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(
|
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.
|
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-
|
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
|
@@ -352,6 +356,8 @@ extra_rdoc_files:
|
|
352
356
|
- doc/release_notes/2.32.0.txt
|
353
357
|
- doc/release_notes/2.33.0.txt
|
354
358
|
- doc/release_notes/2.34.0.txt
|
359
|
+
- doc/release_notes/2.35.0.txt
|
360
|
+
- doc/release_notes/2.36.0.txt
|
355
361
|
- doc/release_notes/2.4.0.txt
|
356
362
|
- doc/release_notes/2.5.0.txt
|
357
363
|
- doc/release_notes/2.6.0.txt
|
@@ -415,6 +421,9 @@ files:
|
|
415
421
|
- doc/login_password_requirements_base.rdoc
|
416
422
|
- doc/logout.rdoc
|
417
423
|
- doc/otp.rdoc
|
424
|
+
- doc/otp_lockout_email.rdoc
|
425
|
+
- doc/otp_modify_email.rdoc
|
426
|
+
- doc/otp_unlock.rdoc
|
418
427
|
- doc/password_complexity.rdoc
|
419
428
|
- doc/password_expiration.rdoc
|
420
429
|
- doc/password_grace_period.rdoc
|
@@ -474,6 +483,8 @@ files:
|
|
474
483
|
- doc/release_notes/2.32.0.txt
|
475
484
|
- doc/release_notes/2.33.0.txt
|
476
485
|
- doc/release_notes/2.34.0.txt
|
486
|
+
- doc/release_notes/2.35.0.txt
|
487
|
+
- doc/release_notes/2.36.0.txt
|
477
488
|
- doc/release_notes/2.4.0.txt
|
478
489
|
- doc/release_notes/2.5.0.txt
|
479
490
|
- doc/release_notes/2.6.0.txt
|
@@ -494,6 +505,7 @@ files:
|
|
494
505
|
- doc/webauthn.rdoc
|
495
506
|
- doc/webauthn_autofill.rdoc
|
496
507
|
- doc/webauthn_login.rdoc
|
508
|
+
- doc/webauthn_modify_email.rdoc
|
497
509
|
- doc/webauthn_verify_account.rdoc
|
498
510
|
- javascript/webauthn_auth.js
|
499
511
|
- javascript/webauthn_autofill.js
|
@@ -526,6 +538,9 @@ files:
|
|
526
538
|
- lib/rodauth/features/login_password_requirements_base.rb
|
527
539
|
- lib/rodauth/features/logout.rb
|
528
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
|
529
544
|
- lib/rodauth/features/password_complexity.rb
|
530
545
|
- lib/rodauth/features/password_expiration.rb
|
531
546
|
- lib/rodauth/features/password_grace_period.rb
|
@@ -546,6 +561,7 @@ files:
|
|
546
561
|
- lib/rodauth/features/webauthn.rb
|
547
562
|
- lib/rodauth/features/webauthn_autofill.rb
|
548
563
|
- lib/rodauth/features/webauthn_login.rb
|
564
|
+
- lib/rodauth/features/webauthn_modify_email.rb
|
549
565
|
- lib/rodauth/features/webauthn_verify_account.rb
|
550
566
|
- lib/rodauth/migrations.rb
|
551
567
|
- lib/rodauth/version.rb
|
@@ -571,7 +587,14 @@ files:
|
|
571
587
|
- templates/otp-auth-code-field.str
|
572
588
|
- templates/otp-auth.str
|
573
589
|
- templates/otp-disable.str
|
590
|
+
- templates/otp-disabled-email.str
|
591
|
+
- templates/otp-locked-out-email.str
|
592
|
+
- templates/otp-setup-email.str
|
574
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
|
575
598
|
- templates/password-changed-email.str
|
576
599
|
- templates/password-confirm-field.str
|
577
600
|
- templates/password-field.str
|
@@ -600,6 +623,8 @@ files:
|
|
600
623
|
- templates/verify-login-change-email.str
|
601
624
|
- templates/verify-login-change.str
|
602
625
|
- templates/webauthn-auth.str
|
626
|
+
- templates/webauthn-authenticator-added-email.str
|
627
|
+
- templates/webauthn-authenticator-removed-email.str
|
603
628
|
- templates/webauthn-autofill.str
|
604
629
|
- templates/webauthn-remove.str
|
605
630
|
- templates/webauthn-setup.str
|
@@ -634,7 +659,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
634
659
|
- !ruby/object:Gem::Version
|
635
660
|
version: '0'
|
636
661
|
requirements: []
|
637
|
-
rubygems_version: 3.5.
|
662
|
+
rubygems_version: 3.5.11
|
638
663
|
signing_key:
|
639
664
|
specification_version: 4
|
640
665
|
summary: Authentication and Account Management Framework for Rack Applications
|