rodauth 2.29.0 → 2.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1714e5a3a0a5bbae56f2905dd528611de3b958d505d312071148b56fdfb3d6f
4
- data.tar.gz: 8bb57c30ced05b0825a5d1fd74efe9f6523202f1b151b591c0bbdf10ad9f12af
3
+ metadata.gz: 8270d10e6c0fbe554fc322958893f2c8069363af6455c0d17b0a7d3aafab11bd
4
+ data.tar.gz: 6eae8a9487764a9b189b27b8f1588e1516d86a6953fdabe0cff6adfd84facb5a
5
5
  SHA512:
6
- metadata.gz: 4dfc0639aaebdeacf6961122265720c992fb0d1af5d7864f5984fa902eef0ae49aea30db63681b1bdd1c598458f8ce5b035fdb3192249f3983afba15442bf990
7
- data.tar.gz: d044a6934b3d06bee1e260de68ca5a88016b6f611da1e59dabf1e16afc63e7a82c6d16d196ed57a23558c14a0d6aecba4030ece7830a4f0a6211260b1e619b50
6
+ metadata.gz: d4dca9a0819842a478fac05f138b07452a054c7e498821eee7e61ab49640eccc46974d38c3aea1f71bf226eb7f76b03888a6734d7ae2139bcc74aca9a24bad6c
7
+ data.tar.gz: 5075118b0c6a8b27df251a2e6bfcd45e3dacc7ba1402bf5bacfc38a50b93b424a0ccdb479b1f5541d589f5687a8424c7319a194c0b01a80c762b725065190013
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ === 2.30.0 (2023-05-22)
2
+
3
+ * Make load_memory in the remember feature not raise NoMethodError if logged in when the account no longer exists (jeremyevans) (#331)
4
+
5
+ * Add webauthn_autofill feature, for supporting autofill of webauthn information on the login form (janko) (#328)
6
+
1
7
  === 2.29.0 (2023-03-22)
2
8
 
3
9
  * 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)
@@ -318,6 +319,16 @@ bad idea), you don't need to use the PostgreSQL citext extension. Just
318
319
  remember to modify the migration below to use +String+ instead of +citext+
319
320
  for the email in that case.
320
321
 
322
+ === Grant schema rights (PostgreSQL 15+)
323
+
324
+ PostgreSQL 15 changed default database security so that only the database
325
+ owner has writable access to the public schema. Rodauth expects the
326
+ +ph+ account to have writable access to the public schema when setting
327
+ things up. Temporarily grant that access (it will be revoked after the
328
+ migation has run)
329
+
330
+ psql -U postgres -c "GRANT CREATE ON SCHEMA public TO ${DATABASE_NAME}_password" ${DATABASE_NAME}
331
+
321
332
  === Using non-default schema
322
333
 
323
334
  PostgreSQL sets up new tables in the public schema by default.
@@ -739,6 +750,13 @@ One thing to notice in the above migrations is that Rodauth uses additional
739
750
  tables for additional features, instead of additional columns in a single
740
751
  table.
741
752
 
753
+ === Revoking schema rights (PostgreSQL 15+)
754
+
755
+ If you explicit granted access to the public schema before running the
756
+ migration, revoke it afterward:
757
+
758
+ psql -U postgres -c "REVOKE CREATE ON SCHEMA public FROM ${DATABASE_NAME}_password" ${DATABASE_NAME}
759
+
742
760
  === Locking Down (PostgreSQL only)
743
761
 
744
762
  After running the migrations, you can increase security slightly by making
@@ -915,6 +933,7 @@ view the appropriate file in the doc directory.
915
933
  * {Verify Account Grace Period}[rdoc-ref:doc/verify_account_grace_period.rdoc]
916
934
  * {Verify Login Change}[rdoc-ref:doc/verify_login_change.rdoc]
917
935
  * {WebAuthn}[rdoc-ref:doc/webauthn.rdoc]
936
+ * {WebAuthn Autofill}[rdoc-ref:doc/webauthn_autofill.rdoc]
918
937
  * {WebAuthn Login}[rdoc-ref:doc/webauthn_login.rdoc]
919
938
  * {WebAuthn Verify Account}[rdoc-ref:doc/webauthn_verify_account.rdoc]
920
939
 
