1
0
Fork 0

Store access-token for re-use

Store the Bitbucket access-token (and the expiration time) so it can be re-used within the time it is valid.
The Bitbucket::requestToken and Bitbucket::getToken now only return the access-token and not all other parameters it receives from the Bitbucket API.
pull/6094/head
Stefan Grootscholten 2016-12-27 00:39:02 +01:00
parent 810267e2a7
commit a4af559ca8
5 changed files with 134 additions and 84 deletions

View File

@ -79,7 +79,9 @@ class Config
private $config; private $config;
private $baseDir; private $baseDir;
private $repositories; private $repositories;
/** @var ConfigSourceInterface */
private $configSource; private $configSource;
/** @var ConfigSourceInterface */
private $authConfigSource; private $authConfigSource;
private $useEnvironment; private $useEnvironment;
private $warnedHosts = array(); private $warnedHosts = array();

View File

@ -27,6 +27,7 @@ class Bitbucket
private $process; private $process;
private $remoteFilesystem; private $remoteFilesystem;
private $token = array(); private $token = array();
private $time;
const OAUTH2_ACCESS_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token'; const OAUTH2_ACCESS_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token';
@ -37,21 +38,26 @@ class Bitbucket
* @param Config $config The composer configuration * @param Config $config The composer configuration
* @param ProcessExecutor $process Process instance, injectable for mocking * @param ProcessExecutor $process Process instance, injectable for mocking
* @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking
* @param int $time Timestamp, injectable for mocking
*/ */
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null) public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null, $time = null)
{ {
$this->io = $io; $this->io = $io;
$this->config = $config; $this->config = $config;
$this->process = $process ?: new ProcessExecutor; $this->process = $process ?: new ProcessExecutor;
$this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config); $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config);
$this->time = $time;
} }
/** /**
* @return array * @return string
*/ */
public function getToken() public function getToken()
{ {
return $this->token; if (! isset($this->token['access_token'])) {
return '';
}
return $this->token['access_token'];
} }
/** /**
@ -109,6 +115,8 @@ class Bitbucket
throw $e; throw $e;
} }
return true;
} }
/** /**
@ -151,16 +159,13 @@ class Bitbucket
$this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret); $this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret);
$this->requestAccessToken($originUrl); if (! $this->requestAccessToken($originUrl)) {
return false;
}
// store value in user config // store value in user config
$this->config->getConfigSource()->removeConfigSetting('bitbucket-oauth.'.$originUrl); $this->storeInAuthConfig($originUrl, $consumerKey, $consumerSecret);
$consumer = array(
"consumer-key" => $consumerKey,
"consumer-secret" => $consumerSecret,
);
$this->config->getAuthConfigSource()->addConfigSetting('bitbucket-oauth.'.$originUrl, $consumer);
// Remove conflicting basic auth credentials (if available) // Remove conflicting basic auth credentials (if available)
$this->config->getAuthConfigSource()->removeConfigSetting('http-basic.' . $originUrl); $this->config->getAuthConfigSource()->removeConfigSetting('http-basic.' . $originUrl);
@ -175,17 +180,66 @@ class Bitbucket
* @param string $originUrl * @param string $originUrl
* @param string $consumerKey * @param string $consumerKey
* @param string $consumerSecret * @param string $consumerSecret
* @return array * @return string
*/ */
public function requestToken($originUrl, $consumerKey, $consumerSecret) public function requestToken($originUrl, $consumerKey, $consumerSecret)
{ {
if (!empty($this->token)) { if (!empty($this->token) || $this->getTokenFromConfig($originUrl)) {
return $this->token; return $this->token['access_token'];
} }
$this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret); $this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret);
$this->requestAccessToken($originUrl); if (! $this->requestAccessToken($originUrl)) {
return '';
}
return $this->token; $this->storeInAuthConfig($originUrl, $consumerKey, $consumerSecret);
return $this->token['access_token'];
}
/**
* Store the new/updated credentials to the configuration
* @param string $originUrl
* @param string $consumerKey
* @param string $consumerSecret
*/
private function storeInAuthConfig($originUrl, $consumerKey, $consumerSecret)
{
$this->config->getConfigSource()->removeConfigSetting('bitbucket-oauth.'.$originUrl);
$time = null === $this->time ? time() : $this->time;
$consumer = array(
"consumer-key" => $consumerKey,
"consumer-secret" => $consumerSecret,
"access-token" => $this->token['access_token'],
"access-token-expiration" => $time + $this->token['expires_in']
);
$this->config->getAuthConfigSource()->addConfigSetting('bitbucket-oauth.'.$originUrl, $consumer);
}
/**
* @param string $originUrl
* @return bool
*/
private function getTokenFromConfig($originUrl)
{
$authConfig = $this->config->get('bitbucket-oauth');
if (empty($authConfig) ||
! isset($authConfig[$originUrl]) ||
! isset($authConfig[$originUrl]['access-token']) ||
! isset($authConfig[$originUrl]['access-token-expiration']) ||
time() > $authConfig[$originUrl]['access-token-expiration']
) {
return false;
}
$this->token = array(
'access_token' => $authConfig[$originUrl]['access-token']
);
return true;
} }
} }

