rodauth 2.29.0 → 2.30.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: 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