diff --git a/composer.json b/composer.json
index cc37fe26194bbe8eff054b255c7de9d88ab04894..338e28bc92b0c6b4884b2f264c787912949a7afd 100644
--- a/composer.json
+++ b/composer.json
@@ -9,7 +9,7 @@
         "chat": "https://www.drupal.org/node/314178"
     },
     "require": {
-        "composer/installers": "^1.0.24",
+        "composer/installers": "^1.9",
         "drupal/core": "self.version",
         "drupal/core-project-message": "self.version",
         "drupal/core-vendor-hardening": "self.version"
diff --git a/composer.lock b/composer.lock
index 00e8e53d4dfd783a588f6aeb1047b622bcc65561..953182d8c5023ad0d5ae5ead9520e2bbdfb1c594 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "966c0ed90c2996cad45b441c58d01aa2",
+    "content-hash": "bf3f5b1c0d2f35255e03825589a1ee4f",
     "packages": [
         {
             "name": "asm89/stack-cors",
@@ -705,10 +705,10 @@
             "dist": {
                 "type": "path",
                 "url": "composer/Plugin/ProjectMessage",
-                "reference": "0691ca6beba657e6da57a893b1c6da757d16afc8"
+                "reference": "b4efdbe26634b41a1b89e4f3770a8074769088a6"
             },
             "require": {
-                "composer-plugin-api": "^1.1",
+                "composer-plugin-api": "^1.1 || ^2",
                 "php": ">=7.3.0"
             },
             "type": "composer-plugin",
@@ -735,10 +735,10 @@
             "dist": {
                 "type": "path",
                 "url": "composer/Plugin/VendorHardening",
-                "reference": "0b015340af38f90df46923a934d0b22026134f18"
+                "reference": "d54f0b3cc8b4237f3a41a0860a808db242f9da9e"
             },
             "require": {
-                "composer-plugin-api": "^1.1",
+                "composer-plugin-api": "^1.1 || ^2",
                 "php": ">=7.3.0"
             },
             "type": "composer-plugin",
diff --git a/composer/Plugin/ProjectMessage/MessagePlugin.php b/composer/Plugin/ProjectMessage/MessagePlugin.php
index 8a5333d8f1ca6bfbbf7d6e8e71a30f8aae17c83e..9e57e6a363b49f95134c4a1eaecdcd4a1eaa8120 100644
--- a/composer/Plugin/ProjectMessage/MessagePlugin.php
+++ b/composer/Plugin/ProjectMessage/MessagePlugin.php
@@ -45,6 +45,18 @@ public function activate(Composer $composer, IOInterface $io) {
     $this->io = $io;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function deactivate(Composer $composer, IOInterface $io) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uninstall(Composer $composer, IOInterface $io) {
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/composer/Plugin/ProjectMessage/composer.json b/composer/Plugin/ProjectMessage/composer.json
index 96ade5102c843ee5bf548d7a461913a7ff4db654..3741b31600101d9d4e5798207364f12a11f39787 100644
--- a/composer/Plugin/ProjectMessage/composer.json
+++ b/composer/Plugin/ProjectMessage/composer.json
@@ -15,6 +15,6 @@
   },
   "require": {
     "php": ">=7.3.0",
-    "composer-plugin-api": "^1.1"
+    "composer-plugin-api": "^1.1 || ^2"
   }
 }
diff --git a/composer/Plugin/Scaffold/AllowedPackages.php b/composer/Plugin/Scaffold/AllowedPackages.php
index 9b28368bbd45781da507b6ebae512345c7c325e0..481492f009ab62871d1844649087879c03068564 100644
--- a/composer/Plugin/Scaffold/AllowedPackages.php
+++ b/composer/Plugin/Scaffold/AllowedPackages.php
@@ -3,6 +3,7 @@
 namespace Drupal\Composer\Plugin\Scaffold;
 
 use Composer\Composer;
+use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\Installer\PackageEvent;
 use Composer\IO\IOInterface;
 use Composer\Package\PackageInterface;
@@ -96,8 +97,12 @@ public function getAllowedPackages() {
    */
   public function event(PackageEvent $event) {
     $operation = $event->getOperation();
-    // Determine the package.
-    $package = $operation->getJobType() == 'update' ? $operation->getTargetPackage() : $operation->getPackage();
+    // Determine the package. Later, in evaluateNewPackages(), we will report
+    // which of the newly-installed packages have scaffold operations, and
+    // whether or not they are allowed to scaffold by the allowed-packages
+    // option in the root-level composer.json file.
+    $operationType = $this->getOperationType($operation);
+    $package = $operationType === 'update' ? $operation->getTargetPackage() : $operation->getPackage();
     if (ScaffoldOptions::hasOptions($package->getExtra())) {
       $this->newPackages[$package->getName()] = $package;
     }
@@ -176,6 +181,26 @@ protected function evaluateNewPackages(array $allowed_packages) {
     return $allowed_packages;
   }
 
+  /**
+   * Determine the type of the provided operation.
+   *
+   * Adjusts API used for Composer 1 or Composer 2.
+   *
+   * @param \Composer\DependencyResolver\Operation\OperationInterface $operation
+   *   The operation object.
+   *
+   * @return string
+   *   The operation type.
+   */
+  protected function getOperationType(OperationInterface $operation) {
+    // Use Composer 2 method.
+    if (method_exists($operation, 'getOperationType')) {
+      return $operation->getOperationType();
+    }
+    // Fallback to Composer 1 method.
+    return $operation->getJobType();
+  }
+
   /**
    * Retrieves a package from the current composer process.
    *
diff --git a/composer/Plugin/Scaffold/Plugin.php b/composer/Plugin/Scaffold/Plugin.php
index a1252cfac140bd4f5483f223b8346efda8a28836..9a694ec349b6cd8589f54cf2a6381eb4a4208d41 100644
--- a/composer/Plugin/Scaffold/Plugin.php
+++ b/composer/Plugin/Scaffold/Plugin.php
@@ -60,6 +60,18 @@ public function activate(Composer $composer, IOInterface $io) {
     $this->requireWasCalled = FALSE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function deactivate(Composer $composer, IOInterface $io) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uninstall(Composer $composer, IOInterface $io) {
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/composer/Plugin/Scaffold/composer.json b/composer/Plugin/Scaffold/composer.json
index 5cbb4ad79b993ece20167d3650e64b97cab3e61f..ca9d6d7b48d2e1ad6f70fa833468769376d00d94 100644
--- a/composer/Plugin/Scaffold/composer.json
+++ b/composer/Plugin/Scaffold/composer.json
@@ -6,7 +6,7 @@
   "homepage": "https://www.drupal.org/project/drupal",
   "license": "GPL-2.0-or-later",
   "require": {
-    "composer-plugin-api": "^1.0.0",
+    "composer-plugin-api": "^1 || ^2",
     "php": ">=7.3.0"
   },
   "conflict": {
diff --git a/composer/Plugin/VendorHardening/VendorHardeningPlugin.php b/composer/Plugin/VendorHardening/VendorHardeningPlugin.php
index c8e6182e113ce18b8ed558020c87fc1ac3927bb4..2cd7bdec52c933cd682e0e1cfc411914ad50d19a 100644
--- a/composer/Plugin/VendorHardening/VendorHardeningPlugin.php
+++ b/composer/Plugin/VendorHardening/VendorHardeningPlugin.php
@@ -65,6 +65,18 @@ public function activate(Composer $composer, IOInterface $io) {
     $this->config = new Config($this->composer->getPackage());
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function deactivate(Composer $composer, IOInterface $io) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uninstall(Composer $composer, IOInterface $io) {
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/composer/Plugin/VendorHardening/composer.json b/composer/Plugin/VendorHardening/composer.json
index 4ed1adedd0d6cce654d049f700da01ac8a396e9e..5f78bc6abdaa625ee6e7616f36c5eba63171a0d9 100644
--- a/composer/Plugin/VendorHardening/composer.json
+++ b/composer/Plugin/VendorHardening/composer.json
@@ -15,6 +15,6 @@
   },
   "require": {
     "php": ">=7.3.0",
-    "composer-plugin-api": "^1.1"
+    "composer-plugin-api": "^1.1 || ^2"
   }
 }
diff --git a/composer/Template/LegacyProject/composer.json b/composer/Template/LegacyProject/composer.json
index c7f532dfe61c923ae76db82034ad34b87c25a8d3..9162033006d7096dd35c08515befd4695526000e 100644
--- a/composer/Template/LegacyProject/composer.json
+++ b/composer/Template/LegacyProject/composer.json
@@ -15,7 +15,7 @@
         }
     ],
     "require": {
-        "composer/installers": "^1.2",
+        "composer/installers": "^1.9",
         "drupal/core-composer-scaffold": "^9",
         "drupal/core-project-message": "^9",
         "drupal/core-recommended": "^9",
diff --git a/composer/Template/RecommendedProject/composer.json b/composer/Template/RecommendedProject/composer.json
index 56aa5cdb2ce49a695d37b629e9655eab64884394..9de50df252f8205bd65d2fc2a572191a14fcf6eb 100644
--- a/composer/Template/RecommendedProject/composer.json
+++ b/composer/Template/RecommendedProject/composer.json
@@ -15,7 +15,7 @@
         }
     ],
     "require": {
-        "composer/installers": "^1.2",
+        "composer/installers": "^1.9",
         "drupal/core-composer-scaffold": "^9",
         "drupal/core-project-message": "^9",
         "drupal/core-recommended": "^9"
diff --git a/core/drupalci.yml b/core/drupalci.yml
index d6c5787ffc5463e629dcba0a503eaa20958a7e9d..63df934307edaadd6bb9a241e8e30a881a4f356e 100644
--- a/core/drupalci.yml
+++ b/core/drupalci.yml
@@ -50,3 +50,9 @@ build:
       # Run nightwatch testing.
       # @see https://www.drupal.org/project/drupal/issues/2869825
       nightwatchjs:
+      # Re-run Composer plugin tests after installing Composer 2
+      container_command.composer-upgrade:
+        commands:
+          - "sudo composer self-update --snapshot"
+          - "./vendor/bin/phpunit -c core --group VendorHardening,ProjectMessage,Scaffold"
+        halt-on-fail: true
diff --git a/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php b/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php
index e9bf830116833f009c66d20a9ecd525d01fc0431..641dba464b6c9eb1cb02284b18418c623e1feff0 100644
--- a/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php
+++ b/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php
@@ -105,6 +105,11 @@ public function testVerifyTemplateTestProviderIsAccurate() {
    * @dataProvider provideTemplateCreateProject
    */
   public function testTemplateCreateProject($project, $package_dir, $docroot_dir) {
+    $composerVersionLine = exec('composer --version');
+    if (strpos($composerVersionLine, 'Composer version 2') !== FALSE) {
+      $this->markTestSkipped('We cannot run the template create project test with Composer 2 until we have a stable version of composer/semver 2.x. The create project test installs drupal/core-recommended and the Drupal Composer plugins from Packagist, so these must also be compatible with Composer 2.x in order for this test to work.');
+    }
+
     $this->copyCodebase();
 
     // Get the Drupal core version branch. For instance, this should be
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ScaffoldUpgradeTest.php b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ScaffoldUpgradeTest.php
index 3f6eeedd454dd5c7f6c1dd286ffda98119744d87..99eb705b28561279f4b176e4b13b857fa387d552 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ScaffoldUpgradeTest.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ScaffoldUpgradeTest.php
@@ -44,6 +44,10 @@ protected function setUp(): void {
    * Test upgrading the Composer Scaffold plugin.
    */
   public function testScaffoldUpgrade() {
+    $composerVersionLine = exec('composer --version');
+    if (strpos($composerVersionLine, 'Composer version 2') !== FALSE) {
+      $this->markTestSkipped('We cannot run the scaffold upgrade test with Composer 2 until we have a stable version of drupal/core-composer-scaffold to start from that we can install with Composer 2.x.');
+    }
     $this->fixturesDir = $this->fixtures->tmpDir($this->getName());
     $replacements = ['SYMLINK' => 'false', 'PROJECT_ROOT' => $this->fixtures->projectRoot()];
     $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);