View File

@ -122,17 +122,17 @@ class Git
if (!$bitbucketUtil->authorizeOAuth($match[1]) && $this->io->isInteractive()) { if (!$bitbucketUtil->authorizeOAuth($match[1]) && $this->io->isInteractive()) {
$bitbucketUtil->authorizeOAuthInteractively($match[1], $message); $bitbucketUtil->authorizeOAuthInteractively($match[1], $message);
$token = $bitbucketUtil->getToken(); $access_token = $bitbucketUtil->getToken();
$this->io->setAuthentication($match[1], 'x-token-auth', $token['access_token']); $this->io->setAuthentication($match[1], 'x-token-auth', $access_token);
} }
} else { //We're authenticating with a locally stored consumer. } else { //We're authenticating with a locally stored consumer.
$auth = $this->io->getAuthentication($match[1]); $auth = $this->io->getAuthentication($match[1]);
//We already have an access_token from a previous request. //We already have an access_token from a previous request.
if ($auth['username'] !== 'x-token-auth') { if ($auth['username'] !== 'x-token-auth') {
$token = $bitbucketUtil->requestToken($match[1], $auth['username'], $auth['password']); $access_token = $bitbucketUtil->requestToken($match[1], $auth['username'], $auth['password']);
if (!empty($token)) { if (! empty($access_token)) {
$this->io->setAuthentication($match[1], 'x-token-auth', $token['access_token']); $this->io->setAuthentication($match[1], 'x-token-auth', $access_token);
} }
} }
} }

View File

