Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/ApplicationDefaultCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ public static function getMiddleware(
* @param string|null $universeDomain Specifies a universe domain to use for the
* calling client library.
* @param null|false|LoggerInterface $logger A PSR3 compliant LoggerInterface.
* @param bool $enableTrustBoundary Lookup and include the trust boundary header.
*
* @return FetchAuthTokenInterface
* @throws DomainException if no implementation can be obtained.
Expand All @@ -166,6 +167,7 @@ public static function getCredentials(
$defaultScope = null,
?string $universeDomain = null,
null|false|LoggerInterface $logger = null,
bool $enableTrustBoundary = false
) {
$creds = null;
$jsonKey = CredentialsLoader::fromEnv()
Expand Down Expand Up @@ -196,12 +198,18 @@ public static function getCredentials(
$creds = CredentialsLoader::makeCredentials(
$scope,
$jsonKey,
$defaultScope
$defaultScope,
$enableTrustBoundary
);
} elseif (AppIdentityCredentials::onAppEngine() && !GCECredentials::onAppEngineFlexible()) {
$creds = new AppIdentityCredentials($anyScope);
} elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
$creds = new GCECredentials(null, $anyScope, null, $quotaProject, null, $universeDomain);
$creds = new GCECredentials(
scope: $anyScope,
quotaProject: $quotaProject,
universeDomain: $universeDomain,
enableTrustBoundary: $enableTrustBoundary,
);
$creds->setIsOnGce(true); // save the credentials a trip to the metadata server
}

Expand Down Expand Up @@ -286,7 +294,7 @@ public static function getIdTokenCredentials(
$targetAudience,
?callable $httpHandler = null,
?array $cacheConfig = null,
?CacheItemPoolInterface $cache = null
?CacheItemPoolInterface $cache = null,
) {
$creds = null;
$jsonKey = CredentialsLoader::fromEnv()
Expand All @@ -308,12 +316,20 @@ public static function getIdTokenCredentials(

$creds = match ($jsonKey['type']) {
'authorized_user' => new UserRefreshCredentials(null, $jsonKey, $targetAudience),
'impersonated_service_account' => new ImpersonatedServiceAccountCredentials(null, $jsonKey, $targetAudience),
'service_account' => new ServiceAccountCredentials(null, $jsonKey, null, $targetAudience),
'impersonated_service_account' => new ImpersonatedServiceAccountCredentials(
scope: null,
jsonKey: $jsonKey,
targetAudience: $targetAudience,
),
'service_account' => new ServiceAccountCredentials(
scope: null,
jsonKey: $jsonKey,
targetAudience: $targetAudience,
),
default => throw new InvalidArgumentException('invalid value in the type field')
};
} elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
$creds = new GCECredentials(null, null, $targetAudience);
$creds = new GCECredentials(targetAudience: $targetAudience);
$creds->setIsOnGce(true); // save the credentials a trip to the metadata server
}

Expand Down
5 changes: 3 additions & 2 deletions src/CacheTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ private function getCachedValue($k)
*
* @param mixed $k
* @param mixed $v
* @param int|null $lifetime
* @return mixed
*/
private function setCachedValue($k, $v)
private function setCachedValue($k, $v, ?int $lifetime = null)
{
if (is_null($this->cache)) {
return null;
Expand All @@ -81,7 +82,7 @@ private function setCachedValue($k, $v)

$cacheItem = $this->cache->getItem($key);
$cacheItem->set($v);
$cacheItem->expiresAfter($this->cacheConfig['lifetime']);
$cacheItem->expiresAfter($lifetime ?? $this->cacheConfig['lifetime']);
return $this->cache->save($cacheItem);
}

Expand Down
92 changes: 85 additions & 7 deletions src/Credentials/ExternalAccountCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\OAuth2;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\TrustBoundaryTrait;
use Google\Auth\UpdateMetadataInterface;
use Google\Auth\UpdateMetadataTrait;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
use LogicException;

/**
* **IMPORTANT**:
Expand All @@ -51,7 +53,12 @@ class ExternalAccountCredentials implements
GetUniverseDomainInterface,
ProjectIdProviderInterface
{
use UpdateMetadataTrait;
use UpdateMetadataTrait {
updateMetadata as traitUpdateMetadata;
}
use TrustBoundaryTrait {
buildTrustBoundaryLookupUrl as traitBuildTrustBoundaryLookupUrl;
}

private const EXTERNAL_ACCOUNT_TYPE = 'external_account';
private const CLOUD_RESOURCE_MANAGER_URL = 'https://cloudresourcemanager.UNIVERSE_DOMAIN/v1/projects/%s';
Expand All @@ -69,10 +76,12 @@ class ExternalAccountCredentials implements
* @param string|string[] $scope The scope of the access request, expressed either as an array
* or as a space-delimited string.
* @param array<mixed> $jsonKey JSON credentials as an associative array.
* @param bool $enableTrustBoundary Lookup and include the trust boundary header.
*/
public function __construct(
$scope,
array $jsonKey
array $jsonKey,
bool $enableTrustBoundary = false
) {
if (!array_key_exists('type', $jsonKey)) {
throw new InvalidArgumentException('json key is missing the type field');
Expand Down Expand Up @@ -114,6 +123,7 @@ 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->enableTrustBoundary = $enableTrustBoundary;

$this->auth = new OAuth2([
'tokenCredentialUri' => $jsonKey['token_url'],
Expand Down Expand Up @@ -200,11 +210,8 @@ private static function buildCredentialSource(array $jsonKey): ExternalAccountCr
}

if ($serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'] ?? null) {
// Parse email from URL. The formal looks as follows:
// https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken
$regex = '/serviceAccounts\/(?<email>[^:]+):generateAccessToken$/';
if (preg_match($regex, $serviceAccountImpersonationUrl, $matches)) {
$env['GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL'] = $matches['email'];
if ($email = self::getServiceAccountImpersonationEmail($serviceAccountImpersonationUrl)) {
$env['GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL'] = $email;
}
}

Expand All @@ -220,6 +227,18 @@ private static function buildCredentialSource(array $jsonKey): ExternalAccountCr
throw new InvalidArgumentException('Unable to determine credential source from json key.');
}

private static function getServiceAccountImpersonationEmail(string $serviceAccountImpersonationUrl): string|null
{
// Parse email from URL. The formal looks as follows:
// https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken
$regex = '/serviceAccounts\/(?<email>[^:]+):generateAccessToken$/';
if (preg_match($regex, $serviceAccountImpersonationUrl, $matches)) {
return $matches['email'];
}

return null;
}

/**
* @param string $stsToken
* @param callable|null $httpHandler
Expand Down Expand Up @@ -290,6 +309,37 @@ public function fetchAuthToken(?callable $httpHandler = null, array $headers = [
return $stsToken;
}

/**
* Updates metadata with the authorization token.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable|null $httpHandler callback which delivers psr7 request
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$authUri = null,
?callable $httpHandler = null
) {
$metadata = $this->traitUpdateMetadata($metadata, $authUri, $httpHandler);

if ($this->enableTrustBoundary) {
$clientName = $this->serviceAccountImpersonationUrl
? self::getServiceAccountImpersonationEmail($this->serviceAccountImpersonationUrl)
: ''; // @TODO: What do we do when this is empty?

$metadata = $this->updateTrustBoundaryMetadata(
$metadata,
$this->buildTrustBoundaryLookupUrl(),
$this->getUniverseDomain(),
$httpHandler,
);
}

return $metadata;
}

/**
* Get the cache token key for the credentials.
* The cache token key format depends on the type of source
Expand Down Expand Up @@ -391,4 +441,32 @@ private function isWorkforcePool(): bool
$regex = '#//iam\.googleapis\.com/locations/[^/]+/workforcePools/#';
return preg_match($regex, $this->auth->getAudience()) === 1;
}

/**
* Builds and returns the URL for the trust boundary lookup API.
*/
private function buildTrustBoundaryLookupUrl(): string
{
// Try to parse as a workload identity pool.
// Audience format: //iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID
$regex = '/projects\/([^\/]+)\/locations\/global\/workloadIdentityPools\/([^\/]+)/';
if (preg_match($regex, $this->auth->getAudience(), $matches)) {
[$_, $projectNumber, $poolId] = $matches;

return $this->traitBuildTrustBoundaryLookupUrl(
poolId: $poolId,
projectNumber: $projectNumber,
);
}

// If that fails, try to parse as a workforce pool.
// Audience format: //iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID
if (preg_match('/locations\/[^\/]+\/workforcePools\/([^\/]+)/', $this->auth->getAudience(), $matches)) {
return $this->traitBuildTrustBoundaryLookupUrl(
poolId: $matches[1],
);
}

throw new LogicException('Invalid audience format');
}
}
36 changes: 35 additions & 1 deletion src/Credentials/GCECredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Google\Auth\IamSignerTrait;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\SignBlobInterface;
use Google\Auth\TrustBoundaryTrait;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
Expand Down Expand Up @@ -64,6 +65,7 @@ class GCECredentials extends CredentialsLoader implements
GetQuotaProjectInterface
{
use IamSignerTrait;
use TrustBoundaryTrait;

// phpcs:disable
const cacheKey = 'GOOGLE_AUTH_PHP_GCE';
Expand Down Expand Up @@ -209,14 +211,16 @@ class GCECredentials extends CredentialsLoader implements
* account identity name to use instead of "default".
* @param string|null $universeDomain [optional] Specify a universe domain to use
* instead of fetching one from the metadata server.
* @param bool $enableTrustBoundary Lookup and include the trust boundary header.
*/
public function __construct(
?Iam $iam = null,
$scope = null,
$targetAudience = null,
$quotaProject = null,
$serviceAccountIdentity = null,
?string $universeDomain = null
?string $universeDomain = null,
bool $enableTrustBoundary = false
) {
$this->iam = $iam;

Expand Down Expand Up @@ -245,6 +249,7 @@ public function __construct(
$this->quotaProject = $quotaProject;
$this->serviceAccountIdentity = $serviceAccountIdentity;
$this->universeDomain = $universeDomain;
$this->enableTrustBoundary = $enableTrustBoundary;
}

/**
Expand Down Expand Up @@ -629,6 +634,35 @@ public function getUniverseDomain(?callable $httpHandler = null): string
return $this->universeDomain;
}

/**
* Updates metadata with the authorization token.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable|null $httpHandler callback which delivers psr7 request
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$authUri = null,
?callable $httpHandler = null
) {
$metadata = parent::updateMetadata($metadata, $authUri, $httpHandler);

if ($this->enableTrustBoundary) {
$metadata = $this->updateTrustBoundaryMetadata(
$metadata,
$this->buildTrustBoundaryLookupUrl(
serviceAccountEmail: $this->getClientName($httpHandler)
),
$this->getUniverseDomain($httpHandler),
$httpHandler,
);
}

return $metadata;
}

/**
* Fetch the value of a GCE metadata server URI.
*
Expand Down
Loading
Loading