rodauth 2.29.0 → 2.31.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +20 -0
- data/README.rdoc +22 -0
- data/doc/error_reasons.rdoc +1 -0
- data/doc/guides/registration_field.rdoc +1 -1
- data/doc/guides/render_confirmation.rdoc +17 -0
- data/doc/internal_request.rdoc +76 -0
- data/doc/json.rdoc +1 -0
- data/doc/jwt.rdoc +6 -1
- data/doc/jwt_refresh.rdoc +1 -1
- data/doc/release_notes/2.30.0.txt +15 -0
- data/doc/release_notes/2.31.0.txt +47 -0
- data/doc/webauthn_autofill.rdoc +19 -0
- data/doc/webauthn_login.rdoc +2 -1
- data/doc/webauthn_verify_account.rdoc +1 -1
- data/javascript/webauthn_autofill.js +38 -0
- data/lib/rodauth/features/argon2.rb +31 -12
- data/lib/rodauth/features/base.rb +4 -0
- data/lib/rodauth/features/internal_request.rb +47 -1
- data/lib/rodauth/features/json.rb +7 -2
- data/lib/rodauth/features/remember.rb +6 -2
- data/lib/rodauth/features/webauthn.rb +36 -22
- data/lib/rodauth/features/webauthn_autofill.rb +64 -0
- data/lib/rodauth/features/webauthn_login.rb +28 -1
- data/lib/rodauth/version.rb +1 -1
- data/templates/login-field.str +1 -1
- data/templates/webauthn-autofill.str +9 -0
- metadata +13 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 34f1a4bc530025482c6550375de67bf9dcbe4ea75ca9f4f775d179805629e115
|
4
|
+
data.tar.gz: 75339b39caf60463c076459e1fc21fd4d2291e4bc756c4cc5107edf7462c0186
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e66e012fca7c52bd25206fd7711921148e8372a249047687e5429074dd9ea64461dcbbda1a0412b16920270814fef9fa5954dda701ae30af66e7a78a7975855
|
7
|
+
data.tar.gz: f7c9ec1aac7150a3fa1b4d2af3e8278f16ef3982d5cd84b6b2984d41b5f5b70f60e9f7c0e099d338055201d820bd099093609b2683086c5a8ec9fdf53dee2e58
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,23 @@
|
|
1
|
+
=== 2.31.0 (2023-08-22)
|
2
|
+
|
3
|
+
* Make clear_session work correctly for internal requests (janko) (#359)
|
4
|
+
|
5
|
+
* Support webauthn_invalid_webauthn_id_message configuration method in the webauthn_autofill feature (janko) (#356)
|
6
|
+
|
7
|
+
* Support webauth features in the internal_request feature (janko) (#355)
|
8
|
+
|
9
|
+
* Allow WebAuthn login to count for two factors if user verification is provided (janko) (#354)
|
10
|
+
|
11
|
+
* Allow explicit use of p_cost in argon2 feature if using argon2 2.1+ (estebanz01) (#353)
|
12
|
+
|
13
|
+
* Add json_response_error? configuration method to json feature, for whether response indicates an error (opya) (#340)
|
14
|
+
|
15
|
+
=== 2.30.0 (2023-05-22)
|
16
|
+
|
17
|
+
* Make load_memory in the remember feature not raise NoMethodError if logged in when the account no longer exists (jeremyevans) (#331)
|
18
|
+
|
19
|
+
* Add webauthn_autofill feature, for supporting autofill of webauthn information on the login form (janko) (#328)
|
20
|
+
|
1
21
|
=== 2.29.0 (2023-03-22)
|
2
22
|
|
3
23
|
* Support :render=>false plugin options (davekaro) (#319)
|
data/README.rdoc
CHANGED
@@ -37,6 +37,7 @@ HTML and JSON API for all supported features.
|
|
37
37
|
* WebAuthn (Multifactor authentication via WebAuthn)
|
38
38
|
* WebAuthn Login (Passwordless login via WebAuthn)
|
39
39
|
* WebAuthn Verify Account (Passwordless WebAuthn Setup)
|
40
|
+
* WebAuthn Autofill (Autofill WebAuthn credentials on login)
|
40
41
|
* OTP (Multifactor authentication via TOTP)
|
41
42
|
* Recovery Codes (Multifactor authentication via backup codes)
|
42
43
|
* SMS Codes (Multifactor authentication via SMS)
|
@@ -97,6 +98,9 @@ rqrcode :: Used by the otp feature
|
|
97
98
|
jwt :: Used by the jwt feature
|
98
99
|
webauthn :: Used by the webauthn feature
|
99
100
|
|
101
|
+
You can use <tt>gem install --development rodauth</tt> to install
|
102
|
+
the development dependencies in order to run tests.
|
103
|
+
|
100
104
|
== Security
|
101
105
|
|
102
106
|
=== Password Hash Access Via Database Functions
|
@@ -318,6 +322,16 @@ bad idea), you don't need to use the PostgreSQL citext extension. Just
|
|
318
322
|
remember to modify the migration below to use +String+ instead of +citext+
|
319
323
|
for the email in that case.
|
320
324
|
|
325
|
+
=== Grant schema rights (PostgreSQL 15+)
|
326
|
+
|
327
|
+
PostgreSQL 15 changed default database security so that only the database
|
328
|
+
owner has writable access to the public schema. Rodauth expects the
|
329
|
+
+ph+ account to have writable access to the public schema when setting
|
330
|
+
things up. Temporarily grant that access (it will be revoked after the
|
331
|
+
migation has run)
|
332
|
+
|
333
|
+
psql -U postgres -c "GRANT CREATE ON SCHEMA public TO ${DATABASE_NAME}_password" ${DATABASE_NAME}
|
334
|
+
|
321
335
|
=== Using non-default schema
|
322
336
|
|
323
337
|
PostgreSQL sets up new tables in the public schema by default.
|
@@ -739,6 +753,13 @@ One thing to notice in the above migrations is that Rodauth uses additional
|
|
739
753
|
tables for additional features, instead of additional columns in a single
|
740
754
|
table.
|
741
755
|
|
756
|
+
=== Revoking schema rights (PostgreSQL 15+)
|
757
|
+
|
758
|
+
If you explicit granted access to the public schema before running the
|
759
|
+
migration, revoke it afterward:
|
760
|
+
|
761
|
+
psql -U postgres -c "REVOKE CREATE ON SCHEMA public FROM ${DATABASE_NAME}_password" ${DATABASE_NAME}
|
762
|
+
|
742
763
|
=== Locking Down (PostgreSQL only)
|
743
764
|
|
744
765
|
After running the migrations, you can increase security slightly by making
|
@@ -915,6 +936,7 @@ view the appropriate file in the doc directory.
|
|
915
936
|
* {Verify Account Grace Period}[rdoc-ref:doc/verify_account_grace_period.rdoc]
|
916
937
|
* {Verify Login Change}[rdoc-ref:doc/verify_login_change.rdoc]
|
917
938
|
* {WebAuthn}[rdoc-ref:doc/webauthn.rdoc]
|
939
|
+
* {WebAuthn Autofill}[rdoc-ref:doc/webauthn_autofill.rdoc]
|
918
940
|
* {WebAuthn Login}[rdoc-ref:doc/webauthn_login.rdoc]
|
919
941
|
* {WebAuthn Verify Account}[rdoc-ref:doc/webauthn_verify_account.rdoc]
|
920
942
|
|
data/doc/error_reasons.rdoc
CHANGED
@@ -38,6 +38,7 @@ Rodauth will call +set_error_reason+ with:
|
|
38
38
|
* :invalid_verify_account_key
|
39
39
|
* :invalid_verify_login_change_key
|
40
40
|
* :invalid_webauthn_auth_param
|
41
|
+
* :invalid_webauthn_id
|
41
42
|
* :invalid_webauthn_remove_param
|
42
43
|
* :invalid_webauthn_setup_param
|
43
44
|
* :invalid_webauthn_sign_count
|
@@ -0,0 +1,17 @@
|
|
1
|
+
= Render confirmation view
|
2
|
+
|
3
|
+
Most Rodauth actions redirect and display a flash notice after they're succesfully performed. However, in some cases you may wish to render a view confirming that the action was succesful, for nicer user experience.
|
4
|
+
|
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
|
+
|
7
|
+
after_create_account do
|
8
|
+
# render "account_created" view template with page title of "Account created!"
|
9
|
+
return_response view("account_created", "Account created!")
|
10
|
+
end
|
11
|
+
|
12
|
+
Similarly, when the user has requested a password reset, you can render a page telling them to check their email:
|
13
|
+
|
14
|
+
after_reset_password_request do
|
15
|
+
# render "password_reset_sent" view template with page title of "Password sent!"
|
16
|
+
return_response view("password_reset_sent", "Password sent!")
|
17
|
+
end
|
data/doc/internal_request.rdoc
CHANGED
@@ -461,3 +461,79 @@ account login, before the change.
|
|
461
461
|
|
462
462
|
Options:
|
463
463
|
+:verify_login_change_key+ :: The verify login change key for the account. This allows verifying login changes by key, without knowing the account id or login.
|
464
|
+
|
465
|
+
=== WebAuthn
|
466
|
+
|
467
|
+
==== webauthn_setup_params (requires account)
|
468
|
+
|
469
|
+
The +webauthn_setup_params+ method returns a hash with +:webauthn_setup+,
|
470
|
+
+:webauthn_setup_challenge+ and +:webauthn_setup_challenge_hmac+ keys.
|
471
|
+
|
472
|
+
The +:webauthn_setup+ options should be provided to the client for WebAuthn
|
473
|
+
registration, while +:webauthn_setup_challenge+ and
|
474
|
+
+webauthn_setup_challenge_hmac+ should be passed to the +webauthn_setup+
|
475
|
+
method.
|
476
|
+
|
477
|
+
==== webauthn_setup (requires account)
|
478
|
+
|
479
|
+
The +webauthn_setup+ method creates a WebAuthn credential for the account.
|
480
|
+
|
481
|
+
Options:
|
482
|
+
+:webauthn_setup+ :: The WebAuthn credential provided by the client during registration.
|
483
|
+
+:webauthn_setup_challenge+ :: The WebAuthn challenge generated for registration.
|
484
|
+
+:webauthn_setup_challenge_hmac+ :: The HMAC of the WebAuthn challenge generated for registration.
|
485
|
+
|
486
|
+
==== webauthn_auth_params (requires account)
|
487
|
+
|
488
|
+
The +webauthn_auth_params+ method returns a hash with +:webauthn_auth+,
|
489
|
+
+:webauthn_auth_challenge+ and +:webauthn_auth_challenge_hmac+ keys.
|
490
|
+
|
491
|
+
The +:webauthn_auth+ options should be provided to the client for WebAuthn
|
492
|
+
authentication, while +:webauthn_auth_challenge+ and
|
493
|
+
+webauthn_auth_challenge_hmac+ should be passed to the +webauthn_auth+ method.
|
494
|
+
|
495
|
+
==== webauthn_auth (requires account)
|
496
|
+
|
497
|
+
The +webauthn_auth+ method determines if the given WebAuthn credential is valid
|
498
|
+
for the account.
|
499
|
+
|
500
|
+
Options:
|
501
|
+
+:webauthn_auth+ :: The WebAuthn credential provided by the client during authentication.
|
502
|
+
+:webauthn_auth_challenge+ :: The WebAuthn challenge generated for authentication.
|
503
|
+
+:webauthn_auth_challenge_hmac+ :: The HMAC of the WebAuthn challenge generated for authentication.
|
504
|
+
|
505
|
+
==== webauthn_remove (requires account)
|
506
|
+
|
507
|
+
The +webauthn_remove+ methods deletes the given WebAuthn credential for the
|
508
|
+
account.
|
509
|
+
|
510
|
+
Options:
|
511
|
+
+:webauthn_remove+ :: The ID of the WebAuthn credential to delete.
|
512
|
+
|
513
|
+
=== WebAuthn Login
|
514
|
+
|
515
|
+
==== webauthn_login_params (requires account or login)
|
516
|
+
|
517
|
+
The +webauthn_login_params+ method returns a hash with +:webauthn_auth+,
|
518
|
+
+:webauthn_auth_challenge+ and +:webauthn_auth_challenge_hmac+ keys.
|
519
|
+
|
520
|
+
The +:webauthn_auth+ options should be provided to the client for WebAuthn
|
521
|
+
authentication, while +:webauthn_auth_challenge+ and
|
522
|
+
+webauthn_auth_challenge_hmac+ should be passed to the +webauthn_login+ method.
|
523
|
+
|
524
|
+
==== webauthn_login (requires account or login)
|
525
|
+
|
526
|
+
The +webauthn_login+ method determines if the given WebAuthn credential is
|
527
|
+
valid for the given account.
|
528
|
+
|
529
|
+
This method will return the account id if the WebAuthn credential is valid.
|
530
|
+
|
531
|
+
Options:
|
532
|
+
+:webauthn_auth+ :: The WebAuthn credential provided by the client during authentication.
|
533
|
+
+:webauthn_auth_challenge+ :: The WebAuthn challenge generated for authentication.
|
534
|
+
+:webauthn_auth_challenge_hmac+ :: The HMAC of the WebAuthn challenge generated for authentication.
|
535
|
+
|
536
|
+
=== WebAuthn Autofill
|
537
|
+
|
538
|
+
Enabling this feature modifies +webauthn_login_params+ and +webauthn_login+
|
539
|
+
methods not to require an account or login.
|
data/doc/json.rdoc
CHANGED
@@ -41,6 +41,7 @@ json_not_accepted_error_message :: The error message to display if +json_check_a
|
|
41
41
|
json_request_content_type_regexp :: The regexp to use to recognize a request as a json request.
|
42
42
|
json_response_content_type :: The content type to set for json responses, <tt>application/json</tt> by default.
|
43
43
|
json_response_custom_error_status? :: Whether to use custom error statuses, instead of always using +json_response_error_status+, true by default, can be set to false for backwards compatibility with Rodauth 1.
|
44
|
+
json_response_error? :: Whether the current JSON response indicates an error. By default, returns whether +json_response_error_key+ is set.
|
44
45
|
json_response_error_key :: The JSON result key containing an error message, +error+ by default.
|
45
46
|
json_response_error_status :: The HTTP status code to use for JSON error responses if not using custom error statuses, 400 by default.
|
46
47
|
json_response_field_error_key :: The JSON result key containing an field error message, <tt>field-error</tt> by default.
|
data/doc/jwt.rdoc
CHANGED
@@ -5,7 +5,7 @@ that ship with Rodauth, using JWT (JSON Web Tokens) to hold the
|
|
5
5
|
session information. It depends on the json feature.
|
6
6
|
|
7
7
|
In order to use this feature, you have to set the +jwt_secret+ configuration
|
8
|
-
option the secret used to cryptographically protect the token.
|
8
|
+
option with the secret used to cryptographically protect the token.
|
9
9
|
|
10
10
|
To use this JSON API, when processing responses for requests to a Rodauth
|
11
11
|
endpoint, check for the Authorization header, and use the value of the
|
@@ -26,6 +26,11 @@ request in your Roda app, you can call the +rodauth.valid_jwt?+ method. If
|
|
26
26
|
+rodauth.valid_jwt?+ returns true, the contents of the jwt can be retrieved
|
27
27
|
from +rodauth.session+.
|
28
28
|
|
29
|
+
Logging the session out does not invalidate the previous JWT token by default.
|
30
|
+
If you would like this behavior, you can use the active_sessions feature, which
|
31
|
+
stores session identifiers in the database and deletes them when the session
|
32
|
+
expires. This provides a whitelist approach of revoking JWT tokens.
|
33
|
+
|
29
34
|
== Auth Value Methods
|
30
35
|
|
31
36
|
invalid_jwt_format_error_message :: The error message to use when a JWT with an invalid format is submitted in the Authorization header.
|
data/doc/jwt_refresh.rdoc
CHANGED
@@ -7,7 +7,7 @@ When this feature is used, the access and refresh token are provided
|
|
7
7
|
at login in the response body (the access token is still provided in the Authorization
|
8
8
|
header), and for any subsequent POST to <tt>/jwt-refresh</tt>.
|
9
9
|
|
10
|
-
Note that using the refresh token
|
10
|
+
Note that using the refresh token invalidates the token and creates
|
11
11
|
a new access token with an updated lifetime. However, it does not invalidate
|
12
12
|
older access tokens. Older access tokens remain valid until they expire. You
|
13
13
|
can use the active_sessions feature if you want previous access tokens to be invalid
|
@@ -0,0 +1,15 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* A webauthn_autofill feature has been added to allow autofilling
|
4
|
+
webauthn credentials during login (also known as conditional
|
5
|
+
mediation). This allows for easier login using passkeys.
|
6
|
+
This requires a supported browser and operating system on the
|
7
|
+
client side to work.
|
8
|
+
|
9
|
+
= Other Improvements
|
10
|
+
|
11
|
+
* The load_memory method in the remember feature no longer raises
|
12
|
+
a NoMethodError if the there is a remember cookie, the session is
|
13
|
+
already logged in, and the account no longer exists. The
|
14
|
+
load_memory method now removes the remember cookie and clears the
|
15
|
+
session in that case.
|
@@ -0,0 +1,47 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* The internal_request feature now supports WebAuthn, using
|
4
|
+
the following methods:
|
5
|
+
|
6
|
+
* With the webauthn feature:
|
7
|
+
* webauthn_setup_params
|
8
|
+
* webauthn_setup
|
9
|
+
* webauthn_auth_params
|
10
|
+
* webauthn_auth
|
11
|
+
* webauthn_remove
|
12
|
+
|
13
|
+
* With the webauthn_login feature:
|
14
|
+
* webauthn_login_params
|
15
|
+
* webauthn_login
|
16
|
+
|
17
|
+
* A webauthn_login_user_verification_additional_factor? configuration
|
18
|
+
method has been added to the webauthn_login feature. By default,
|
19
|
+
this method returns false. If you configure the method to return
|
20
|
+
true, and the WebAuthn credential provided specifies that it
|
21
|
+
verified the user, then this will treat the user verification as
|
22
|
+
a second factor, so the user will be considered multifactor
|
23
|
+
authenticated after successful login. You should only set this
|
24
|
+
method to true if you consider the WebAuthn user verification
|
25
|
+
strong enough to be a independent factor.
|
26
|
+
|
27
|
+
* A json_response_error? configuration method has been added to the
|
28
|
+
json feature. This should return whether the current response
|
29
|
+
should be treated as an error by the json feature. By default,
|
30
|
+
it is true if json_response_error_key is set in the response,
|
31
|
+
since that is the default place that Rodauth stores errors when
|
32
|
+
using the json feature.
|
33
|
+
|
34
|
+
* A webauthn_invalid_webauthn_id_message configuration method has
|
35
|
+
been added for customizing the error message used for invalid
|
36
|
+
WebAuthn IDs.
|
37
|
+
|
38
|
+
= Other Improvements
|
39
|
+
|
40
|
+
* The argon2 feature now supports setting the Argon2 p_cost if
|
41
|
+
argon2 2.1+ is installed.
|
42
|
+
|
43
|
+
* An :invalid_webauthn_id error reason is now used for invalid
|
44
|
+
WebAuthn IDs.
|
45
|
+
|
46
|
+
* The clear_session method now works as expected for internal
|
47
|
+
requests.
|
@@ -0,0 +1,19 @@
|
|
1
|
+
= Documentation for WebAuthn Autofill Feature
|
2
|
+
|
3
|
+
The webauthn_autofill feature enables autofill UI (aka "conditional mediation")
|
4
|
+
for WebAuthn credentials, logging the user in on selection. It depends on the
|
5
|
+
webauthn_login feature.
|
6
|
+
|
7
|
+
This feature allows generating WebAuthn credential options and submitting a
|
8
|
+
WebAuthn login request without providing a login, which can be used
|
9
|
+
independently from the autofill UI.
|
10
|
+
|
11
|
+
== Auth Value Methods
|
12
|
+
|
13
|
+
webauthn_autofill_js :: The javascript code to execute on the login page to enable autofill UI.
|
14
|
+
webauthn_autofill_js_route :: The route to the webauthn autofill javascript file.
|
15
|
+
webauthn_invalid_webauthn_id_message :: The error message to show when provided WebAuthn ID wasn't found in the database.
|
16
|
+
|
17
|
+
== Auth Methods
|
18
|
+
|
19
|
+
before_webauthn_autofill_js_route :: Run arbitrary code before handling a webauthn autofill javascript route.
|
data/doc/webauthn_login.rdoc
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
= Documentation for WebAuthn Login Feature
|
2
2
|
|
3
|
-
The
|
3
|
+
The webauthn_login feature implements passwordless authentication via
|
4
4
|
WebAuthn. It depends on the login and webauthn features.
|
5
5
|
|
6
6
|
== Auth Value Methods
|
7
7
|
|
8
|
+
webauthn_login_user_verification_additional_factor? :: Whether passwordless login via WebAuthn should consider user verification as 2nd factor when using multifactor authentication, false by default. Setting this to true means that the app trusts the user verification done by the authenticator is strong enough to be considered an additional factor.
|
8
9
|
webauthn_login_error_flash :: The flash error to show if there is a failure during passwordless login via WebAuthn.
|
9
10
|
webauthn_login_failure_redirect :: Whether to redirect if there is a failure during passwordless login via WebAuthn.
|
10
11
|
webauthn_login_route :: The route to the webauthn login action.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
= Documentation for WebAuthn Verify Account Feature
|
2
2
|
|
3
|
-
The
|
3
|
+
The webauthn_verify_account feature implements setting up an WebAuthn authenticator
|
4
4
|
during the account verification process, and making such setup
|
5
5
|
a requirement for account verification. By default, it disables
|
6
6
|
asking for a password during account creation and verification,
|
@@ -0,0 +1,38 @@
|
|
1
|
+
(function() {
|
2
|
+
var pack = function(v) { return btoa(String.fromCharCode.apply(null, new Uint8Array(v))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); };
|
3
|
+
var unpack = function(v) { return Uint8Array.from(atob(v.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)); };
|
4
|
+
var element = document.getElementById('webauthn-login-form');
|
5
|
+
|
6
|
+
if (!window.PublicKeyCredential || !PublicKeyCredential.isConditionalMediationAvailable) return;
|
7
|
+
|
8
|
+
PublicKeyCredential.isConditionalMediationAvailable().then(function(available) {
|
9
|
+
if (!available) return;
|
10
|
+
|
11
|
+
var opts = JSON.parse(element.getAttribute("data-credential-options"));
|
12
|
+
opts.challenge = unpack(opts.challenge);
|
13
|
+
opts.allowCredentials.forEach(function(cred) { cred.id = unpack(cred.id); });
|
14
|
+
|
15
|
+
navigator.credentials.get({mediation: "conditional", publicKey: opts}).then(function(cred) {
|
16
|
+
var rawId = pack(cred.rawId);
|
17
|
+
var authValue = {
|
18
|
+
type: cred.type,
|
19
|
+
id: rawId,
|
20
|
+
rawId: rawId,
|
21
|
+
response: {
|
22
|
+
authenticatorData: pack(cred.response.authenticatorData),
|
23
|
+
clientDataJSON: pack(cred.response.clientDataJSON),
|
24
|
+
signature: pack(cred.response.signature)
|
25
|
+
}
|
26
|
+
};
|
27
|
+
|
28
|
+
if (cred.response.userHandle) {
|
29
|
+
authValue.response.userHandle = pack(cred.response.userHandle);
|
30
|
+
}
|
31
|
+
|
32
|
+
document.getElementById('webauthn-auth').value = JSON.stringify(authValue);
|
33
|
+
|
34
|
+
element.submit();
|
35
|
+
});
|
36
|
+
});
|
37
|
+
})();
|
38
|
+
|
@@ -43,7 +43,7 @@ module Rodauth
|
|
43
43
|
|
44
44
|
def password_hash_cost
|
45
45
|
return super unless use_argon2?
|
46
|
-
argon2_hash_cost
|
46
|
+
argon2_hash_cost
|
47
47
|
end
|
48
48
|
|
49
49
|
def password_hash_match?(hash, password)
|
@@ -60,21 +60,40 @@ module Rodauth
|
|
60
60
|
::Argon2::Password.new(argon2_params).create(password)
|
61
61
|
end
|
62
62
|
|
63
|
-
|
64
|
-
|
63
|
+
if Argon2::VERSION >= '2.1'
|
64
|
+
def extract_password_hash_cost(hash)
|
65
|
+
return super unless argon2_hash_algorithm?(hash )
|
65
66
|
|
66
|
-
|
67
|
-
|
68
|
-
|
67
|
+
/\A\$argon2id\$v=\d+\$m=(\d+),t=(\d+),p=(\d+)/ =~ hash
|
68
|
+
{ t_cost: $2.to_i, m_cost: Math.log2($1.to_i).to_i, p_cost: $3.to_i }
|
69
|
+
end
|
69
70
|
|
70
|
-
|
71
|
-
|
72
|
-
|
71
|
+
if ENV['RACK_ENV'] == 'test'
|
72
|
+
def argon2_hash_cost
|
73
|
+
{ t_cost: 1, m_cost: 3, p_cost: 1 }
|
74
|
+
end
|
75
|
+
# :nocov:
|
76
|
+
else
|
77
|
+
def argon2_hash_cost
|
78
|
+
{ t_cost: 2, m_cost: 16, p_cost: 1 }
|
79
|
+
end
|
73
80
|
end
|
74
|
-
# :nocov:
|
75
81
|
else
|
76
|
-
def
|
77
|
-
|
82
|
+
def extract_password_hash_cost(hash)
|
83
|
+
return super unless argon2_hash_algorithm?(hash )
|
84
|
+
|
85
|
+
/\A\$argon2id\$v=\d+\$m=(\d+),t=(\d+)/ =~ hash
|
86
|
+
{ t_cost: $2.to_i, m_cost: Math.log2($1.to_i).to_i }
|
87
|
+
end
|
88
|
+
|
89
|
+
if ENV['RACK_ENV'] == 'test'
|
90
|
+
def argon2_hash_cost
|
91
|
+
{ t_cost: 1, m_cost: 3 }
|
92
|
+
end
|
93
|
+
else
|
94
|
+
def argon2_hash_cost
|
95
|
+
{ t_cost: 2, m_cost: 16 }
|
96
|
+
end
|
78
97
|
end
|
79
98
|
end
|
80
99
|
# :nocov:
|
@@ -269,6 +269,10 @@ module Rodauth
|
|
269
269
|
Sequel::DATABASES.first or raise "Sequel database connection is missing"
|
270
270
|
end
|
271
271
|
|
272
|
+
def login_field_autocomplete_value
|
273
|
+
login_uses_email? ? "email" : "on"
|
274
|
+
end
|
275
|
+
|
272
276
|
def password_field_autocomplete_value
|
273
277
|
@password_field_autocomplete_value || 'current-password'
|
274
278
|
end
|
@@ -50,6 +50,10 @@ module Rodauth
|
|
50
50
|
@params[k]
|
51
51
|
end
|
52
52
|
|
53
|
+
def clear_session
|
54
|
+
@session.clear
|
55
|
+
end
|
56
|
+
|
53
57
|
def set_error_flash(message)
|
54
58
|
@flash = message
|
55
59
|
_handle_internal_request_error
|
@@ -81,6 +85,24 @@ module Rodauth
|
|
81
85
|
_return_from_internal_request(recovery_codes)
|
82
86
|
end
|
83
87
|
|
88
|
+
def webauthn_setup_view
|
89
|
+
cred = new_webauthn_credential
|
90
|
+
_return_from_internal_request({
|
91
|
+
webauthn_setup: cred.as_json,
|
92
|
+
webauthn_setup_challenge: cred.challenge,
|
93
|
+
webauthn_setup_challenge_hmac: compute_hmac(cred.challenge)
|
94
|
+
})
|
95
|
+
end
|
96
|
+
|
97
|
+
def webauthn_auth_view
|
98
|
+
cred = webauthn_credential_options_for_get
|
99
|
+
_return_from_internal_request({
|
100
|
+
webauthn_auth: cred.as_json,
|
101
|
+
webauthn_auth_challenge: cred.challenge,
|
102
|
+
webauthn_auth_challenge_hmac: compute_hmac(cred.challenge)
|
103
|
+
})
|
104
|
+
end
|
105
|
+
|
84
106
|
def handle_internal_request(meth)
|
85
107
|
catch(:halt) do
|
86
108
|
_around_rodauth do
|
@@ -153,6 +175,11 @@ module Rodauth
|
|
153
175
|
_set_login_param_from_account
|
154
176
|
end
|
155
177
|
|
178
|
+
def before_webauthn_login_route
|
179
|
+
super
|
180
|
+
_set_login_param_from_account
|
181
|
+
end
|
182
|
+
|
156
183
|
def account_from_key(token, status_id=nil)
|
157
184
|
return super unless session_value
|
158
185
|
return unless yield session_value
|
@@ -232,6 +259,25 @@ module Rodauth
|
|
232
259
|
_handle_otp_setup(request)
|
233
260
|
end
|
234
261
|
|
262
|
+
def _handle_webauthn_setup_params(request)
|
263
|
+
request.env['REQUEST_METHOD'] = 'GET'
|
264
|
+
_handle_webauthn_setup(request)
|
265
|
+
end
|
266
|
+
|
267
|
+
def _handle_webauthn_auth_params(request)
|
268
|
+
request.env['REQUEST_METHOD'] = 'GET'
|
269
|
+
_handle_webauthn_auth(request)
|
270
|
+
end
|
271
|
+
|
272
|
+
def _handle_webauthn_login_params(request)
|
273
|
+
_set_login_param_from_account
|
274
|
+
unless webauthn_login_options?
|
275
|
+
raise InternalRequestError, "no login provided" unless param_or_nil(login_param)
|
276
|
+
raise InternalRequestError, "no account for login"
|
277
|
+
end
|
278
|
+
webauthn_auth_view
|
279
|
+
end
|
280
|
+
|
235
281
|
def _predicate_internal_request(meth, request)
|
236
282
|
_return_false_on_error!
|
237
283
|
_set_internal_request_return_value(true)
|
@@ -302,7 +348,7 @@ module Rodauth
|
|
302
348
|
session[rodauth.session_key] = account_id
|
303
349
|
unless authenticated_by = opts.delete(:authenticated_by)
|
304
350
|
authenticated_by = case route
|
305
|
-
when :otp_auth, :sms_request, :sms_auth, :recovery_auth, :valid_otp_auth?, :valid_sms_auth?, :valid_recovery_auth?
|
351
|
+
when :otp_auth, :sms_request, :sms_auth, :recovery_auth, :webauthn_auth, :webauthn_auth_params, :valid_otp_auth?, :valid_sms_auth?, :valid_recovery_auth?
|
306
352
|
['internal1']
|
307
353
|
else
|
308
354
|
['internal1', 'internal2']
|
@@ -22,6 +22,7 @@ module Rodauth
|
|
22
22
|
|
23
23
|
auth_methods(
|
24
24
|
:json_request?,
|
25
|
+
:json_response_error?
|
25
26
|
)
|
26
27
|
|
27
28
|
auth_private_methods :json_response_body
|
@@ -65,6 +66,10 @@ module Rodauth
|
|
65
66
|
return_json_response
|
66
67
|
end
|
67
68
|
|
69
|
+
def json_response_error?
|
70
|
+
!!json_response[json_response_error_key]
|
71
|
+
end
|
72
|
+
|
68
73
|
private
|
69
74
|
|
70
75
|
def before_two_factor_manage_route
|
@@ -116,7 +121,7 @@ module Rodauth
|
|
116
121
|
|
117
122
|
def before_webauthn_login_route
|
118
123
|
super if defined?(super)
|
119
|
-
if use_json? && !param_or_nil(webauthn_auth_param) &&
|
124
|
+
if use_json? && !param_or_nil(webauthn_auth_param) && webauthn_login_options?
|
120
125
|
cred = webauthn_credential_options_for_get
|
121
126
|
json_response[webauthn_auth_param] = cred.as_json
|
122
127
|
json_response[webauthn_auth_challenge_param] = cred.challenge
|
@@ -172,7 +177,7 @@ module Rodauth
|
|
172
177
|
end
|
173
178
|
|
174
179
|
def _return_json_response
|
175
|
-
response.status ||= json_response_error_status if
|
180
|
+
response.status ||= json_response_error_status if json_response_error?
|
176
181
|
response['Content-Type'] ||= json_response_content_type
|
177
182
|
return_response _json_response_body(json_response)
|
178
183
|
end
|
@@ -114,8 +114,12 @@ module Rodauth
|
|
114
114
|
def load_memory
|
115
115
|
if logged_in?
|
116
116
|
if extend_remember_deadline_while_logged_in?
|
117
|
-
account_from_session
|
118
|
-
|
117
|
+
if account_from_session
|
118
|
+
extend_remember_deadline
|
119
|
+
else
|
120
|
+
forget_login
|
121
|
+
clear_session
|
122
|
+
end
|
119
123
|
end
|
120
124
|
elsif account_from_remember_cookie
|
121
125
|
before_load_memory
|
@@ -112,6 +112,12 @@ module Rodauth
|
|
112
112
|
|
113
113
|
def_deprecated_alias :webauthn_credential_options_for_get, :webauth_credential_options_for_get
|
114
114
|
|
115
|
+
internal_request_method :webauthn_setup_params
|
116
|
+
internal_request_method :webauthn_setup
|
117
|
+
internal_request_method :webauthn_auth_params
|
118
|
+
internal_request_method :webauthn_auth
|
119
|
+
internal_request_method :webauthn_remove
|
120
|
+
|
115
121
|
route(:webauthn_auth_js) do |r|
|
116
122
|
before_webauthn_auth_js_route
|
117
123
|
r.get do
|
@@ -320,7 +326,7 @@ module Rodauth
|
|
320
326
|
|
321
327
|
def webauthn_credential_options_for_get
|
322
328
|
WebAuthn::Credential.options_for_get(
|
323
|
-
:allow =>
|
329
|
+
:allow => webauthn_allow,
|
324
330
|
:timeout => webauthn_auth_timeout,
|
325
331
|
:rp_id => webauthn_rp_id,
|
326
332
|
:user_verification => webauthn_user_verification,
|
@@ -336,6 +342,10 @@ module Rodauth
|
|
336
342
|
base_url
|
337
343
|
end
|
338
344
|
|
345
|
+
def webauthn_allow
|
346
|
+
account_webauthn_ids
|
347
|
+
end
|
348
|
+
|
339
349
|
def webauthn_rp_id
|
340
350
|
webauthn_origin.sub(/\Ahttps?:\/\//, '').sub(/:\d+\z/, '')
|
341
351
|
end
|
@@ -453,21 +463,8 @@ module Rodauth
|
|
453
463
|
end
|
454
464
|
|
455
465
|
def webauthn_auth_credential_from_form_submission
|
456
|
-
case auth_data = raw_param(webauthn_auth_param)
|
457
|
-
when String
|
458
|
-
begin
|
459
|
-
auth_data = JSON.parse(auth_data)
|
460
|
-
rescue
|
461
|
-
throw_error_reason(:invalid_webauthn_auth_param, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
462
|
-
end
|
463
|
-
when Hash
|
464
|
-
# nothing
|
465
|
-
else
|
466
|
-
throw_error_reason(:invalid_webauthn_auth_param, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
467
|
-
end
|
468
|
-
|
469
466
|
begin
|
470
|
-
webauthn_credential = WebAuthn::Credential.from_get(
|
467
|
+
webauthn_credential = WebAuthn::Credential.from_get(webauthn_auth_data)
|
471
468
|
unless valid_webauthn_credential_auth?(webauthn_credential)
|
472
469
|
throw_error_reason(:invalid_webauthn_auth_param, invalid_key_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
473
470
|
end
|
@@ -480,26 +477,28 @@ module Rodauth
|
|
480
477
|
webauthn_credential
|
481
478
|
end
|
482
479
|
|
483
|
-
def
|
484
|
-
case
|
480
|
+
def webauthn_auth_data
|
481
|
+
case auth_data = raw_param(webauthn_auth_param)
|
485
482
|
when String
|
486
483
|
begin
|
487
|
-
|
484
|
+
JSON.parse(auth_data)
|
488
485
|
rescue
|
489
|
-
throw_error_reason(:
|
486
|
+
throw_error_reason(:invalid_webauthn_auth_param, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
490
487
|
end
|
491
488
|
when Hash
|
492
|
-
|
489
|
+
auth_data
|
493
490
|
else
|
494
|
-
throw_error_reason(:
|
491
|
+
throw_error_reason(:invalid_webauthn_auth_param, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
495
492
|
end
|
493
|
+
end
|
496
494
|
|
495
|
+
def webauthn_setup_credential_from_form_submission
|
497
496
|
unless two_factor_password_match?(param(password_param))
|
498
497
|
throw_error_reason(:invalid_password, invalid_password_error_status, password_param, invalid_password_message)
|
499
498
|
end
|
500
499
|
|
501
500
|
begin
|
502
|
-
webauthn_credential = WebAuthn::Credential.from_create(
|
501
|
+
webauthn_credential = WebAuthn::Credential.from_create(webauthn_setup_data)
|
503
502
|
unless valid_new_webauthn_credential?(webauthn_credential)
|
504
503
|
throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
|
505
504
|
end
|
@@ -509,5 +508,20 @@ module Rodauth
|
|
509
508
|
|
510
509
|
webauthn_credential
|
511
510
|
end
|
511
|
+
|
512
|
+
def webauthn_setup_data
|
513
|
+
case setup_data = raw_param(webauthn_setup_param)
|
514
|
+
when String
|
515
|
+
begin
|
516
|
+
JSON.parse(setup_data)
|
517
|
+
rescue
|
518
|
+
throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
|
519
|
+
end
|
520
|
+
when Hash
|
521
|
+
setup_data
|
522
|
+
else
|
523
|
+
throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
|
524
|
+
end
|
525
|
+
end
|
512
526
|
end
|
513
527
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:webauthn_autofill, :WebauthnAutofill) do
|
5
|
+
depends :webauthn_login
|
6
|
+
|
7
|
+
auth_value_method :webauthn_autofill_js, File.binread(File.expand_path('../../../../javascript/webauthn_autofill.js', __FILE__)).freeze
|
8
|
+
|
9
|
+
translatable_method :webauthn_invalid_webauthn_id_message, "no webauthn key with given id found"
|
10
|
+
|
11
|
+
route(:webauthn_autofill_js) do |r|
|
12
|
+
before_webauthn_autofill_js_route
|
13
|
+
r.get do
|
14
|
+
response['Content-Type'] = 'text/javascript'
|
15
|
+
webauthn_autofill_js
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def webauthn_allow
|
20
|
+
return [] unless logged_in? || account
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def webauthn_user_verification
|
25
|
+
'preferred'
|
26
|
+
end
|
27
|
+
|
28
|
+
def webauthn_authenticator_selection
|
29
|
+
super.merge({ 'residentKey' => 'required', 'requireResidentKey' => true })
|
30
|
+
end
|
31
|
+
|
32
|
+
def login_field_autocomplete_value
|
33
|
+
request.path_info == login_path ? "#{super} webauthn" : super
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def _login_form_footer
|
39
|
+
footer = super
|
40
|
+
footer += render("webauthn-autofill") unless valid_login_entered?
|
41
|
+
footer
|
42
|
+
end
|
43
|
+
|
44
|
+
def account_from_webauthn_login
|
45
|
+
return super if param_or_nil(login_param)
|
46
|
+
|
47
|
+
credential_id = webauthn_auth_data["id"]
|
48
|
+
account_id = db[webauthn_keys_table]
|
49
|
+
.where(webauthn_keys_webauthn_id_column => credential_id)
|
50
|
+
.get(webauthn_keys_account_id_column)
|
51
|
+
|
52
|
+
unless account_id
|
53
|
+
throw_error_reason(:invalid_webauthn_id, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_webauthn_id_message)
|
54
|
+
end
|
55
|
+
|
56
|
+
@account = account_ds(account_id).first
|
57
|
+
end
|
58
|
+
|
59
|
+
def webauthn_login_options?
|
60
|
+
return true unless param_or_nil(login_param)
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -10,13 +10,18 @@ module Rodauth
|
|
10
10
|
|
11
11
|
error_flash "There was an error authenticating via WebAuthn"
|
12
12
|
|
13
|
+
auth_value_method :webauthn_login_user_verification_additional_factor?, false
|
14
|
+
|
15
|
+
internal_request_method :webauthn_login_params
|
16
|
+
internal_request_method :webauthn_login
|
17
|
+
|
13
18
|
route(:webauthn_login) do |r|
|
14
19
|
check_already_logged_in
|
15
20
|
before_webauthn_login_route
|
16
21
|
|
17
22
|
r.post do
|
18
23
|
catch_error do
|
19
|
-
unless
|
24
|
+
unless account_from_webauthn_login && open_account?
|
20
25
|
throw_error_reason(:no_matching_login, no_matching_login_error_status, login_param, no_matching_login_message)
|
21
26
|
end
|
22
27
|
|
@@ -24,6 +29,9 @@ module Rodauth
|
|
24
29
|
before_webauthn_login
|
25
30
|
login('webauthn') do
|
26
31
|
webauthn_update_session(webauthn_credential.id)
|
32
|
+
if webauthn_login_verification_factor?(webauthn_credential)
|
33
|
+
two_factor_update_session('webauthn-verification')
|
34
|
+
end
|
27
35
|
end
|
28
36
|
end
|
29
37
|
|
@@ -48,12 +56,31 @@ module Rodauth
|
|
48
56
|
end
|
49
57
|
end
|
50
58
|
|
59
|
+
def webauthn_user_verification
|
60
|
+
return 'preferred' if webauthn_login_user_verification_additional_factor?
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
51
64
|
def use_multi_phase_login?
|
52
65
|
true
|
53
66
|
end
|
54
67
|
|
55
68
|
private
|
56
69
|
|
70
|
+
def webauthn_login_verification_factor?(webauthn_credential)
|
71
|
+
webauthn_login_user_verification_additional_factor? &&
|
72
|
+
webauthn_credential.response.authenticator_data.user_verified? &&
|
73
|
+
uses_two_factor_authentication?
|
74
|
+
end
|
75
|
+
|
76
|
+
def account_from_webauthn_login
|
77
|
+
account_from_login(param(login_param))
|
78
|
+
end
|
79
|
+
|
80
|
+
def webauthn_login_options?
|
81
|
+
!!account_from_webauthn_login
|
82
|
+
end
|
83
|
+
|
57
84
|
def _multi_phase_login_forms
|
58
85
|
forms = super
|
59
86
|
if valid_login_entered? && webauthn_setup?
|
data/lib/rodauth/version.rb
CHANGED
data/templates/login-field.str
CHANGED
@@ -1,4 +1,4 @@
|
|
1
1
|
<div class="form-group mb-3">
|
2
2
|
<label for="login" class="form-label">#{rodauth.login_label}#{rodauth.input_field_label_suffix}</label>
|
3
|
-
#{rodauth.input_field_string(rodauth.login_param, 'login', :type=>rodauth.login_input_type, :autocomplete=>rodauth.
|
3
|
+
#{rodauth.input_field_string(rodauth.login_param, 'login', :type=>rodauth.login_input_type, :autocomplete=>rodauth.login_field_autocomplete_value)}
|
4
4
|
</div>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<form method="post" action="#{rodauth.webauthn_login_path}" class="rodauth" role="form" id="webauthn-login-form" data-credential-options="#{h((cred = rodauth.webauthn_credential_options_for_get).as_json.to_json)}">
|
2
|
+
#{rodauth.webauthn_auth_additional_form_tags}
|
3
|
+
#{rodauth.csrf_tag(rodauth.webauthn_login_path)}
|
4
|
+
<input type="hidden" name="#{rodauth.webauthn_auth_challenge_param}" value="#{cred.challenge}" />
|
5
|
+
<input type="hidden" name="#{rodauth.webauthn_auth_challenge_hmac_param}" value="#{rodauth.compute_hmac(cred.challenge)}" />
|
6
|
+
<input class="rodauth_hidden d-none" aria-hidden="true" type="text" name="#{rodauth.webauthn_auth_param}" id="webauthn-auth" value="" />
|
7
|
+
#{rodauth.button(rodauth.webauthn_auth_button, class: "d-none")}
|
8
|
+
</form>
|
9
|
+
<script src="#{rodauth.webauthn_js_host}#{rodauth.webauthn_autofill_js_path}"></script>
|
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.31.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -296,6 +296,7 @@ extra_rdoc_files:
|
|
296
296
|
- doc/verify_account_grace_period.rdoc
|
297
297
|
- doc/verify_login_change.rdoc
|
298
298
|
- doc/webauthn.rdoc
|
299
|
+
- doc/webauthn_autofill.rdoc
|
299
300
|
- doc/webauthn_login.rdoc
|
300
301
|
- doc/webauthn_verify_account.rdoc
|
301
302
|
- doc/release_notes/1.0.0.txt
|
@@ -346,6 +347,8 @@ extra_rdoc_files:
|
|
346
347
|
- doc/release_notes/2.28.0.txt
|
347
348
|
- doc/release_notes/2.29.0.txt
|
348
349
|
- doc/release_notes/2.3.0.txt
|
350
|
+
- doc/release_notes/2.30.0.txt
|
351
|
+
- doc/release_notes/2.31.0.txt
|
349
352
|
- doc/release_notes/2.4.0.txt
|
350
353
|
- doc/release_notes/2.5.0.txt
|
351
354
|
- doc/release_notes/2.6.0.txt
|
@@ -392,6 +395,7 @@ files:
|
|
392
395
|
- doc/guides/query_params.rdoc
|
393
396
|
- doc/guides/redirects.rdoc
|
394
397
|
- doc/guides/registration_field.rdoc
|
398
|
+
- doc/guides/render_confirmation.rdoc
|
395
399
|
- doc/guides/require_mfa.rdoc
|
396
400
|
- doc/guides/reset_password_autologin.rdoc
|
397
401
|
- doc/guides/share_configuration.rdoc
|
@@ -462,6 +466,8 @@ files:
|
|
462
466
|
- doc/release_notes/2.28.0.txt
|
463
467
|
- doc/release_notes/2.29.0.txt
|
464
468
|
- doc/release_notes/2.3.0.txt
|
469
|
+
- doc/release_notes/2.30.0.txt
|
470
|
+
- doc/release_notes/2.31.0.txt
|
465
471
|
- doc/release_notes/2.4.0.txt
|
466
472
|
- doc/release_notes/2.5.0.txt
|
467
473
|
- doc/release_notes/2.6.0.txt
|
@@ -480,9 +486,11 @@ files:
|
|
480
486
|
- doc/verify_account_grace_period.rdoc
|
481
487
|
- doc/verify_login_change.rdoc
|
482
488
|
- doc/webauthn.rdoc
|
489
|
+
- doc/webauthn_autofill.rdoc
|
483
490
|
- doc/webauthn_login.rdoc
|
484
491
|
- doc/webauthn_verify_account.rdoc
|
485
492
|
- javascript/webauthn_auth.js
|
493
|
+
- javascript/webauthn_autofill.js
|
486
494
|
- javascript/webauthn_setup.js
|
487
495
|
- lib/roda/plugins/rodauth.rb
|
488
496
|
- lib/rodauth.rb
|
@@ -530,6 +538,7 @@ files:
|
|
530
538
|
- lib/rodauth/features/verify_account_grace_period.rb
|
531
539
|
- lib/rodauth/features/verify_login_change.rb
|
532
540
|
- lib/rodauth/features/webauthn.rb
|
541
|
+
- lib/rodauth/features/webauthn_autofill.rb
|
533
542
|
- lib/rodauth/features/webauthn_login.rb
|
534
543
|
- lib/rodauth/features/webauthn_verify_account.rb
|
535
544
|
- lib/rodauth/migrations.rb
|
@@ -585,6 +594,7 @@ files:
|
|
585
594
|
- templates/verify-login-change-email.str
|
586
595
|
- templates/verify-login-change.str
|
587
596
|
- templates/webauthn-auth.str
|
597
|
+
- templates/webauthn-autofill.str
|
588
598
|
- templates/webauthn-remove.str
|
589
599
|
- templates/webauthn-setup.str
|
590
600
|
homepage: https://rodauth.jeremyevans.net
|
@@ -618,7 +628,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
618
628
|
- !ruby/object:Gem::Version
|
619
629
|
version: '0'
|
620
630
|
requirements: []
|
621
|
-
rubygems_version: 3.4.
|
631
|
+
rubygems_version: 3.4.10
|
622
632
|
signing_key:
|
623
633
|
specification_version: 4
|
624
634
|
summary: Authentication and Account Management Framework for Rack Applications
|