diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index 7b703499e3246d8558d9a8db6523fca05669eb62..72b819fa9c184e0837ce9d84c865aaa1baa2611b 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -85,7 +85,7 @@ services:
       - { name: event_subscriber }
   automatic_updates.composer_executable_validator:
     class: Drupal\automatic_updates\Validator\ComposerExecutableValidator
-    arguments: ['@automatic_updates.exec_finder']
+    arguments: ['@automatic_updates.composer_runner']
     tags:
       - { name: event_subscriber }
   automatic_updates.path_locator:
diff --git a/src/Validator/ComposerExecutableValidator.php b/src/Validator/ComposerExecutableValidator.php
index ce892f3b612c15346aa8c36d32b11928c71dc994..80d623a0a6fa42fc643a84af9c0096eab19693f4 100644
--- a/src/Validator/ComposerExecutableValidator.php
+++ b/src/Validator/ComposerExecutableValidator.php
@@ -5,30 +5,42 @@ namespace Drupal\automatic_updates\Validator;
 use Drupal\automatic_updates\AutomaticUpdatesEvents;
 use Drupal\automatic_updates\Event\UpdateEvent;
 use Drupal\automatic_updates\Validation\ValidationResult;
-use PhpTuf\ComposerStager\Exception\IOException;
-use PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinderInterface;
+use Drupal\Core\Extension\ExtensionVersion;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
+use PhpTuf\ComposerStager\Exception\ExceptionInterface;
+use PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
- * Validates that the Composer executable can be found.
+ * Validates that the Composer executable can be found in the correct version.
  */
