From 9d965b9c65fa761f948abe141c7f78b2df71af1d Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Tue, 23 May 2023 23:06:48 +0200 Subject: [PATCH] Fix authentication issues with private bitbucket repos (#11464) --- src/Composer/Util/AuthHelper.php | 8 +++ tests/Composer/Test/Util/AuthHelperTest.php | 65 +++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index ec3476670..3f0312602 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -28,6 +28,8 @@ class AuthHelper protected $config; /** @var array Map of origins to message displayed */ private $displayedOriginAuthentications = []; + /** @var array Map of URLs and whether they already retried with authentication from Bitbucket */ + private $bitbucketRetry = []; public function __construct(IOInterface $io, Config $config) { @@ -164,6 +166,12 @@ class AuthHelper $this->io->setAuthentication($origin, 'x-token-auth', $accessToken); $askForOAuthToken = false; } + } elseif (!isset($this->bitbucketRetry[$url])) { + // when multiple requests fire at the same time, they will all fail and the first one resets the token to be correct above but then the others + // reach the code path and without this fallback they would end up throwing below + // see https://github.com/composer/composer/pull/11464 for more details + $askForOAuthToken = false; + $this->bitbucketRetry[$url] = true; } else { throw new TransportException('Could not authenticate against ' . $origin, 401); } diff --git a/tests/Composer/Test/Util/AuthHelperTest.php b/tests/Composer/Test/Util/AuthHelperTest.php index e9dd992c4..ee5802321 100644 --- a/tests/Composer/Test/Util/AuthHelperTest.php +++ b/tests/Composer/Test/Util/AuthHelperTest.php @@ -538,6 +538,71 @@ class AuthHelperTest extends TestCase $this->authHelper->promptAuthIfNeeded('https://gitlab.com/acme/archive.zip', $origin, 404, 'GitLab requires authentication and it was not provided'); } + public function testPromptAuthIfNeededMultipleBitbucketDownloads(): void + { + $origin = 'bitbucket.org'; + + $expectedResult = [ + 'retry' => true, + 'storeAuth' => false, + ]; + + $authConfig = [ + 'bitbucket.org' => [ + 'access-token' => 'bitbucket_access_token', + 'access-token-expiration' => time() + 1800, + ] + ]; + + $this->config + ->method('get') + ->willReturnMap([ + ['github-domains', 0, []], + ['gitlab-domains', 0, []], + ['bitbucket-oauth', 0, $authConfig], + ['github-domains', 0, []], + ['gitlab-domains', 0, []], + ]); + + $this->io + ->expects($this->exactly(2)) + ->method('hasAuthentication') + ->with($origin) + ->willReturn(true); + + $getAuthenticationReturnValues = [ + ['username' => 'bitbucket_client_id', 'password' => 'bitbucket_client_secret'], + ['username' => 'x-token-auth', 'password' => 'bitbucket_access_token'], + ]; + + $this->io + ->expects($this->exactly(2)) + ->method('getAuthentication') + ->willReturnCallback( + function ($repositoryName) use (&$getAuthenticationReturnValues) { + return array_shift($getAuthenticationReturnValues); + } + ); + + $this->io + ->expects($this->once()) + ->method('setAuthentication') + ->with($origin, 'x-token-auth', 'bitbucket_access_token'); + + $result1 = $this->authHelper->promptAuthIfNeeded('https://bitbucket.org/workspace/repo1/get/hash1.zip', $origin, 401, 'HTTP/2 401 '); + $result2 = $this->authHelper->promptAuthIfNeeded('https://bitbucket.org/workspace/repo2/get/hash2.zip', $origin, 401, 'HTTP/2 401 '); + + $this->assertSame( + $expectedResult, + $result1 + ); + + $this->assertSame( + $expectedResult, + $result2 + ); + } + /** * @param array $auth *