Skip to content

Commit

Permalink
feat: workforce credentials (#485)
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer authored Feb 1, 2024
1 parent e06d045 commit c1b240f
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 37 deletions.
95 changes: 91 additions & 4 deletions src/Credentials/ExternalAccountCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,34 @@
use Google\Auth\ExternalAccountCredentialSourceInterface;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\GetUniverseDomainInterface;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\OAuth2;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\UpdateMetadataInterface;
use Google\Auth\UpdateMetadataTrait;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;

class ExternalAccountCredentials implements FetchAuthTokenInterface, UpdateMetadataInterface, GetQuotaProjectInterface
class ExternalAccountCredentials implements
FetchAuthTokenInterface,
UpdateMetadataInterface,
GetQuotaProjectInterface,
GetUniverseDomainInterface,
ProjectIdProviderInterface
{
use UpdateMetadataTrait;

private const EXTERNAL_ACCOUNT_TYPE = 'external_account';
private const CLOUD_RESOURCE_MANAGER_URL='https://cloudresourcemanager.UNIVERSE_DOMAIN/v1/projects/%s';

private OAuth2 $auth;
private ?string $quotaProject;
private ?string $serviceAccountImpersonationUrl;
private ?string $workforcePoolUserProject;
private ?string $projectId;
private string $universeDomain;

/**
* @param string|string[] $scope The scope of the access request, expressed either as an array
Expand Down Expand Up @@ -90,14 +101,25 @@ public function __construct(
}

$this->quotaProject = $jsonKey['quota_project_id'] ?? null;
$this->workforcePoolUserProject = $jsonKey['workforce_pool_user_project'] ?? null;
$this->universeDomain = $jsonKey['universe_domain'] ?? GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN;

$this->auth = new OAuth2([
'tokenCredentialUri' => $jsonKey['token_url'],
'audience' => $jsonKey['audience'],
'scope' => $scope,
'subjectTokenType' => $jsonKey['subject_token_type'],
'subjectTokenFetcher' => self::buildCredentialSource($jsonKey),
'additionalOptions' => $this->workforcePoolUserProject
? ['userProject' => $this->workforcePoolUserProject]
: [],
]);

if (!$this->isWorkforcePool() && $this->workforcePoolUserProject) {
throw new InvalidArgumentException(
'workforce_pool_user_project should not be set for non-workforce pool credentials.'
);
}
}

/**
Expand Down Expand Up @@ -154,6 +176,7 @@ private static function buildCredentialSource(array $jsonKey): ExternalAccountCr

throw new InvalidArgumentException('Unable to determine credential source from json key.');
}

/**
* @param string $stsToken
* @param callable $httpHandler
Expand Down Expand Up @@ -181,7 +204,7 @@ private function getImpersonatedAccessToken(string $stsToken, callable $httpHand
],
(string) json_encode([
'lifetime' => sprintf('%ss', OAuth2::DEFAULT_EXPIRY_SECONDS),
'scope' => $this->auth->getScope(),
'scope' => explode(' ', $this->auth->getScope()),
]),
);
if (is_null($httpHandler)) {
Expand All @@ -190,8 +213,8 @@ private function getImpersonatedAccessToken(string $stsToken, callable $httpHand
$response = $httpHandler($request);
$body = json_decode((string) $response->getBody(), true);
return [
'access_token' => $body['accessToken'],
'expires_at' => strtotime($body['expireTime']),
'access_token' => $body['accessToken'],
'expires_at' => strtotime($body['expireTime']),
];
}

Expand Down Expand Up @@ -238,4 +261,68 @@ public function getQuotaProject()
{
return $this->quotaProject;
}

/**
* Get the universe domain used for this API request
*
* @return string
*/
public function getUniverseDomain(): string
{
return $this->universeDomain;
}

/**
* Get the project ID.
*
* @param callable $httpHandler Callback which delivers psr7 request
* @param string $accessToken The access token to use to sign the blob. If
* provided, saves a call to the metadata server for a new access
* token. **Defaults to** `null`.
* @return string|null
*/
public function getProjectId(callable $httpHandler = null, string $accessToken = null)
{
if (isset($this->projectId)) {
return $this->projectId;
}

$projectNumber = $this->getProjectNumber() ?: $this->workforcePoolUserProject;
if (!$projectNumber) {
return null;
}

if (is_null($httpHandler)) {
$httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
}

$url = str_replace(
'UNIVERSE_DOMAIN',
$this->getUniverseDomain(),
sprintf(self::CLOUD_RESOURCE_MANAGER_URL, $projectNumber)
);

if (is_null($accessToken)) {
$accessToken = $this->fetchAuthToken($httpHandler)['access_token'];
}

$request = new Request('GET', $url, ['authorization' => 'Bearer ' . $accessToken]);
$response = $httpHandler($request);

$body = json_decode((string) $response->getBody(), true);
return $this->projectId = $body['projectId'];
}

private function getProjectNumber(): ?string
{
$parts = explode('/', $this->auth->getAudience());
$i = array_search('projects', $parts);
return $parts[$i + 1] ?? null;
}

private function isWorkforcePool(): bool
{
$regex = '#//iam\.googleapis\.com/locations/[^/]+/workforcePools/#';
return preg_match($regex, $this->auth->getAudience()) === 1;
}
}
18 changes: 15 additions & 3 deletions src/FetchAuthTokenCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,12 @@ public function signBlob($stringToSign, $forceOpenSsl = false)
);
}

