Added token refresh for GitLab to support GitLab 15+ (#10988)
Co-authored-by: Jordi Boggiano <j.boggiano@seld.be>pull/10996/head
parent
41a13fa0a1
commit
41d6467b3b
|
@ -4585,7 +4585,7 @@ parameters:
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#"
|
message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#"
|
||||||
count: 1
|
count: 2
|
||||||
path: ../src/Composer/Util/GitLab.php
|
path: ../src/Composer/Util/GitLab.php
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
|
@ -340,9 +340,24 @@
|
||||||
},
|
},
|
||||||
"gitlab-oauth": {
|
"gitlab-oauth": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "An object of domain name => gitlab API oauth tokens, typically {\"gitlab.com\":\"<token>\"}.",
|
"description": "An object of domain name => gitlab API oauth tokens, typically {\"gitlab.com\":{\"expires-at\":\"<expiration date>\", \"refresh-token\":\"<refresh token>\", \"token\":\"<token>\"}}.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": ["string", "object"],
|
||||||
|
"required": [ "token"],
|
||||||
|
"properties": {
|
||||||
|
"expires-at": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "The expiration date for this GitLab token"
|
||||||
|
},
|
||||||
|
"refresh-token": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The refresh token used for GitLab authentication"
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The token used for GitLab authentication"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gitlab-token": {
|
"gitlab-token": {
|
||||||
|
|
|
@ -137,6 +137,7 @@ abstract class BaseIO implements IOInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($gitlabOauth as $domain => $token) {
|
foreach ($gitlabOauth as $domain => $token) {
|
||||||
|
$token = is_array($token) ? $token["token"] : $token;
|
||||||
$this->checkAndSetAuthentication($domain, $token, 'oauth2');
|
$this->checkAndSetAuthentication($domain, $token, 'oauth2');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -537,6 +537,10 @@ class GitLabDriver extends VcsDriver
|
||||||
return parent::getContents($url);
|
return parent::getContents($url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($gitLabUtil->isOAuthExpired($this->originUrl) && $gitLabUtil->authorizeOAuthRefresh($this->scheme, $this->originUrl)) {
|
||||||
|
return parent::getContents($url);
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->io->isInteractive()) {
|
if (!$this->io->isInteractive()) {
|
||||||
$this->attemptCloneFallback();
|
$this->attemptCloneFallback();
|
||||||
|
|
||||||
|
|
|
@ -126,7 +126,8 @@ class GitLab
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->io->writeError(sprintf('A token will be created and stored in "%s", your password will never be stored', $this->config->getAuthConfigSource()->getName()));
|
$this->io->writeError(sprintf('A token will be created and stored in "%s", your password will never be stored', $this->config->getAuthConfigSource()->getName()));
|
||||||
$this->io->writeError('To revoke access to this token you can visit '.$scheme.'://'.$originUrl.'/-/profile/personal_access_tokens');
|
$this->io->writeError('To revoke access to this token you can visit '.$scheme.'://'.$originUrl.'/-/profile/applications');
|
||||||
|
$this->io->writeError('Alternatively you can setup an personal access token on '.$scheme.'://'.$originUrl.'/-/profile/personal_access_token and store it under "gitlab-token" see https://getcomposer.org/doc/articles/authentication-for-private-packages.md#gitlab-token for more details.');
|
||||||
|
|
||||||
$attemptCounter = 0;
|
$attemptCounter = 0;
|
||||||
|
|
||||||
|
@ -160,7 +161,18 @@ class GitLab
|
||||||
$this->io->setAuthentication($originUrl, $response['access_token'], 'oauth2');
|
$this->io->setAuthentication($originUrl, $response['access_token'], 'oauth2');
|
||||||
|
|
||||||
// store value in user config in auth file
|
// store value in user config in auth file
|
||||||
|
if (isset($response['expires_in'])) {
|
||||||
|
$this->config->getAuthConfigSource()->addConfigSetting(
|
||||||
|
'gitlab-oauth.'.$originUrl,
|
||||||
|
[
|
||||||
|
'expires-at' => intval($response['created_at']) + intval($response['expires_in']),
|
||||||
|
'refresh-token' => $response['refresh_token'],
|
||||||
|
'token' => $response['access_token'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
$this->config->getAuthConfigSource()->addConfigSetting('gitlab-oauth.'.$originUrl, $response['access_token']);
|
$this->config->getAuthConfigSource()->addConfigSetting('gitlab-oauth.'.$originUrl, $response['access_token']);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -168,11 +180,46 @@ class GitLab
|
||||||
throw new \RuntimeException('Invalid GitLab credentials 5 times in a row, aborting.');
|
throw new \RuntimeException('Invalid GitLab credentials 5 times in a row, aborting.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorizes a GitLab domain interactively via OAuth.
|
||||||
|
*
|
||||||
|
* @param string $scheme Scheme used in the origin URL
|
||||||
|
* @param string $originUrl The host this GitLab instance is located at
|
||||||
|
*
|
||||||
|
* @throws \RuntimeException
|
||||||
|
* @throws TransportException|\Exception
|
||||||
|
*
|
||||||
|
* @return bool true on success
|
||||||
|
*/
|
||||||
|
public function authorizeOAuthRefresh(string $scheme, string $originUrl): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$response = $this->refreshToken($scheme, $originUrl);
|
||||||
|
} catch (TransportException $e) {
|
||||||
|
$this->io->writeError("Couldn't refresh access token: ".$e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->io->setAuthentication($originUrl, $response['access_token'], 'oauth2');
|
||||||
|
|
||||||
|
// store value in user config in auth file
|
||||||
|
$this->config->getAuthConfigSource()->addConfigSetting(
|
||||||
|
'gitlab-oauth.'.$originUrl,
|
||||||
|
[
|
||||||
|
'expires-at' => intval($response['created_at']) + intval($response['expires_in']),
|
||||||
|
'refresh-token' => $response['refresh_token'],
|
||||||
|
'token' => $response['access_token'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $scheme
|
* @param string $scheme
|
||||||
* @param string $originUrl
|
* @param string $originUrl
|
||||||
*
|
*
|
||||||
* @return array{access_token: non-empty-string, token_type: non-empty-string, expires_in: positive-int}
|
* @return array{access_token: non-empty-string, refresh_token: non-empty-string, token_type: non-empty-string, expires_in?: positive-int, created_at: positive-int}
|
||||||
*
|
*
|
||||||
* @see https://docs.gitlab.com/ee/api/oauth2.html#resource-owner-password-credentials-flow
|
* @see https://docs.gitlab.com/ee/api/oauth2.html#resource-owner-password-credentials-flow
|
||||||
*/
|
*/
|
||||||
|
@ -204,4 +251,59 @@ class GitLab
|
||||||
|
|
||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the OAuth access token expired?
|
||||||
|
*
|
||||||
|
* @return bool true on expired token, false if token is fresh or expiration date is not set
|
||||||
|
*/
|
||||||
|
public function isOAuthExpired(string $originUrl): bool
|
||||||
|
{
|
||||||
|
$authTokens = $this->config->get('gitlab-oauth');
|
||||||
|
if (isset($authTokens[$originUrl]['expires-at'])) {
|
||||||
|
if ($authTokens[$originUrl]['expires-at'] < time()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $scheme
|
||||||
|
* @param string $originUrl
|
||||||
|
*
|
||||||
|
* @return array{access_token: non-empty-string, refresh_token: non-empty-string, token_type: non-empty-string, expires_in: positive-int, created_at: positive-int}
|
||||||
|
*
|
||||||
|
* @see https://docs.gitlab.com/ee/api/oauth2.html#resource-owner-password-credentials-flow
|
||||||
|
*/
|
||||||
|
private function refreshToken(string $scheme, string $originUrl): array
|
||||||
|
{
|
||||||
|
$authTokens = $this->config->get('gitlab-oauth');
|
||||||
|
if (!isset($authTokens[$originUrl]['refresh-token'])) {
|
||||||
|
throw new \RuntimeException('No GitLab refresh token present for '.$originUrl.'.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$refreshToken = $authTokens[$originUrl]['refresh-token'];
|
||||||
|
$headers = array('Content-Type: application/x-www-form-urlencoded');
|
||||||
|
|
||||||
|
$data = http_build_query(array(
|
||||||
|
'refresh_token' => $refreshToken,
|
||||||
|
'grant_type' => 'refresh_token',
|
||||||
|
), '', '&');
|
||||||
|
$options = array(
|
||||||
|
'retry-auth-failure' => false,
|
||||||
|
'http' => array(
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => $headers,
|
||||||
|
'content' => $data,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$token = $this->httpDownloader->get($scheme.'://'.$originUrl.'/oauth/token', $options)->decodeJson();
|
||||||
|
$this->io->writeError('GitLab token successfully refreshed', true, IOInterface::VERY_VERBOSE);
|
||||||
|
$this->io->writeError('To revoke access to this token you can visit '.$scheme.'://'.$originUrl.'/-/profile/applications', true, IOInterface::VERY_VERBOSE);
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ class GitLabTest extends TestCase
|
||||||
private $origin = 'gitlab.com';
|
private $origin = 'gitlab.com';
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $token = 'gitlabtoken';
|
private $token = 'gitlabtoken';
|
||||||
|
/** @var string */
|
||||||
|
private $refreshtoken = 'gitlabrefreshtoken';
|
||||||
|
|
||||||
public function testUsernamePasswordAuthenticationFlow(): void
|
public function testUsernamePasswordAuthenticationFlow(): void
|
||||||
{
|
{
|
||||||
|
@ -54,7 +56,7 @@ class GitLabTest extends TestCase
|
||||||
|
|
||||||
$httpDownloader = $this->getHttpDownloaderMock();
|
$httpDownloader = $this->getHttpDownloaderMock();
|
||||||
$httpDownloader->expects(
|
$httpDownloader->expects(
|
||||||
[['url' => sprintf('http://%s/oauth/token', $this->origin), 'body' => sprintf('{"access_token": "%s", "token_type": "bearer", "expires_in": 7200}', $this->token)]],
|
[['url' => sprintf('http://%s/oauth/token', $this->origin), 'body' => sprintf('{"access_token": "%s", "refresh_token": "%s", "token_type": "bearer", "expires_in": 7200, "created_at": 0}', $this->token, $this->refreshtoken)]],
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue