From 5a31c75289db801fade349805d6fcee299c28da1 Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Sat, 2 Jul 2016 15:10:33 +0200 Subject: [PATCH 1/2] Fix some of the remaining OAuth issues. - Bitbucket will silently redirect to a login page when downloading a zip. Added a check to see if the content-type is not text/html - Make the path from Basic Authentication to OAuth as smooth as possible. --- src/Composer/Util/Bitbucket.php | 2 + src/Composer/Util/RemoteFilesystem.php | 57 +++++++++++++++++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php index 688b0fba1..f8b460103 100644 --- a/src/Composer/Util/Bitbucket.php +++ b/src/Composer/Util/Bitbucket.php @@ -161,6 +161,8 @@ class Bitbucket "consumer-secret" => $consumerSecret, ); $this->config->getAuthConfigSource()->addConfigSetting('bitbucket-oauth.'.$originUrl, $consumer); + // Remove conflicting basic auth credentials (if available) + $this->config->getAuthConfigSource()->removeConfigSetting('http-basic.' . $originUrl); $this->io->writeError('Consumer stored successfully.'); diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 42026e086..9db80792f 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -175,6 +175,24 @@ class RemoteFilesystem return $value; } + /** + * @param array $headers array of returned headers + * @return string|null + */ + public function findContentType(array $headers) + { + $value = null; + foreach ($headers as $header) { + if (preg_match('/^Content-type:\s*([^;]+)/i', $header, $match)) { + // In case of redirects, http_response_headers contains the headers of all responses + // so we can not return directly and need to keep iterating + $value = $match[1]; + } + } + + return $value; + } + /** * Get file content or copy action. * @@ -334,8 +352,21 @@ class RemoteFilesystem } $statusCode = null; + $contentType = null; if (!empty($http_response_header[0])) { $statusCode = $this->findStatusCode($http_response_header); + $contentType = $this->findContentType($http_response_header); + } + + if ($originUrl === 'bitbucket.org' && + preg_match('/\.zip$/', $fileUrl) && + $contentType === 'text/html' + ) { + // The received content is a login page asking to authenticate + $result = false; + if ($this->retryAuthFailure) { + $this->promptAuthAndRetry(401); + } } // handle 3xx redirects for php<5.6, 304 Not Modified is excluded @@ -575,24 +606,30 @@ class RemoteFilesystem throw new TransportException('Could not authenticate against '.$this->originUrl, 401); } } elseif ($this->config && $this->originUrl === 'bitbucket.org') { - if (! $this->io->hasAuthentication($this->originUrl)) { - $message = "\n".'Could not fetch ' . $this->fileUrl . ', please create a bitbucket OAuth token to access private repos'; - $bitBucketUtil = new Bitbucket($this->io, $this->config); - if (! $bitBucketUtil->authorizeOAuth($this->originUrl) - && (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($this->originUrl, $message)) - ) { - throw new TransportException('Could not authenticate against ' . $this->originUrl, 401); - } - } else { + $askForOAuthToken = true; + if ($this->io->hasAuthentication($this->originUrl)) { $auth = $this->io->getAuthentication($this->originUrl); if ($auth['username'] !== 'x-token-auth') { $bitbucketUtil = new Bitbucket($this->io, $this->config); $token = $bitbucketUtil->requestToken($this->originUrl, $auth['username'], $auth['password']); - $this->io->setAuthentication($this->originUrl, 'x-token-auth', $token['access_token']); + if (! empty($token)) { + $this->io->setAuthentication($this->originUrl, 'x-token-auth', $token['access_token']); + $askForOAuthToken = false; + } } else { throw new TransportException('Could not authenticate against ' . $this->originUrl, 401); } } + + if ($askForOAuthToken) { + $message = "\n".'Could not fetch ' . $this->fileUrl . ', please create a bitbucket OAuth token to ' . ($httpStatus === 401 ? 'to access private repos' : 'to go over the API rate limit'); + $bitBucketUtil = new Bitbucket($this->io, $this->config); + if (! $bitBucketUtil->authorizeOAuth($this->originUrl) + && (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($this->originUrl, $message)) + ) { + throw new TransportException('Could not authenticate against ' . $this->originUrl, 401); + } + } } else { // 404s are only handled for github if ($httpStatus === 404) { From 9b00713a67b8263853776fcbc67f99cec435e201 Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Sat, 2 Jul 2016 17:03:01 +0200 Subject: [PATCH 2/2] Update unit test for Bitbucket util --- tests/Composer/Test/Util/BitbucketTest.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/Composer/Test/Util/BitbucketTest.php b/tests/Composer/Test/Util/BitbucketTest.php index 9f6cd2a5f..45b4ce752 100644 --- a/tests/Composer/Test/Util/BitbucketTest.php +++ b/tests/Composer/Test/Util/BitbucketTest.php @@ -164,10 +164,11 @@ class BitbucketTest extends \PHPUnit_Framework_TestCase ->willReturn(sprintf('{}', $this->token)) ; + $authJson = $this->getAuthJsonMock(); $this->config - ->expects($this->exactly(2)) + ->expects($this->exactly(3)) ->method('getAuthConfigSource') - ->willReturn($this->getAuthJsonMock()) + ->willReturn($authJson) ; $this->config ->expects($this->once()) @@ -175,6 +176,20 @@ class BitbucketTest extends \PHPUnit_Framework_TestCase ->willReturn($this->getConfJsonMock()) ; + $authJson->expects($this->once()) + ->method('addConfigSetting') + ->with( + 'bitbucket-oauth.'.$this->origin, + array( + 'consumer-key' => $this->consumer_key, + 'consumer-secret' => $this->consumer_secret + ) + ); + + $authJson->expects($this->once()) + ->method('removeConfigSetting') + ->with('http-basic.'.$this->origin); + $this->assertTrue($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); }