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
parent
810267e2a7
commit
a4af559ca8
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue