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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1714e5a3a0a5bbae56f2905dd528611de3b958d505d312071148b56fdfb3d6f
4
- data.tar.gz: 8bb57c30ced05b0825a5d1fd74efe9f6523202f1b151b591c0bbdf10ad9f12af
3
+ metadata.gz: 34f1a4bc530025482c6550375de67bf9dcbe4ea75ca9f4f775d179805629e115
4
+ data.tar.gz: 75339b39caf60463c076459e1fc21fd4d2291e4bc756c4cc5107edf7462c0186
5
5
  SHA512:
6
- metadata.gz: 4dfc0639aaebdeacf6961122265720c992fb0d1af5d7864f5984fa902eef0ae49aea30db63681b1bdd1c598458f8ce5b035fdb3192249f3983afba15442bf990
7
- data.tar.gz: d044a6934b3d06bee1e260de68ca5a88016b6f611da1e59dabf1e16afc63e7a82c6d16d196ed57a23558c14a0d6aecba4030ece7830a4f0a6211260b1e619b50
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
 
@@ -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
@@ -10,7 +10,7 @@ or their company's name.
10
10
  Let's assume you wanted to wanted to store the additional field(s) directly on
11
11
  the +accounts+ table:
12
12
 
13
- atler_table :accounts do
13
+ alter_table :accounts do
14
14
  add_column :name, String
15
15
  end
16
16
 
@@ -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
@@ -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 invalides the token and creates
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.
@@ -1,10 +1,11 @@
1
1
  = Documentation for WebAuthn Login Feature
2
2
 
3
- The webauthn feature implements passwordless authentication via
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 webauthn feature implements setting up an WebAuthn authenticator
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
- def extract_password_hash_cost(hash)
64
- return super unless argon2_hash_algorithm?(hash )
63
+ if Argon2::VERSION >= '2.1'
64
+ def extract_password_hash_cost(hash)
65
+ return super unless argon2_hash_algorithm?(hash )
65
66
 
66
- /\A\$argon2id\$v=\d+\$m=(\d+),t=(\d+)/ =~ hash
67
- { t_cost: $2.to_i, m_cost: Math.log2($1.to_i).to_i }
68
- end
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
- if ENV['RACK_ENV'] == 'test'
71
- def argon2_hash_cost
72
- {t_cost: 1, m_cost: 3}
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 argon2_hash_cost
77
- {t_cost: 2, m_cost: 16}
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) && account_from_login(param(login_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 json_response[json_response_error_key]
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
- extend_remember_deadline
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 => account_webauthn_ids,
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(auth_data)
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 webauthn_setup_credential_from_form_submission
484
- case setup_data = raw_param(webauthn_setup_param)
480
+ def webauthn_auth_data
481
+ case auth_data = raw_param(webauthn_auth_param)
485
482
  when String
486
483
  begin
487
- setup_data = JSON.parse(setup_data)
484
+ JSON.parse(auth_data)
488
485
  rescue
489
- throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
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
- # nothing
489
+ auth_data
493
490
  else
494
- throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
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(setup_data)
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 account_from_login(param(login_param)) && open_account?
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?
@@ -6,7 +6,7 @@ module Rodauth
6
6
  MAJOR = 2
7
7
 
8
8
  # The minor version of Rodauth, updated for new feature releases of Rodauth.
9
- MINOR = 29
9
+ MINOR = 31
10
10
 
11
11
  # The patch version of Rodauth, updated only for bug fixes from the last
12
12
  # feature release.
@@ -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.login_uses_email? ? "email" : "on")}
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.29.0
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-03-22 00:00:00.000000000 Z
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.6
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