-class ComposerExecutableValidator implements EventSubscriberInterface {
+class ComposerExecutableValidator implements EventSubscriberInterface, ProcessOutputCallbackInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The Composer runner.
+   *
+   * @var \PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface
+   */
+  protected $composer;
 
   /**
-   * The executable finder service.
+   * The detected version of Composer.
    *
-   * @var \PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinderInterface
+   * @var string
    */
-  protected $executableFinder;
+  protected $version;
 
   /**
    * Constructs a ComposerExecutableValidator object.
    *
-   * @param \PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinderInterface $executable_finder
-   *   The executable finder service.
+   * @param \PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface $composer
+   *   The Composer runner.
    */
-  public function __construct(ExecutableFinderInterface $executable_finder) {
-    $this->executableFinder = $executable_finder;
+  public function __construct(ComposerRunnerInterface $composer) {
+    $this->composer = $composer;
   }
 
   /**
@@ -39,13 +51,34 @@ class ComposerExecutableValidator implements EventSubscriberInterface {
    */
   public function checkForComposerExecutable(UpdateEvent $event): void {
     try {
-      $this->executableFinder->find('composer');
+      $this->composer->run(['--version'], $this);
     }
-    catch (IOException $e) {
+    catch (ExceptionInterface $e) {
       $error = ValidationResult::createError([
         $e->getMessage(),
       ]);
       $event->addValidationResult($error);
+      return;
+    }
+
+    if ($this->version) {
+      $major_version = ExtensionVersion::createFromVersionString($this->version)
+        ->getMajorVersion();
+
+      if ($major_version < 2) {
+        $error = ValidationResult::createError([
+          $this->t('Composer 2 or later is required, but version @version was detected.', [
+            '@version' => $this->version,
+          ]),
+        ]);
+        $event->addValidationResult($error);
+      }
+    }
+    else {
+      $error = ValidationResult::createError([
+        $this->t('The Composer version could not be detected.'),
+      ]);
+      $event->addValidationResult($error);
     }
   }
 
@@ -58,4 +91,15 @@ class ComposerExecutableValidator implements EventSubscriberInterface {
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __invoke(string $type, string $buffer): void {
+    $matched = [];
+    // Search for a semantic version number and optional stability flag.
+    if (preg_match('/([0-9]+\.?){3}-?((alpha|beta|rc)[0-9]*)?/i', $buffer, $matched)) {
+      $this->version = $matched[0];
+    }
+  }
+
 }
diff --git a/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php b/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php
index 2ad27ce9ed4eaa4401507b3902a827a440aebf8d..e6fe4b76e3553267781485a275dd185a3b1efd08 100644
--- a/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php
+++ b/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php
@@ -3,10 +3,12 @@
 namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation;
 
 use Drupal\automatic_updates\Validation\ValidationResult;
+use Drupal\automatic_updates\Validator\ComposerExecutableValidator;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait;
 use PhpTuf\ComposerStager\Exception\IOException;
 use PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinderInterface;
+use Prophecy\Argument;
 
 /**
  * @covers \Drupal\automatic_updates\Validator\ComposerExecutableValidator
@@ -46,4 +48,104 @@ class ComposerExecutableValidatorTest extends KernelTestBase {
     $this->assertValidationResultsEqual([$error], $results);
   }
 
+  /**
+   * Data provider for ::testComposerVersionValidation().
+   *
+   * @return array[]
+   *   Sets of arguments to pass to the test method.
+   */
+  public function providerComposerVersionValidation(): array {
+    // Invalid or undetectable Composer versions will always produce the same
+    // error.
+    $invalid_version = ValidationResult::createError(['The Composer version could not be detected.']);
+
+    // Unsupported Composer versions will report the detected version number
+    // in the validation result, so we need a function to churn out those fake
+    // results for the test method.
+    $unsupported_version = function (string $version): ValidationResult {
+      return ValidationResult::createError([
+        "Composer 2 or later is required, but version $version was detected.",
+      ]);
+    };
+
+    return [
+      // A valid 2.x version of Composer should not produce any errors.
+      [
+        '2.1.6',
+        [],
+      ],
+      [
+        '1.10.22',
+        [$unsupported_version('1.10.22')],
+      ],
+      [
+        '1.7.3',
+        [$unsupported_version('1.7.3')],
+      ],
+      [
+        '2.0.0-alpha3',
+        [],
+      ],
+      [
+        '2.1.0-RC1',
+        [],
+      ],
+      [
+        '1.0.0-RC',
+        [$unsupported_version('1.0.0-RC')],
+      ],
+      [
+        '1.0.0-beta1',
+        [$unsupported_version('1.0.0-beta1')],
+      ],
+      [
+        '1.9-dev',
+        [$invalid_version],
+      ],
+      [
+        '@package_version@',
+        [$invalid_version],
+      ],
+    ];
+  }
+
+  /**
+   * Tests validation of various Composer versions.
+   *
+   * @param string $reported_version
+   *   The version of Composer that `composer --version` should report.
+   * @param \Drupal\automatic_updates\Validation\ValidationResult[] $expected_results
+   *   The expected validation results.
+   *
+   * @dataProvider providerComposerVersionValidation
+   */
+  public function testComposerVersionValidation(string $reported_version, array $expected_results): void {
+    // Mock the output of `composer --version`, will be passed to the validator,
+    // which is itself a callback function that gets called repeatedly as
+    // Composer produces output.
+    /** @var \PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface|\Prophecy\Prophecy\ObjectProphecy $runner */
+    $runner = $this->prophesize('\PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface');
+
+    $runner->run(['--version'], Argument::type(ComposerExecutableValidator::class))
+      // Whatever is passed to ::run() will be passed to this mock callback in
+      // $arguments, and we know exactly what that will contain: an array of
+      // command arguments for Composer, and the validator object.
+      ->will(function (array $arguments) use ($reported_version) {
+        /** @var \Drupal\automatic_updates\Validator\ComposerExecutableValidator $validator */
+        $validator = $arguments[1];
+        // Invoke the validator (which, as mentioned, is a callback function),
+        // with fake output from `composer --version`. It should try to tease a
+        // recognized, supported version number out of this output.
+        $validator($validator::OUT, "Composer version $reported_version");
+      });
+    $this->container->set('automatic_updates.composer_runner', $runner->reveal());
+
+    // If the validator can't find a recognized, supported version of Composer,
+    // it should produce errors.
+    $actual_results = $this->container->get('automatic_updates.readiness_validation_manager')
+      ->run()
+      ->getResults();
+    $this->assertValidationResultsEqual($expected_results, $actual_results);
+  }
+
 }