@ -601,9 +601,9 @@ class RemoteFilesystem
$auth = $this->io->getAuthentication($this->originUrl); $auth = $this->io->getAuthentication($this->originUrl);
if ($auth['username'] !== 'x-token-auth') { if ($auth['username'] !== 'x-token-auth') {
$bitbucketUtil = new Bitbucket($this->io, $this->config); $bitbucketUtil = new Bitbucket($this->io, $this->config);
$token = $bitbucketUtil->requestToken($this->originUrl, $auth['username'], $auth['password']); $access_token = $bitbucketUtil->requestToken($this->originUrl, $auth['username'], $auth['password']);
if (! empty($token)) { if (! empty($access_token)) {
$this->io->setAuthentication($this->originUrl, 'x-token-auth', $token['access_token']); $this->io->setAuthentication($this->originUrl, 'x-token-auth', $access_token);
$askForOAuthToken = false; $askForOAuthToken = false;
} }
} else { } else {

View File

@ -35,6 +35,8 @@ class BitbucketTest extends \PHPUnit_Framework_TestCase
private $config; private $config;
/** @type Bitbucket */ /** @type Bitbucket */
private $bitbucket; private $bitbucket;
/** @var int */
private $time;
protected function setUp() protected function setUp()
{ {
@ -52,7 +54,9 @@ class BitbucketTest extends \PHPUnit_Framework_TestCase
$this->config = $this->getMock('Composer\Config'); $this->config = $this->getMock('Composer\Config');
$this->bitbucket = new Bitbucket($this->io, $this->config, null, $this->rfs); $this->time = time();
$this->bitbucket = new Bitbucket($this->io, $this->config, null, $this->rfs, $this->time);
} }
public function testRequestAccessTokenWithValidOAuthConsumer() public function testRequestAccessTokenWithValidOAuthConsumer()
@ -82,14 +86,15 @@ class BitbucketTest extends \PHPUnit_Framework_TestCase
) )
); );
$this->config->expects($this->once())
->method('get')
->with('bitbucket-oauth')
->willReturn(null);
$this->setExpectationsForStoringAccessToken();
$this->assertEquals( $this->assertEquals(
array( $this->token,
'access_token' => $this->token,
'scopes' => 'repository',
'expires_in' => 3600,
'refresh_token' => 'refreshtoken',
'token_type' => 'bearer'
),
$this->bitbucket->requestToken($this->origin, $this->consumer_key, $this->consumer_secret) $this->bitbucket->requestToken($this->origin, $this->consumer_key, $this->consumer_secret)
); );
} }
@ -133,7 +138,12 @@ class BitbucketTest extends \PHPUnit_Framework_TestCase
) )
); );
$this->assertEquals(array(), $this->bitbucket->requestToken($this->origin, $this->username, $this->password)); $this->config->expects($this->once())
->method('get')
->with('bitbucket-oauth')
->willReturn(null);
$this->assertEquals('', $this->bitbucket->requestToken($this->origin, $this->username, $this->password));
} }
public function testUsernamePasswordAuthenticationFlow() public function testUsernamePasswordAuthenticationFlow()
@ -161,67 +171,51 @@ class BitbucketTest extends \PHPUnit_Framework_TestCase
$this->isFalse(), $this->isFalse(),
$this->anything() $this->anything()
) )
->willReturn(sprintf('{}', $this->token)) ->willReturn(
; sprintf(
'{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refresh_token", "token_type": "bearer"}',
$authJson = $this->getAuthJsonMock(); $this->token
$this->config
->expects($this->exactly(3))
->method('getAuthConfigSource')
->willReturn($authJson)
;
$this->config
->expects($this->once())
->method('getConfigSource')
->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()) $this->setExpectationsForStoringAccessToken(true);
->method('removeConfigSetting')
->with('http-basic.'.$this->origin);
$this->assertTrue($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); $this->assertTrue($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message));
} }
private function getAuthJsonMock() private function setExpectationsForStoringAccessToken($removeBasicAuth = false)
{ {
$authjson = $this $configSourceMock = $this->getMock('Composer\Config\ConfigSourceInterface');
->getMockBuilder('Composer\Config\JsonConfigSource') $this->config->expects($this->once())
->disableOriginalConstructor() ->method('getConfigSource')
->getMock() ->willReturn($configSourceMock);
;
$authjson
->expects($this->atLeastOnce())
->method('getName')
->willReturn('auth.json')
;
return $authjson; $configSourceMock->expects($this->once())
}
private function getConfJsonMock()
{
$confjson = $this
->getMockBuilder('Composer\Config\JsonConfigSource')
->disableOriginalConstructor()
->getMock()
;
$confjson
->expects($this->atLeastOnce())
->method('removeConfigSetting') ->method('removeConfigSetting')
->with('bitbucket-oauth.'.$this->origin) ->with('bitbucket-oauth.' . $this->origin);
;
return $confjson; $authConfigSourceMock = $this->getMock('Composer\Config\ConfigSourceInterface');
$this->config->expects($this->atLeastOnce())
->method('getAuthConfigSource')
->willReturn($authConfigSourceMock);
$authConfigSourceMock->expects($this->once())
->method('addConfigSetting')
->with(
'bitbucket-oauth.' . $this->origin,
array(
"consumer-key" => $this->consumer_key,
"consumer-secret" => $this->consumer_secret,
"access-token" => $this->token,
"access-token-expiration" => $this->time + 3600
)
);
if ($removeBasicAuth) {
$authConfigSourceMock->expects($this->once())
->method('removeConfigSetting')
->with('http-basic.' . $this->origin);
}
} }
} }