diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index 148122fd1..85bfe7608 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -79,11 +79,12 @@ class AuthHelper * @param int $statusCode HTTP status code that triggered this call * @param string|null $reason a message/description explaining why this was called * @param string[] $headers + * @param int $retryCount the amount of retries already done on this URL * @return array containing retry (bool) and storeAuth (string|bool) keys, if retry is true the request should be * retried, if storeAuth is true then on a successful retry the authentication should be persisted to auth.json * @phpstan-return array{retry: bool, storeAuth: 'prompt'|bool} */ - public function promptAuthIfNeeded(string $url, string $origin, int $statusCode, ?string $reason = null, array $headers = array()): array + public function promptAuthIfNeeded(string $url, string $origin, int $statusCode, ?string $reason = null, array $headers = array(), int $retryCount = 0): array { $storeAuth = false; @@ -200,8 +201,15 @@ class AuthHelper throw new TransportException($message, $statusCode); } + // fail if we already have auth if ($this->io->hasAuthentication($origin)) { + // if two or more requests are started together for the same host, and the first + // received authentication already, we let the others retry before failing them + if ($retryCount === 0) { + return array('retry' => true, 'storeAuth' => false); + } + throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode); } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 93b76f305..6fe22e713 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -531,7 +531,7 @@ class CurlDownloader private function isAuthenticatedRetryNeeded(array $job, Response $response): array { if (in_array($response->getStatusCode(), array(401, 403)) && $job['attributes']['retryAuthFailure']) { - $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], $response->getStatusCode(), $response->getStatusMessage(), $response->getHeaders()); + $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], $response->getStatusCode(), $response->getStatusMessage(), $response->getHeaders(), $job['attributes']['retries']); if ($result['retry']) { return $result; @@ -563,7 +563,7 @@ class CurlDownloader if ($needsAuthRetry) { if ($job['attributes']['retryAuthFailure']) { - $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], 401); + $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], 401, null, array(), $job['attributes']['retries']); if ($result['retry']) { return $result; } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index f7daa283d..00c68f629 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -594,7 +594,7 @@ class RemoteFilesystem */ protected function promptAuthAndRetry($httpStatus, ?string $reason = null, array $headers = array()) { - $result = $this->authHelper->promptAuthIfNeeded($this->fileUrl, $this->originUrl, $httpStatus, $reason, $headers); + $result = $this->authHelper->promptAuthIfNeeded($this->fileUrl, $this->originUrl, $httpStatus, $reason, $headers, 1 /** always pass 1 as RemoteFilesystem is single threaded there is no race condition possible */); $this->storeAuth = $result['storeAuth']; $this->retry = $result['retry'];