// Pass the access token from cache to GCECredentials for signing a blob.
// This saves a call to the metadata server when a cached token exists.
if ($this->fetcher instanceof Credentials\GCECredentials) {
// Pass the access token from cache for credentials that sign blobs
// using the IAM API. This saves a call to fetch an access token when a
// cached token exists.
if ($this->fetcher instanceof Credentials\GCECredentials
|| $this->fetcher instanceof Credentials\ImpersonatedServiceAccountCredentials
) {
$cached = $this->fetchAuthTokenFromCache();
$accessToken = $cached['access_token'] ?? null;
return $this->fetcher->signBlob($stringToSign, $forceOpenSsl, $accessToken);
Expand Down Expand Up @@ -189,6 +192,15 @@ public function getProjectId(callable $httpHandler = null)
);
}

// Pass the access token from cache for credentials that require an
// access token to fetch the project ID. This saves a call to fetch an
// access token when a cached token exists.
if ($this->fetcher instanceof Credentials\ExternalAccountCredentials) {
$cached = $this->fetchAuthTokenFromCache();
$accessToken = $cached['access_token'] ?? null;
return $this->fetcher->getProjectId($httpHandler, $accessToken);
}

return $this->fetcher->getProjectId($httpHandler);
}

Expand Down
13 changes: 13 additions & 0 deletions src/OAuth2.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,14 @@ class OAuth2 implements FetchAuthTokenInterface
*/
private ?string $issuedTokenType = null;

/**
* From STS response.
* An identifier for the representation of the issued security token.
*
* @var array<mixed>
*/
private array $additionalOptions;

/**
* Create a new OAuthCredentials.
*
Expand Down Expand Up @@ -438,6 +446,7 @@ public function __construct(array $config)
'subjectTokenType' => null,
'actorToken' => null,
'actorTokenType' => null,
'additionalOptions' => [],
], $config);

$this->setAuthorizationUri($opts['authorizationUri']);
Expand Down Expand Up @@ -466,6 +475,7 @@ public function __construct(array $config)
$this->subjectTokenType = $opts['subjectTokenType'];
$this->actorToken = $opts['actorToken'];
$this->actorTokenType = $opts['actorTokenType'];
$this->additionalOptions = $opts['additionalOptions'];

$this->updateToken($opts);
}
Expand Down Expand Up @@ -616,6 +626,9 @@ public function generateCredentialsRequest(callable $httpHandler = null)
'actor_token' => $this->actorToken,
'actor_token_type' => $this->actorTokenType,
]);
if ($this->additionalOptions) {
$params['options'] = json_encode($this->additionalOptions);
}
break;
default:
if (!is_null($this->getRedirectUri())) {
Expand Down
Loading

0 comments on commit c1b240f

Please sign in to comment.