@@ -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,14 @@
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
+ == Auth Value Methods
8
+
9
+ webauthn_autofill_js :: The javascript code to execute on the login page to enable autofill UI.
10
+ webauthn_autofill_js_route :: The route to the webauthn autofill javascript file.
11
+
12
+ == Auth Methods
13
+
14
+ before_webauthn_autofill_js_route :: Run arbitrary code before handling a webauthn autofill javascript route.
@@ -1,6 +1,6 @@
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
@@ -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
+
@@ -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
@@ -116,7 +116,7 @@ module Rodauth
116
116
 
117
117
  def before_webauthn_login_route
118
118
  super if defined?(super)
119
- if use_json? && !param_or_nil(webauthn_auth_param) && account_from_login(param(login_param))
119
+ if use_json? && !param_or_nil(webauthn_auth_param) && webauthn_login_options?
120
120
  cred = webauthn_credential_options_for_get
121
121
  json_response[webauthn_auth_param] = cred.as_json
122
122
  json_response[webauthn_auth_challenge_param] = cred.challenge
@@ -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
@@ -320,7 +320,7 @@ module Rodauth
320
320
 
321
321
  def webauthn_credential_options_for_get
322
322
  WebAuthn::Credential.options_for_get(
323
- :allow => account_webauthn_ids,
323
+ :allow => webauthn_allow,
324
324
  :timeout => webauthn_auth_timeout,
325
325
  :rp_id => webauthn_rp_id,
326
326
  :user_verification => webauthn_user_verification,
@@ -336,6 +336,10 @@ module Rodauth
336
336
  base_url
337
337
  end
338
338
 
339
+ def webauthn_allow
340
+ account_webauthn_ids
341
+ end
342
+
339
343
  def webauthn_rp_id
340
344
  webauthn_origin.sub(/\Ahttps?:\/\//, '').sub(/:\d+\z/, '')
341
345
  end
@@ -453,21 +457,8 @@ module Rodauth
453
457
  end
454
458
 
455
459
  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
460
  begin
470
- webauthn_credential = WebAuthn::Credential.from_get(auth_data)
461
+ webauthn_credential = WebAuthn::Credential.from_get(webauthn_auth_data)
471
462
  unless valid_webauthn_credential_auth?(webauthn_credential)
472
463
  throw_error_reason(:invalid_webauthn_auth_param, invalid_key_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
473
464
  end
@@ -480,26 +471,28 @@ module Rodauth
480
471
  webauthn_credential
481
472
  end
482
473
 
483
- def webauthn_setup_credential_from_form_submission
484
- case setup_data = raw_param(webauthn_setup_param)
474
+ def webauthn_auth_data
475
+ case auth_data = raw_param(webauthn_auth_param)
485
476
  when String
486
477
  begin
487
- setup_data = JSON.parse(setup_data)
478
+ JSON.parse(auth_data)
488
479
  rescue
489
- throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
480
+ throw_error_reason(:invalid_webauthn_auth_param, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
490
481
  end
491
482
  when Hash
492
- # nothing
483
+ auth_data
493
484
  else
494
- throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
485
+ throw_error_reason(:invalid_webauthn_auth_param, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
495
486
  end
487
+ end
496
488
 
489
+ def webauthn_setup_credential_from_form_submission
497
490
  unless two_factor_password_match?(param(password_param))
498
491
  throw_error_reason(:invalid_password, invalid_password_error_status, password_param, invalid_password_message)
499
492
  end
500
493
 
501
494
  begin
502
- webauthn_credential = WebAuthn::Credential.from_create(setup_data)
495
+ webauthn_credential = WebAuthn::Credential.from_create(webauthn_setup_data)
503
496
  unless valid_new_webauthn_credential?(webauthn_credential)
504
497
  throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
505
498
  end
@@ -509,5 +502,20 @@ module Rodauth
509
502
 
510
503
  webauthn_credential
511
504
  end
505
+
506
+ def webauthn_setup_data
507
+ case setup_data = raw_param(webauthn_setup_param)
508
+ when String
509
+ begin
510
+ JSON.parse(setup_data)
511
+ rescue
512
+ throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
513
+ end
514
+ when Hash
515
+ setup_data
516
+ else
517
+ throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
518
+ end
519
+ end
512
520
  end
513
521
  end
@@ -0,0 +1,58 @@
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
+ route(:webauthn_autofill_js) do |r|
10
+ before_webauthn_autofill_js_route
11
+ r.get do
12
+ response['Content-Type'] = 'text/javascript'
13
+ webauthn_autofill_js
14
+ end
15
+ end
16
+
17
+ def webauthn_allow
18
+ return [] unless logged_in? || account
19
+ super
20
+ end
21
+
22
+ def webauthn_user_verification
23
+ 'preferred'
24
+ end
25
+
26
+ def webauthn_authenticator_selection
27
+ super.merge({ 'residentKey' => 'required', 'requireResidentKey' => true })
28
+ end
29
+
30
+ def login_field_autocomplete_value
31
+ request.path_info == login_path ? "#{super} webauthn" : super
32
+ end
33
+
34
+ private
35
+
36
+ def _login_form_footer
37
+ footer = super
38
+ footer += render("webauthn-autofill") unless valid_login_entered?
39
+ footer
40
+ end
41
+
42
+ def account_from_webauthn_login
43
+ return super if param_or_nil(login_param)
44
+
45
+ credential_id = webauthn_auth_data["id"]
46
+ account_id = db[webauthn_keys_table]
47
+ .where(webauthn_keys_webauthn_id_column => credential_id)
48
+ .get(webauthn_keys_account_id_column)
49
+
50
+ @account = account_ds(account_id).first if account_id
51
+ end
52
+
53
+ def webauthn_login_options?
54
+ return true unless param_or_nil(login_param)
55
+ super
56
+ end
57
+ end
58
+ end
@@ -16,7 +16,7 @@ module Rodauth
16
16
 
17
17
  r.post do
18
18
  catch_error do
19
- unless account_from_login(param(login_param)) && open_account?
19
+ unless account_from_webauthn_login && open_account?
20
20
  throw_error_reason(:no_matching_login, no_matching_login_error_status, login_param, no_matching_login_message)
21
21
  end
22
22
 
@@ -54,6 +54,14 @@ module Rodauth
54
54
 
55
55
  private
56
56
 
57
+ def account_from_webauthn_login
58
+ account_from_login(param(login_param))
59
+ end
60
+
61
+ def webauthn_login_options?
62
+ !!account_from_webauthn_login
63
+ end
64
+
57
65
  def _multi_phase_login_forms
58
66
  forms = super
59
67
  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 = 30
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.30.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-05-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,7 @@ 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
349
351
  - doc/release_notes/2.4.0.txt
350
352
  - doc/release_notes/2.5.0.txt
351
353
  - doc/release_notes/2.6.0.txt
@@ -462,6 +464,7 @@ files:
462
464
  - doc/release_notes/2.28.0.txt
463
465
  - doc/release_notes/2.29.0.txt
464
466
  - doc/release_notes/2.3.0.txt
467
+ - doc/release_notes/2.30.0.txt
465
468
  - doc/release_notes/2.4.0.txt
466
469
  - doc/release_notes/2.5.0.txt
467
470
  - doc/release_notes/2.6.0.txt
@@ -480,9 +483,11 @@ files:
480
483
  - doc/verify_account_grace_period.rdoc
481
484
  - doc/verify_login_change.rdoc
482
485
  - doc/webauthn.rdoc
486
+ - doc/webauthn_autofill.rdoc
483
487
  - doc/webauthn_login.rdoc
484
488
  - doc/webauthn_verify_account.rdoc
485
489
  - javascript/webauthn_auth.js
490
+ - javascript/webauthn_autofill.js
486
491
  - javascript/webauthn_setup.js
487
492
  - lib/roda/plugins/rodauth.rb
488
493
  - lib/rodauth.rb
@@ -530,6 +535,7 @@ files:
530
535
  - lib/rodauth/features/verify_account_grace_period.rb
531
536
  - lib/rodauth/features/verify_login_change.rb
532
537
  - lib/rodauth/features/webauthn.rb
538
+ - lib/rodauth/features/webauthn_autofill.rb
533
539
  - lib/rodauth/features/webauthn_login.rb
534
540
  - lib/rodauth/features/webauthn_verify_account.rb
535
541
  - lib/rodauth/migrations.rb
@@ -585,6 +591,7 @@ files:
585
591
  - templates/verify-login-change-email.str
586
592
  - templates/verify-login-change.str
587
593
  - templates/webauthn-auth.str
594
+ - templates/webauthn-autofill.str
588
595
  - templates/webauthn-remove.str
589
596
  - templates/webauthn-setup.str
590
597
  homepage: https://rodauth.jeremyevans.net
@@ -618,7 +625,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
618
625
  - !ruby/object:Gem::Version
619
626
  version: '0'
620
627
  requirements: []
621
- rubygems_version: 3.4.6
628
+ rubygems_version: 3.4.10
622
629
  signing_key:
623
630
  specification_version: 4
624
631
  summary: Authentication and Account Management Framework for Rack Applications