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\\.$#"
|
||||
count: 1
|
||||
count: 2
|
||||
path: ../src/Composer/Util/GitLab.php
|
||||
|
||||
-
|
||||
|
|
|
@ -340,9 +340,24 @@
|
|||
},
|
||||
"gitlab-oauth": {
|
||||
"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": {
|
||||
"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": {
|
||||
|
|
|
@ -137,6 +137,7 @@ abstract class BaseIO implements IOInterface
|
|||
}
|
||||
|
||||
foreach ($gitlabOauth as $domain => $token) {
|
||||
$token = is_array($token) ? $token["token"] : $token;
|
||||
$this->checkAndSetAuthentication($domain, $token, 'oauth2');
|
||||
}
|
||||
|
||||
|
|
|
@ -537,6 +537,10 @@ class GitLabDriver extends VcsDriver
|
|||
return parent::getContents($url);
|
||||
}
|
||||
|
||||
if ($gitLabUtil->isOAuthExpired($this->originUrl) && $gitLabUtil->authorizeOAuthRefresh($this->scheme, $this->originUrl)) {
|
||||
return parent::getContents($url);
|
||||
}
|
||||
|
||||
if (!$this->io->isInteractive()) {
|
||||
$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('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;
|
||||
|
||||
|
@ -160,7 +161,18 @@ class GitLab
|
|||
$this->io->setAuthentication($originUrl, $response['access_token'], 'oauth2');
|
||||
|
||||
// store value in user config in auth file
|
||||
$this->config->getAuthConfigSource()->addConfigSetting('gitlab-oauth.'.$originUrl, $response['access_token']);
|
||||
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']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -168,11 +180,46 @@ class GitLab
|
|||
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 $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
|
||||
*/
|
||||
|
@ -204,4 +251,59 @@ class GitLab
|
|||
|
||||
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';
|
||||
/** @var string */
|
||||
private $token = 'gitlabtoken';
|
||||
/** @var string */
|
||||
private $refreshtoken = 'gitlabrefreshtoken';
|
||||
|
||||
public function testUsernamePasswordAuthenticationFlow(): void
|
||||
{
|
||||
|
@ -54,7 +56,7 @@ class GitLabTest extends TestCase
|
|||
|
||||
$httpDownloader = $this->getHttpDownloaderMock();
|
||||
$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
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in New Issue