Initial commit

This commit is contained in:
Local Administrator
2025-04-18 10:32:42 +02:00
commit b83134aca3
29643 changed files with 3045897 additions and 0 deletions

View File

@@ -0,0 +1,473 @@
<?php
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use DateTime;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\SignatureInvalidException;
use Google\Auth\Cache\MemoryCacheItemPool;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Utils;
use InvalidArgumentException;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\RSA;
use phpseclib3\Math\BigInteger;
use Psr\Cache\CacheItemPoolInterface;
use RuntimeException;
use SimpleJWT\InvalidTokenException;
use SimpleJWT\JWT as SimpleJWT;
use SimpleJWT\Keys\KeyFactory;
use SimpleJWT\Keys\KeySet;
use TypeError;
use UnexpectedValueException;
/**
* Wrapper around Google Access Tokens which provides convenience functions.
*
* @experimental
*/
class AccessToken
{
const FEDERATED_SIGNON_CERT_URL = 'https://www.googleapis.com/oauth2/v3/certs';
const IAP_CERT_URL = 'https://www.gstatic.com/iap/verify/public_key-jwk';
const IAP_ISSUER = 'https://cloud.google.com/iap';
const OAUTH2_ISSUER = 'accounts.google.com';
const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com';
const OAUTH2_REVOKE_URI = 'https://oauth2.googleapis.com/revoke';
/**
* @var callable
*/
private $httpHandler;
/**
* @var CacheItemPoolInterface
*/
private $cache;
/**
* @param callable $httpHandler [optional] An HTTP Handler to deliver PSR-7 requests.
* @param CacheItemPoolInterface $cache [optional] A PSR-6 compatible cache implementation.
*/
public function __construct(
callable $httpHandler = null,
CacheItemPoolInterface $cache = null
) {
$this->httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
$this->cache = $cache ?: new MemoryCacheItemPool();
}
/**
* Verifies an id token and returns the authenticated apiLoginTicket.
* Throws an exception if the id token is not valid.
* The audience parameter can be used to control which id tokens are
* accepted. By default, the id token must have been issued to this OAuth2 client.
*
* @param string $token The JSON Web Token to be verified.
* @param array<mixed> $options [optional] {
* Configuration options.
* @type string $audience The indended recipient of the token.
* @type string $issuer The intended issuer of the token.
* @type string $cacheKey The cache key of the cached certs. Defaults to
* the sha1 of $certsLocation if provided, otherwise is set to
* "federated_signon_certs_v3".
* @type string $certsLocation The location (remote or local) from which
* to retrieve certificates, if not cached. This value should only be
* provided in limited circumstances in which you are sure of the
* behavior.
* @type bool $throwException Whether the function should throw an
* exception if the verification fails. This is useful for
* determining the reason verification failed.
* }
* @return array<mixed>|false the token payload, if successful, or false if not.
* @throws InvalidArgumentException If certs could not be retrieved from a local file.
* @throws InvalidArgumentException If received certs are in an invalid format.
* @throws InvalidArgumentException If the cert alg is not supported.
* @throws RuntimeException If certs could not be retrieved from a remote location.
* @throws UnexpectedValueException If the token issuer does not match.
* @throws UnexpectedValueException If the token audience does not match.
*/
public function verify($token, array $options = [])
{
$audience = $options['audience'] ?? null;
$issuer = $options['issuer'] ?? null;
$certsLocation = $options['certsLocation'] ?? self::FEDERATED_SIGNON_CERT_URL;
$cacheKey = $options['cacheKey'] ?? $this->getCacheKeyFromCertLocation($certsLocation);
$throwException = $options['throwException'] ?? false; // for backwards compatibility
// Check signature against each available cert.
$certs = $this->getCerts($certsLocation, $cacheKey, $options);
$alg = $this->determineAlg($certs);
if (!in_array($alg, ['RS256', 'ES256'])) {
throw new InvalidArgumentException(
'unrecognized "alg" in certs, expected ES256 or RS256'
);
}
try {
if ($alg == 'RS256') {
return $this->verifyRs256($token, $certs, $audience, $issuer);
}
return $this->verifyEs256($token, $certs, $audience, $issuer);
} catch (ExpiredException $e) { // firebase/php-jwt 5+
} catch (SignatureInvalidException $e) { // firebase/php-jwt 5+
} catch (InvalidTokenException $e) { // simplejwt
} catch (InvalidArgumentException $e) {
} catch (UnexpectedValueException $e) {
}
if ($throwException) {
throw $e;
}
return false;
}
/**
* Identifies the expected algorithm to verify by looking at the "alg" key
* of the provided certs.
*
* @param array<mixed> $certs Certificate array according to the JWK spec (see
* https://tools.ietf.org/html/rfc7517).
* @return string The expected algorithm, such as "ES256" or "RS256".
*/
private function determineAlg(array $certs)
{
$alg = null;
foreach ($certs as $cert) {
if (empty($cert['alg'])) {
throw new InvalidArgumentException(
'certs expects "alg" to be set'
);
}
$alg = $alg ?: $cert['alg'];
if ($alg != $cert['alg']) {
throw new InvalidArgumentException(
'More than one alg detected in certs'
);
}
}
return $alg;
}
/**
* Verifies an ES256-signed JWT.
*
* @param string $token The JSON Web Token to be verified.
* @param array<mixed> $certs Certificate array according to the JWK spec (see
* https://tools.ietf.org/html/rfc7517).
* @param string|null $audience If set, returns false if the provided
* audience does not match the "aud" claim on the JWT.
* @param string|null $issuer If set, returns false if the provided
* issuer does not match the "iss" claim on the JWT.
* @return array<mixed> the token payload, if successful, or false if not.
*/
private function verifyEs256($token, array $certs, $audience = null, $issuer = null)
{
$this->checkSimpleJwt();
$jwkset = new KeySet();
foreach ($certs as $cert) {
$jwkset->add(KeyFactory::create($cert, 'php'));
}
// Validate the signature using the key set and ES256 algorithm.
$jwt = $this->callSimpleJwtDecode([$token, $jwkset, 'ES256']);
$payload = $jwt->getClaims();
if ($audience) {
if (!isset($payload['aud']) || $payload['aud'] != $audience) {
throw new UnexpectedValueException('Audience does not match');
}
}
// @see https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload
$issuer = $issuer ?: self::IAP_ISSUER;
if (!isset($payload['iss']) || $payload['iss'] !== $issuer) {
throw new UnexpectedValueException('Issuer does not match');
}
return $payload;
}
/**
* Verifies an RS256-signed JWT.
*
* @param string $token The JSON Web Token to be verified.
* @param array<mixed> $certs Certificate array according to the JWK spec (see
* https://tools.ietf.org/html/rfc7517).
* @param string|null $audience If set, returns false if the provided
* audience does not match the "aud" claim on the JWT.
* @param string|null $issuer If set, returns false if the provided
* issuer does not match the "iss" claim on the JWT.
* @return array<mixed> the token payload, if successful, or false if not.
*/
private function verifyRs256($token, array $certs, $audience = null, $issuer = null)
{
$this->checkAndInitializePhpsec();
$keys = [];
foreach ($certs as $cert) {
if (empty($cert['kid'])) {
throw new InvalidArgumentException(
'certs expects "kid" to be set'
);
}
if (empty($cert['n']) || empty($cert['e'])) {
throw new InvalidArgumentException(
'RSA certs expects "n" and "e" to be set'
);
}
$publicKey = $this->loadPhpsecPublicKey($cert['n'], $cert['e']);
// create an array of key IDs to certs for the JWT library
$keys[$cert['kid']] = new Key($publicKey, 'RS256');
}
$payload = $this->callJwtStatic('decode', [
$token,
$keys,
]);
if ($audience) {
if (!property_exists($payload, 'aud') || $payload->aud != $audience) {
throw new UnexpectedValueException('Audience does not match');
}
}
// support HTTP and HTTPS issuers
// @see https://developers.google.com/identity/sign-in/web/backend-auth
$issuers = $issuer ? [$issuer] : [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS];
if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) {
throw new UnexpectedValueException('Issuer does not match');
}
return (array) $payload;
}
/**
* Revoke an OAuth2 access token or refresh token. This method will revoke the current access
* token, if a token isn't provided.
*
* @param string|array<mixed> $token The token (access token or a refresh token) that should be revoked.
* @param array<mixed> $options [optional] Configuration options.
* @return bool Returns True if the revocation was successful, otherwise False.
*/
public function revoke($token, array $options = [])
{
if (is_array($token)) {
if (isset($token['refresh_token'])) {
$token = $token['refresh_token'];
} else {
$token = $token['access_token'];
}
}
$body = Utils::streamFor(http_build_query(['token' => $token]));
$request = new Request('POST', self::OAUTH2_REVOKE_URI, [
'Cache-Control' => 'no-store',
'Content-Type' => 'application/x-www-form-urlencoded',
], $body);
$httpHandler = $this->httpHandler;
$response = $httpHandler($request, $options);
return $response->getStatusCode() == 200;
}
/**
* Gets federated sign-on certificates to use for verifying identity tokens.
* Returns certs as array structure, where keys are key ids, and values
* are PEM encoded certificates.
*
* @param string $location The location from which to retrieve certs.
* @param string $cacheKey The key under which to cache the retrieved certs.
* @param array<mixed> $options [optional] Configuration options.
* @return array<mixed>
* @throws InvalidArgumentException If received certs are in an invalid format.
*/
private function getCerts($location, $cacheKey, array $options = [])
{
$cacheItem = $this->cache->getItem($cacheKey);
$certs = $cacheItem ? $cacheItem->get() : null;
$expireTime = null;
if (!$certs) {
list($certs, $expireTime) = $this->retrieveCertsFromLocation($location, $options);
}
if (!isset($certs['keys'])) {
if ($location !== self::IAP_CERT_URL) {
throw new InvalidArgumentException(
'federated sign-on certs expects "keys" to be set'
);
}
throw new InvalidArgumentException(
'certs expects "keys" to be set'
);
}
// Push caching off until after verifying certs are in a valid format.
// Don't want to cache bad data.
if ($expireTime) {
$cacheItem->expiresAt(new DateTime($expireTime));
$cacheItem->set($certs);
$this->cache->save($cacheItem);
}
return $certs['keys'];
}
/**
* Retrieve and cache a certificates file.
*
* @param string $url location
* @param array<mixed> $options [optional] Configuration options.
* @return array{array<mixed>, string}
* @throws InvalidArgumentException If certs could not be retrieved from a local file.
* @throws RuntimeException If certs could not be retrieved from a remote location.
*/
private function retrieveCertsFromLocation($url, array $options = [])
{
// If we're retrieving a local file, just grab it.
$expireTime = '+1 hour';
if (strpos($url, 'http') !== 0) {
if (!file_exists($url)) {
throw new InvalidArgumentException(sprintf(
'Failed to retrieve verification certificates from path: %s.',
$url
));
}
return [
json_decode((string) file_get_contents($url), true),
$expireTime
];
}
$httpHandler = $this->httpHandler;
$response = $httpHandler(new Request('GET', $url), $options);
if ($response->getStatusCode() == 200) {
if ($cacheControl = $response->getHeaderLine('Cache-Control')) {
array_map(function ($value) use (&$expireTime) {
list($key, $value) = explode('=', $value) + [null, null];
if (trim($key) == 'max-age') {
$expireTime = '+' . $value . ' seconds';
}
}, explode(',', $cacheControl));
}
return [
json_decode((string) $response->getBody(), true),
$expireTime
];
}
throw new RuntimeException(sprintf(
'Failed to retrieve verification certificates: "%s".',
$response->getBody()->getContents()
), $response->getStatusCode());
}
/**
* @return void
*/
private function checkAndInitializePhpsec()
{
if (!class_exists(RSA::class)) {
throw new RuntimeException('Please require phpseclib/phpseclib v3 to use this utility.');
}
}
/**
* @return string
* @throws TypeError If the key cannot be initialized to a string.
*/
private function loadPhpsecPublicKey(string $modulus, string $exponent): string
{
$key = PublicKeyLoader::load([
'n' => new BigInteger($this->callJwtStatic('urlsafeB64Decode', [
$modulus,
]), 256),
'e' => new BigInteger($this->callJwtStatic('urlsafeB64Decode', [
$exponent
]), 256),
]);
$formattedPublicKey = $key->toString('PKCS8');
if (!is_string($formattedPublicKey)) {
throw new TypeError('Failed to initialize the key');
}
return $formattedPublicKey;
}
/**
* @return void
*/
private function checkSimpleJwt()
{
// @codeCoverageIgnoreStart
if (!class_exists(SimpleJwt::class)) {
throw new RuntimeException('Please require kelvinmo/simplejwt ^0.2 to use this utility.');
}
// @codeCoverageIgnoreEnd
}
/**
* Provide a hook to mock calls to the JWT static methods.
*
* @param string $method
* @param array<mixed> $args
* @return mixed
*/
protected function callJwtStatic($method, array $args = [])
{
return call_user_func_array([JWT::class, $method], $args); // @phpstan-ignore-line
}
/**
* Provide a hook to mock calls to the JWT static methods.
*
* @param array<mixed> $args
* @return mixed
*/
protected function callSimpleJwtDecode(array $args = [])
{
return call_user_func_array([SimpleJwt::class, 'decode'], $args);
}
/**
* Generate a cache key based on the cert location using sha1 with the
* exception of using "federated_signon_certs_v3" to preserve BC.
*
* @param string $certsLocation
* @return string
*/
private function getCacheKeyFromCertLocation($certsLocation)
{
$key = $certsLocation === self::FEDERATED_SIGNON_CERT_URL
? 'federated_signon_certs_v3'
: sha1($certsLocation);
return 'google_auth_certs_cache|' . $key;
}
}

View File

@@ -0,0 +1,356 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use DomainException;
use Google\Auth\Credentials\AppIdentityCredentials;
use Google\Auth\Credentials\GCECredentials;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\Middleware\AuthTokenMiddleware;
use Google\Auth\Middleware\ProxyAuthTokenMiddleware;
use Google\Auth\Subscriber\AuthTokenSubscriber;
use GuzzleHttp\Client;
use InvalidArgumentException;
use Psr\Cache\CacheItemPoolInterface;
/**
* ApplicationDefaultCredentials obtains the default credentials for
* authorizing a request to a Google service.
*
* Application Default Credentials are described here:
* https://developers.google.com/accounts/docs/application-default-credentials
*
* This class implements the search for the application default credentials as
* described in the link.
*
* It provides three factory methods:
* - #get returns the computed credentials object
* - #getSubscriber returns an AuthTokenSubscriber built from the credentials object
* - #getMiddleware returns an AuthTokenMiddleware built from the credentials object
*
* This allows it to be used as follows with GuzzleHttp\Client:
*
* ```
* use Google\Auth\ApplicationDefaultCredentials;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $middleware = ApplicationDefaultCredentials::getMiddleware(
* 'https://www.googleapis.com/auth/taskqueue'
* );
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
* 'auth' => 'google_auth' // authorize all requests
* ]);
*
* $res = $client->get('myproject/taskqueues/myqueue');
* ```
*/
class ApplicationDefaultCredentials
{
/**
* @deprecated
*
* Obtains an AuthTokenSubscriber that uses the default FetchAuthTokenInterface
* implementation to use in this environment.
*
* If supplied, $scope is used to in creating the credentials instance if
* this does not fallback to the compute engine defaults.
*
* @param string|string[] $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
* @param callable $httpHandler callback which delivers psr7 request
* @param array<mixed> $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface $cache A cache implementation, may be
* provided if you have one already available for use.
* @return AuthTokenSubscriber
* @throws DomainException if no implementation can be obtained.
*/
public static function getSubscriber(// @phpstan-ignore-line
$scope = null,
callable $httpHandler = null,
array $cacheConfig = null,
CacheItemPoolInterface $cache = null
) {
$creds = self::getCredentials($scope, $httpHandler, $cacheConfig, $cache);
/** @phpstan-ignore-next-line */
return new AuthTokenSubscriber($creds, $httpHandler);
}
/**
* Obtains an AuthTokenMiddleware that uses the default FetchAuthTokenInterface
* implementation to use in this environment.
*
* If supplied, $scope is used to in creating the credentials instance if
* this does not fallback to the compute engine defaults.
*
* @param string|string[] $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
* @param callable $httpHandler callback which delivers psr7 request
* @param array<mixed> $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface $cache A cache implementation, may be
* provided if you have one already available for use.
* @param string $quotaProject specifies a project to bill for access
* charges associated with the request.
* @return AuthTokenMiddleware
* @throws DomainException if no implementation can be obtained.
*/
public static function getMiddleware(
$scope = null,
callable $httpHandler = null,
array $cacheConfig = null,
CacheItemPoolInterface $cache = null,
$quotaProject = null
) {
$creds = self::getCredentials($scope, $httpHandler, $cacheConfig, $cache, $quotaProject);
return new AuthTokenMiddleware($creds, $httpHandler);
}
/**
* Obtains the default FetchAuthTokenInterface implementation to use
* in this environment.
*
* @param string|string[] $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
* @param callable $httpHandler callback which delivers psr7 request
* @param array<mixed> $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface $cache A cache implementation, may be
* provided if you have one already available for use.
* @param string $quotaProject specifies a project to bill for access
* charges associated with the request.
* @param string|string[] $defaultScope The default scope to use if no
* user-defined scopes exist, expressed either as an Array or as a
* space-delimited string.
* @param string $universeDomain Specifies a universe domain to use for the
* calling client library
*
* @return FetchAuthTokenInterface
* @throws DomainException if no implementation can be obtained.
*/
public static function getCredentials(
$scope = null,
callable $httpHandler = null,
array $cacheConfig = null,
CacheItemPoolInterface $cache = null,
$quotaProject = null,
$defaultScope = null,
string $universeDomain = null
) {
$creds = null;
$jsonKey = CredentialsLoader::fromEnv()
?: CredentialsLoader::fromWellKnownFile();
$anyScope = $scope ?: $defaultScope;
if (!$httpHandler) {
if (!($client = HttpClientCache::getHttpClient())) {
$client = new Client();
HttpClientCache::setHttpClient($client);
}
$httpHandler = HttpHandlerFactory::build($client);
}
if (is_null($quotaProject)) {
// if a quota project isn't specified, try to get one from the env var
$quotaProject = CredentialsLoader::quotaProjectFromEnv();
}
if (!is_null($jsonKey)) {
if ($quotaProject) {
$jsonKey['quota_project_id'] = $quotaProject;
}
if ($universeDomain) {
$jsonKey['universe_domain'] = $universeDomain;
}
$creds = CredentialsLoader::makeCredentials(
$scope,
$jsonKey,
$defaultScope
);
} 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->setIsOnGce(true); // save the credentials a trip to the metadata server
}
if (is_null($creds)) {
throw new DomainException(self::notFound());
}
if (!is_null($cache)) {
$creds = new FetchAuthTokenCache($creds, $cacheConfig, $cache);
}
return $creds;
}
/**
* Obtains an AuthTokenMiddleware which will fetch an ID token to use in the
* Authorization header. The middleware is configured with the default
* FetchAuthTokenInterface implementation to use in this environment.
*
* If supplied, $targetAudience is used to set the "aud" on the resulting
* ID token.
*
* @param string $targetAudience The audience for the ID token.
* @param callable $httpHandler callback which delivers psr7 request
* @param array<mixed> $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface $cache A cache implementation, may be
* provided if you have one already available for use.
* @return AuthTokenMiddleware
* @throws DomainException if no implementation can be obtained.
*/
public static function getIdTokenMiddleware(
$targetAudience,
callable $httpHandler = null,
array $cacheConfig = null,
CacheItemPoolInterface $cache = null
) {
$creds = self::getIdTokenCredentials($targetAudience, $httpHandler, $cacheConfig, $cache);
return new AuthTokenMiddleware($creds, $httpHandler);
}
/**
* Obtains an ProxyAuthTokenMiddleware which will fetch an ID token to use in the
* Authorization header. The middleware is configured with the default
* FetchAuthTokenInterface implementation to use in this environment.
*
* If supplied, $targetAudience is used to set the "aud" on the resulting
* ID token.
*
* @param string $targetAudience The audience for the ID token.
* @param callable $httpHandler callback which delivers psr7 request
* @param array<mixed> $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface $cache A cache implementation, may be
* provided if you have one already available for use.
* @return ProxyAuthTokenMiddleware
* @throws DomainException if no implementation can be obtained.
*/
public static function getProxyIdTokenMiddleware(
$targetAudience,
callable $httpHandler = null,
array $cacheConfig = null,
CacheItemPoolInterface $cache = null
) {
$creds = self::getIdTokenCredentials($targetAudience, $httpHandler, $cacheConfig, $cache);
return new ProxyAuthTokenMiddleware($creds, $httpHandler);
}
/**
* Obtains the default FetchAuthTokenInterface implementation to use
* in this environment, configured with a $targetAudience for fetching an ID
* token.
*
* @param string $targetAudience The audience for the ID token.
* @param callable $httpHandler callback which delivers psr7 request
* @param array<mixed> $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface $cache A cache implementation, may be
* provided if you have one already available for use.
* @return FetchAuthTokenInterface
* @throws DomainException if no implementation can be obtained.
* @throws InvalidArgumentException if JSON "type" key is invalid
*/
public static function getIdTokenCredentials(
$targetAudience,
callable $httpHandler = null,
array $cacheConfig = null,
CacheItemPoolInterface $cache = null
) {
$creds = null;
$jsonKey = CredentialsLoader::fromEnv()
?: CredentialsLoader::fromWellKnownFile();
if (!$httpHandler) {
if (!($client = HttpClientCache::getHttpClient())) {
$client = new Client();
HttpClientCache::setHttpClient($client);
}
$httpHandler = HttpHandlerFactory::build($client);
}
if (!is_null($jsonKey)) {
if (!array_key_exists('type', $jsonKey)) {
throw new \InvalidArgumentException('json key is missing the type field');
}
if ($jsonKey['type'] == 'authorized_user') {
throw new InvalidArgumentException('ID tokens are not supported for end user credentials');
}
if ($jsonKey['type'] != 'service_account') {
throw new InvalidArgumentException('invalid value in the type field');
}
$creds = new ServiceAccountCredentials(null, $jsonKey, null, $targetAudience);
} elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
$creds = new GCECredentials(null, null, $targetAudience);
$creds->setIsOnGce(true); // save the credentials a trip to the metadata server
}
if (is_null($creds)) {
throw new DomainException(self::notFound());
}
if (!is_null($cache)) {
$creds = new FetchAuthTokenCache($creds, $cacheConfig, $cache);
}
return $creds;
}
/**
* @return string
*/
private static function notFound()
{
$msg = 'Your default credentials were not found. To set up ';
$msg .= 'Application Default Credentials, see ';
$msg .= 'https://cloud.google.com/docs/authentication/external/set-up-adc';
return $msg;
}
/**
* @param callable $httpHandler
* @param array<mixed> $cacheConfig
* @param CacheItemPoolInterface $cache
* @return bool
*/
private static function onGce(
callable $httpHandler = null,
array $cacheConfig = null,
CacheItemPoolInterface $cache = null
) {
$gceCacheConfig = [];
foreach (['lifetime', 'prefix'] as $key) {
if (isset($cacheConfig['gce_' . $key])) {
$gceCacheConfig[$key] = $cacheConfig['gce_' . $key];
}
}
return (new GCECache($gceCacheConfig, $cache))->onGce($httpHandler);
}
}

View File

@@ -0,0 +1,230 @@
<?php
/**
* Copyright 2024 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Cache;
use ErrorException;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
class FileSystemCacheItemPool implements CacheItemPoolInterface
{
/**
* @var string
*/
private string $cachePath;
/**
* @var array<CacheItemInterface>
*/
private array $buffer = [];
/**
* Creates a FileSystemCacheItemPool cache that stores values in local storage
*
* @param string $path The string representation of the path where the cache will store the serialized objects.
*/
public function __construct(string $path)
{
$this->cachePath = $path;
if (is_dir($this->cachePath)) {
return;
}
if (!mkdir($this->cachePath)) {
throw new ErrorException("Cache folder couldn't be created.");
}
}
/**
* {@inheritdoc}
*/
public function getItem(string $key): CacheItemInterface
{
if (!$this->validKey($key)) {
throw new InvalidArgumentException("The key '$key' is not valid. The key should follow the pattern |^[a-zA-Z0-9_\.! ]+$|");
}
$item = new TypedItem($key);
$itemPath = $this->cacheFilePath($key);
if (!file_exists($itemPath)) {
return $item;
}
$serializedItem = file_get_contents($itemPath);
if ($serializedItem === false) {
return $item;
}
$item->set(unserialize($serializedItem));
return $item;
}
/**
* {@inheritdoc}
*
* @return iterable<CacheItemInterface> An iterable object containing all the
* A traversable collection of Cache Items keyed by the cache keys of
* each item. A Cache item will be returned for each key, even if that
* key is not found. However, if no keys are specified then an empty
* traversable MUST be returned instead.
*/
public function getItems(array $keys = []): iterable
{
$result = [];
foreach ($keys as $key) {
$result[$key] = $this->getItem($key);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function save(CacheItemInterface $item): bool
{
if (!$this->validKey($item->getKey())) {
return false;
}
$itemPath = $this->cacheFilePath($item->getKey());
$serializedItem = serialize($item->get());
$result = file_put_contents($itemPath, $serializedItem);
// 0 bytes write is considered a successful operation
if ($result === false) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function hasItem(string $key): bool
{
return $this->getItem($key)->isHit();
}
/**
* {@inheritdoc}
*/
public function clear(): bool
{
$this->buffer = [];
if (!is_dir($this->cachePath)) {
return false;
}
$files = scandir($this->cachePath);
if (!$files) {
return false;
}
foreach ($files as $fileName) {
if ($fileName === '.' || $fileName === '..') {
continue;
}
if (!unlink($this->cachePath . '/' . $fileName)) {
return false;
}
}
return true;
}
/**
* {@inheritdoc}
*/
public function deleteItem(string $key): bool
{
if (!$this->validKey($key)) {
throw new InvalidArgumentException("The key '$key' is not valid. The key should follow the pattern |^[a-zA-Z0-9_\.! ]+$|");
}
$itemPath = $this->cacheFilePath($key);
if (!file_exists($itemPath)) {
return true;
}
return unlink($itemPath);
}
/**
* {@inheritdoc}
*/
public function deleteItems(array $keys): bool
{
$result = true;
foreach ($keys as $key) {
if (!$this->deleteItem($key)) {
$result = false;
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function saveDeferred(CacheItemInterface $item): bool
{
array_push($this->buffer, $item);
return true;
}
/**
* {@inheritdoc}
*/
public function commit(): bool
{
$result = true;
foreach ($this->buffer as $item) {
if (!$this->save($item)) {
$result = false;
}
}
return $result;
}
private function cacheFilePath(string $key): string
{
return $this->cachePath . '/' . $key;
}
private function validKey(string $key): bool
{
return (bool) preg_match('|^[a-zA-Z0-9_\.]+$|', $key);
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Cache;
use Psr\Cache\InvalidArgumentException as PsrInvalidArgumentException;
class InvalidArgumentException extends \InvalidArgumentException implements PsrInvalidArgumentException
{
}

View File

@@ -0,0 +1,175 @@
<?php
/*
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Cache;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Psr\Cache\CacheItemInterface;
use TypeError;
/**
* A cache item.
*
* This class will be used by MemoryCacheItemPool and SysVCacheItemPool
* on PHP 7.4 and below. It is compatible with psr/cache 1.0 and 2.0 (PSR-6).
* @deprecated
* @see TypedItem for compatiblity with psr/cache 3.0.
*/
final class Item implements CacheItemInterface
{
/**
* @var string
*/
private $key;
/**
* @var mixed
*/
private $value;
/**
* @var DateTimeInterface|null
*/
private $expiration;
/**
* @var bool
*/
private $isHit = false;
/**
* @param string $key
*/
public function __construct($key)
{
$this->key = $key;
}
/**
* {@inheritdoc}
*/
public function getKey()
{
return $this->key;
}
/**
* {@inheritdoc}
*/
public function get()
{
return $this->isHit() ? $this->value : null;
}
/**
* {@inheritdoc}
*/
public function isHit()
{
if (!$this->isHit) {
return false;
}
if ($this->expiration === null) {
return true;
}
return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp();
}
/**
* {@inheritdoc}
*/
public function set($value)
{
$this->isHit = true;
$this->value = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function expiresAt($expiration)
{
if ($this->isValidExpiration($expiration)) {
$this->expiration = $expiration;
return $this;
}
$error = sprintf(
'Argument 1 passed to %s::expiresAt() must implement interface DateTimeInterface, %s given',
get_class($this),
gettype($expiration)
);
throw new TypeError($error);
}
/**
* {@inheritdoc}
*/
public function expiresAfter($time)
{
if (is_int($time)) {
$this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S"));
} elseif ($time instanceof \DateInterval) {
$this->expiration = $this->currentTime()->add($time);
} elseif ($time === null) {
$this->expiration = $time;
} else {
$message = 'Argument 1 passed to %s::expiresAfter() must be an ' .
'instance of DateInterval or of the type integer, %s given';
$error = sprintf($message, get_class($this), gettype($time));
throw new TypeError($error);
}
return $this;
}
/**
* Determines if an expiration is valid based on the rules defined by PSR6.
*
* @param mixed $expiration
* @return bool
*/
private function isValidExpiration($expiration)
{
if ($expiration === null) {
return true;
}
if ($expiration instanceof DateTimeInterface) {
return true;
}
return false;
}
/**
* @return DateTime
*/
protected function currentTime()
{
return new DateTime('now', new DateTimeZone('UTC'));
}
}

View File

@@ -0,0 +1,182 @@
<?php
/*
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Cache;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
/**
* Simple in-memory cache implementation.
*/
final class MemoryCacheItemPool implements CacheItemPoolInterface
{
/**
* @var CacheItemInterface[]
*/
private $items;
/**
* @var CacheItemInterface[]
*/
private $deferredItems;
/**
* {@inheritdoc}
*
* @return CacheItemInterface The corresponding Cache Item.
*/
public function getItem($key): CacheItemInterface
{
return current($this->getItems([$key])); // @phpstan-ignore-line
}
/**
* {@inheritdoc}
*
* @return iterable<CacheItemInterface>
* A traversable collection of Cache Items keyed by the cache keys of
* each item. A Cache item will be returned for each key, even if that
* key is not found. However, if no keys are specified then an empty
* traversable MUST be returned instead.
*/
public function getItems(array $keys = []): iterable
{
$items = [];
foreach ($keys as $key) {
$items[$key] = $this->hasItem($key) ? clone $this->items[$key] : new TypedItem($key);
}
return $items;
}
/**
* {@inheritdoc}
*
* @return bool
* True if item exists in the cache, false otherwise.
*/
public function hasItem($key): bool
{
$this->isValidKey($key);
return isset($this->items[$key]) && $this->items[$key]->isHit();
}
/**
* {@inheritdoc}
*
* @return bool
* True if the pool was successfully cleared. False if there was an error.
*/
public function clear(): bool
{
$this->items = [];
$this->deferredItems = [];
return true;
}
/**
* {@inheritdoc}
*
* @return bool
* True if the item was successfully removed. False if there was an error.
*/
public function deleteItem($key): bool
{
return $this->deleteItems([$key]);
}
/**
* {@inheritdoc}
*
* @return bool
* True if the items were successfully removed. False if there was an error.
*/
public function deleteItems(array $keys): bool
{
array_walk($keys, [$this, 'isValidKey']);
foreach ($keys as $key) {
unset($this->items[$key]);
}
return true;
}
/**
* {@inheritdoc}
*
* @return bool
* True if the item was successfully persisted. False if there was an error.
*/
public function save(CacheItemInterface $item): bool
{
$this->items[$item->getKey()] = $item;
return true;
}
/**
* {@inheritdoc}
*
* @return bool
* False if the item could not be queued or if a commit was attempted and failed. True otherwise.
*/
public function saveDeferred(CacheItemInterface $item): bool
{
$this->deferredItems[$item->getKey()] = $item;
return true;
}
/**
* {@inheritdoc}
*
* @return bool
* True if all not-yet-saved items were successfully saved or there were none. False otherwise.
*/
public function commit(): bool
{
foreach ($this->deferredItems as $item) {
$this->save($item);
}
$this->deferredItems = [];
return true;
}
/**
* Determines if the provided key is valid.
*
* @param string $key
* @return bool
* @throws InvalidArgumentException
*/
private function isValidKey($key)
{
$invalidCharacters = '{}()/\\\\@:';
if (!is_string($key) || preg_match("#[$invalidCharacters]#", $key)) {
throw new InvalidArgumentException('The provided key is not valid: ' . var_export($key, true));
}
return true;
}
}

View File

@@ -0,0 +1,248 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Cache;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
/**
* SystemV shared memory based CacheItemPool implementation.
*
* This CacheItemPool implementation can be used among multiple processes, but
* it doesn't provide any locking mechanism. If multiple processes write to
* this ItemPool, you have to avoid race condition manually in your code.
*/
class SysVCacheItemPool implements CacheItemPoolInterface
{
const VAR_KEY = 1;
const DEFAULT_PROJ = 'A';
const DEFAULT_MEMSIZE = 10000;
const DEFAULT_PERM = 0600;
/**
* @var int
*/
private $sysvKey;
/**
* @var CacheItemInterface[]
*/
private $items;
/**
* @var CacheItemInterface[]
*/
private $deferredItems;
/**
* @var array<mixed>
*/
private $options;
/**
* @var bool
*/
private $hasLoadedItems = false;
/**
* Create a SystemV shared memory based CacheItemPool.
*
* @param array<mixed> $options {
* [optional] Configuration options.
*
* @type int $variableKey The variable key for getting the data from the shared memory. **Defaults to** 1.
* @type string $proj The project identifier for ftok. This needs to be a one character string.
* **Defaults to** 'A'.
* @type int $memsize The memory size in bytes for shm_attach. **Defaults to** 10000.
* @type int $perm The permission for shm_attach. **Defaults to** 0600.
* }
*/
public function __construct($options = [])
{
if (! extension_loaded('sysvshm')) {
throw new \RuntimeException(
'sysvshm extension is required to use this ItemPool'
);
}
$this->options = $options + [
'variableKey' => self::VAR_KEY,
'proj' => self::DEFAULT_PROJ,
'memsize' => self::DEFAULT_MEMSIZE,
'perm' => self::DEFAULT_PERM
];
$this->items = [];
$this->deferredItems = [];
$this->sysvKey = ftok(__FILE__, $this->options['proj']);
}
/**
* @param mixed $key
* @return CacheItemInterface
*/
public function getItem($key): CacheItemInterface
{
$this->loadItems();
return current($this->getItems([$key])); // @phpstan-ignore-line
}
/**
* @param array<mixed> $keys
* @return iterable<CacheItemInterface>
*/
public function getItems(array $keys = []): iterable
{
$this->loadItems();
$items = [];
foreach ($keys as $key) {
$items[$key] = $this->hasItem($key) ?
clone $this->items[$key] :
new TypedItem($key);
}
return $items;
}
/**
* {@inheritdoc}
*/
public function hasItem($key): bool
{
$this->loadItems();
return isset($this->items[$key]) && $this->items[$key]->isHit();
}
/**
* {@inheritdoc}
*/
public function clear(): bool
{
$this->items = [];
$this->deferredItems = [];
return $this->saveCurrentItems();
}
/**
* {@inheritdoc}
*/
public function deleteItem($key): bool
{
return $this->deleteItems([$key]);
}
/**
* {@inheritdoc}
*/
public function deleteItems(array $keys): bool
{
if (!$this->hasLoadedItems) {
$this->loadItems();
}
foreach ($keys as $key) {
unset($this->items[$key]);
}
return $this->saveCurrentItems();
}
/**
* {@inheritdoc}
*/
public function save(CacheItemInterface $item): bool
{
if (!$this->hasLoadedItems) {
$this->loadItems();
}
$this->items[$item->getKey()] = $item;
return $this->saveCurrentItems();
}
/**
* {@inheritdoc}
*/
public function saveDeferred(CacheItemInterface $item): bool
{
$this->deferredItems[$item->getKey()] = $item;
return true;
}
/**
* {@inheritdoc}
*/
public function commit(): bool
{
foreach ($this->deferredItems as $item) {
if ($this->save($item) === false) {
return false;
}
}
$this->deferredItems = [];
return true;
}
/**
* Save the current items.
*
* @return bool true when success, false upon failure
*/
private function saveCurrentItems()
{
$shmid = shm_attach(
$this->sysvKey,
$this->options['memsize'],
$this->options['perm']
);
if ($shmid !== false) {
$ret = shm_put_var(
$shmid,
$this->options['variableKey'],
$this->items
);
shm_detach($shmid);
return $ret;
}
return false;
}
/**
* Load the items from the shared memory.
*
* @return bool true when success, false upon failure
*/
private function loadItems()
{
$shmid = shm_attach(
$this->sysvKey,
$this->options['memsize'],
$this->options['perm']
);
if ($shmid !== false) {
$data = @shm_get_var($shmid, $this->options['variableKey']);
if (!empty($data)) {
$this->items = $data;
} else {
$this->items = [];
}
shm_detach($shmid);
$this->hasLoadedItems = true;
return true;
}
return false;
}
}

View File

@@ -0,0 +1,170 @@
<?php
/*
* Copyright 2022 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Cache;
use Psr\Cache\CacheItemInterface;
/**
* A cache item.
*
* This class will be used by MemoryCacheItemPool and SysVCacheItemPool
* on PHP 8.0 and above. It is compatible with psr/cache 3.0 (PSR-6).
* @see Item for compatiblity with previous versions of PHP.
*/
final class TypedItem implements CacheItemInterface
{
/**
* @var mixed
*/
private mixed $value;
/**
* @var \DateTimeInterface|null
*/
private ?\DateTimeInterface $expiration;
/**
* @var bool
*/
private bool $isHit = false;
/**
* @param string $key
*/
public function __construct(
private string $key
) {
$this->key = $key;
$this->expiration = null;
}
/**
* {@inheritdoc}
*/
public function getKey(): string
{
return $this->key;
}
/**
* {@inheritdoc}
*/
public function get(): mixed
{
return $this->isHit() ? $this->value : null;
}
/**
* {@inheritdoc}
*/
public function isHit(): bool
{
if (!$this->isHit) {
return false;
}
if ($this->expiration === null) {
return true;
}
return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp();
}
/**
* {@inheritdoc}
*/
public function set(mixed $value): static
{
$this->isHit = true;
$this->value = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function expiresAt($expiration): static
{
if ($this->isValidExpiration($expiration)) {
$this->expiration = $expiration;
return $this;
}
$error = sprintf(
'Argument 1 passed to %s::expiresAt() must implement interface DateTimeInterface, %s given',
get_class($this),
gettype($expiration)
);
throw new \TypeError($error);
}
/**
* {@inheritdoc}
*/
public function expiresAfter($time): static
{
if (is_int($time)) {
$this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S"));
} elseif ($time instanceof \DateInterval) {
$this->expiration = $this->currentTime()->add($time);
} elseif ($time === null) {
$this->expiration = $time;
} else {
$message = 'Argument 1 passed to %s::expiresAfter() must be an ' .
'instance of DateInterval or of the type integer, %s given';
$error = sprintf($message, get_class($this), gettype($time));
throw new \TypeError($error);
}
return $this;
}
/**
* Determines if an expiration is valid based on the rules defined by PSR6.
*
* @param mixed $expiration
* @return bool
*/
private function isValidExpiration($expiration)
{
if ($expiration === null) {
return true;
}
// We test for two types here due to the fact the DateTimeInterface
// was not introduced until PHP 5.5. Checking for the DateTime type as
// well allows us to support 5.4.
if ($expiration instanceof \DateTimeInterface) {
return true;
}
return false;
}
/**
* @return \DateTime
*/
protected function currentTime()
{
return new \DateTime('now', new \DateTimeZone('UTC'));
}
}

View File

@@ -0,0 +1,110 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use Psr\Cache\CacheItemPoolInterface;
trait CacheTrait
{
/**
* @var int
*/
private $maxKeyLength = 64;
/**
* @var array<mixed>
*/
private $cacheConfig;
/**
* @var ?CacheItemPoolInterface
*/
private $cache;
/**
* Gets the cached value if it is present in the cache when that is
* available.
*
* @param mixed $k
*
* @return mixed
*/
private function getCachedValue($k)
{
if (is_null($this->cache)) {
return null;
}
$key = $this->getFullCacheKey($k);
if (is_null($key)) {
return null;
}
$cacheItem = $this->cache->getItem($key);
if ($cacheItem->isHit()) {
return $cacheItem->get();
}
}
/**
* Saves the value in the cache when that is available.
*
* @param mixed $k
* @param mixed $v
* @return mixed
*/
private function setCachedValue($k, $v)
{
if (is_null($this->cache)) {
return null;
}
$key = $this->getFullCacheKey($k);
if (is_null($key)) {
return null;
}
$cacheItem = $this->cache->getItem($key);
$cacheItem->set($v);
$cacheItem->expiresAfter($this->cacheConfig['lifetime']);
return $this->cache->save($cacheItem);
}
/**
* @param null|string $key
* @return null|string
*/
private function getFullCacheKey($key)
{
if (is_null($key)) {
return null;
}
$key = $this->cacheConfig['prefix'] . $key;
// ensure we do not have illegal characters
$key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $key);
// Hash keys if they exceed $maxKeyLength (defaults to 64)
if ($this->maxKeyLength && strlen($key) > $this->maxKeyLength) {
$key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
}
return $key;
}
}

View File

@@ -0,0 +1,375 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\CredentialSource;
use Google\Auth\ExternalAccountCredentialSourceInterface;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\Psr7\Request;
/**
* Authenticates requests using AWS credentials.
*/
class AwsNativeSource implements ExternalAccountCredentialSourceInterface
{
private const CRED_VERIFICATION_QUERY = 'Action=GetCallerIdentity&Version=2011-06-15';
private string $audience;
private string $regionalCredVerificationUrl;
private ?string $regionUrl;
private ?string $securityCredentialsUrl;
private ?string $imdsv2SessionTokenUrl;
/**
* @param string $audience The audience for the credential.
* @param string $regionalCredVerificationUrl The regional AWS GetCallerIdentity action URL used to determine the
* AWS account ID and its roles. This is not called by this library, but
* is sent in the subject token to be called by the STS token server.
* @param string|null $regionUrl This URL should be used to determine the current AWS region needed for the signed
* request construction.
* @param string|null $securityCredentialsUrl The AWS metadata server URL used to retrieve the access key, secret
* key and security token needed to sign the GetCallerIdentity request.
* @param string|null $imdsv2SessionTokenUrl Presence of this URL enforces the auth libraries to fetch a Session
* Token from AWS. This field is required for EC2 instances using IMDSv2.
*/
public function __construct(
string $audience,
string $regionalCredVerificationUrl,
string $regionUrl = null,
string $securityCredentialsUrl = null,
string $imdsv2SessionTokenUrl = null
) {
$this->audience = $audience;
$this->regionalCredVerificationUrl = $regionalCredVerificationUrl;
$this->regionUrl = $regionUrl;
$this->securityCredentialsUrl = $securityCredentialsUrl;
$this->imdsv2SessionTokenUrl = $imdsv2SessionTokenUrl;
}
public function fetchSubjectToken(callable $httpHandler = null): string
{
if (is_null($httpHandler)) {
$httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
}
$headers = [];
if ($this->imdsv2SessionTokenUrl) {
$headers = [
'X-aws-ec2-metadata-token' => self::getImdsV2SessionToken($this->imdsv2SessionTokenUrl, $httpHandler)
];
}
if (!$signingVars = self::getSigningVarsFromEnv()) {
if (!$this->securityCredentialsUrl) {
throw new \LogicException('Unable to get credentials from ENV, and no security credentials URL provided');
}
$signingVars = self::getSigningVarsFromUrl(
$httpHandler,
$this->securityCredentialsUrl,
self::getRoleName($httpHandler, $this->securityCredentialsUrl, $headers),
$headers
);
}
if (!$region = self::getRegionFromEnv()) {
if (!$this->regionUrl) {
throw new \LogicException('Unable to get region from ENV, and no region URL provided');
}
$region = self::getRegionFromUrl($httpHandler, $this->regionUrl, $headers);
}
$url = str_replace('{region}', $region, $this->regionalCredVerificationUrl);
$host = parse_url($url)['host'] ?? '';
// From here we use the signing vars to create the signed request to receive a token
[$accessKeyId, $secretAccessKey, $securityToken] = $signingVars;
$headers = self::getSignedRequestHeaders($region, $host, $accessKeyId, $secretAccessKey, $securityToken);
// Inject x-goog-cloud-target-resource into header
$headers['x-goog-cloud-target-resource'] = $this->audience;
// Format headers as they're expected in the subject token
$formattedHeaders= array_map(
fn ($k, $v) => ['key' => $k, 'value' => $v],
array_keys($headers),
$headers,
);
$request = [
'headers' => $formattedHeaders,
'method' => 'POST',
'url' => $url,
];
return urlencode(json_encode($request) ?: '');
}
/**
* @internal
*/
public static function getImdsV2SessionToken(string $imdsV2Url, callable $httpHandler): string
{
$headers = [
'X-aws-ec2-metadata-token-ttl-seconds' => '21600'
];
$request = new Request(
'PUT',
$imdsV2Url,
$headers
);
$response = $httpHandler($request);
return (string) $response->getBody();
}
/**
* @see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
*
* @internal
*
* @return array<string, string>
*/
public static function getSignedRequestHeaders(
string $region,
string $host,
string $accessKeyId,
string $secretAccessKey,
?string $securityToken
): array {
$service = 'sts';
# Create a date for headers and the credential string in ISO-8601 format
$amzdate = gmdate('Ymd\THis\Z');
$datestamp = gmdate('Ymd'); # Date w/o time, used in credential scope
# Create the canonical headers and signed headers. Header names
# must be trimmed and lowercase, and sorted in code point order from
# low to high. Note that there is a trailing \n.
$canonicalHeaders = sprintf("host:%s\nx-amz-date:%s\n", $host, $amzdate);
if ($securityToken) {
$canonicalHeaders .= sprintf("x-amz-security-token:%s\n", $securityToken);
}
# Step 5: Create the list of signed headers. This lists the headers
# in the canonicalHeaders list, delimited with ";" and in alpha order.
# Note: The request can include any headers; $canonicalHeaders and
# $signedHeaders lists those that you want to be included in the
# hash of the request. "Host" and "x-amz-date" are always required.
$signedHeaders = 'host;x-amz-date';
if ($securityToken) {
$signedHeaders .= ';x-amz-security-token';
}
# Step 6: Create payload hash (hash of the request body content). For GET
# requests, the payload is an empty string ("").
$payloadHash = hash('sha256', '');
# Step 7: Combine elements to create canonical request
$canonicalRequest = implode("\n", [
'POST', // method
'/', // canonical URL
self::CRED_VERIFICATION_QUERY, // query string
$canonicalHeaders,
$signedHeaders,
$payloadHash
]);
# ************* TASK 2: CREATE THE STRING TO SIGN*************
# Match the algorithm to the hashing algorithm you use, either SHA-1 or
# SHA-256 (recommended)
$algorithm = 'AWS4-HMAC-SHA256';
$scope = implode('/', [$datestamp, $region, $service, 'aws4_request']);
$stringToSign = implode("\n", [$algorithm, $amzdate, $scope, hash('sha256', $canonicalRequest)]);
# ************* TASK 3: CALCULATE THE SIGNATURE *************
# Create the signing key using the function defined above.
// (done above)
$signingKey = self::getSignatureKey($secretAccessKey, $datestamp, $region, $service);
# Sign the string_to_sign using the signing_key
$signature = bin2hex(self::hmacSign($signingKey, $stringToSign));
# ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
# The signing information can be either in a query string value or in
# a header named Authorization. This code shows how to use a header.
# Create authorization header and add to request headers
$authorizationHeader = sprintf(
'%s Credential=%s/%s, SignedHeaders=%s, Signature=%s',
$algorithm,
$accessKeyId,
$scope,
$signedHeaders,
$signature
);
# The request can include any headers, but MUST include "host", "x-amz-date",
# and (for this scenario) "Authorization". "host" and "x-amz-date" must
# be included in the canonical_headers and signed_headers, as noted
# earlier. Order here is not significant.
$headers = [
'host' => $host,
'x-amz-date' => $amzdate,
'Authorization' => $authorizationHeader,
];
if ($securityToken) {
$headers['x-amz-security-token'] = $securityToken;
}
return $headers;
}
/**
* @internal
*/
public static function getRegionFromEnv(): ?string
{
$region = getenv('AWS_REGION');
if (empty($region)) {
$region = getenv('AWS_DEFAULT_REGION');
}
return $region ?: null;
}
/**
* @internal
*
* @param callable $httpHandler
* @param string $regionUrl
* @param array<string, string|string[]> $headers Request headers to send in with the request.
*/
public static function getRegionFromUrl(callable $httpHandler, string $regionUrl, array $headers): string
{
// get the region/zone from the region URL
$regionRequest = new Request('GET', $regionUrl, $headers);
$regionResponse = $httpHandler($regionRequest);
// Remove last character. For example, if us-east-2b is returned,
// the region would be us-east-2.
return substr((string) $regionResponse->getBody(), 0, -1);
}
/**
* @internal
*
* @param callable $httpHandler
* @param string $securityCredentialsUrl
* @param array<string, string|string[]> $headers Request headers to send in with the request.
*/
public static function getRoleName(callable $httpHandler, string $securityCredentialsUrl, array $headers): string
{
// Get the AWS role name
$roleRequest = new Request('GET', $securityCredentialsUrl, $headers);
$roleResponse = $httpHandler($roleRequest);
$roleName = (string) $roleResponse->getBody();
return $roleName;
}
/**
* @internal
*
* @param callable $httpHandler
* @param string $securityCredentialsUrl
* @param array<string, string|string[]> $headers Request headers to send in with the request.
* @return array{string, string, ?string}
*/
public static function getSigningVarsFromUrl(
callable $httpHandler,
string $securityCredentialsUrl,
string $roleName,
array $headers
): array {
// Get the AWS credentials
$credsRequest = new Request(
'GET',
$securityCredentialsUrl . '/' . $roleName,
$headers
);
$credsResponse = $httpHandler($credsRequest);
$awsCreds = json_decode((string) $credsResponse->getBody(), true);
return [
$awsCreds['AccessKeyId'], // accessKeyId
$awsCreds['SecretAccessKey'], // secretAccessKey
$awsCreds['Token'], // token
];
}
/**
* @internal
*
* @return array{string, string, ?string}
*/
public static function getSigningVarsFromEnv(): ?array
{
$accessKeyId = getenv('AWS_ACCESS_KEY_ID');
$secretAccessKey = getenv('AWS_SECRET_ACCESS_KEY');
if ($accessKeyId && $secretAccessKey) {
return [
$accessKeyId,
$secretAccessKey,
getenv('AWS_SESSION_TOKEN') ?: null, // session token (can be null)
];
}
return null;
}
/**
* Gets the unique key for caching
* For AwsNativeSource the values are:
* Imdsv2SessionTokenUrl.SecurityCredentialsUrl.RegionUrl.RegionalCredVerificationUrl
*
* @return string
*/
public function getCacheKey(): string
{
return ($this->imdsv2SessionTokenUrl ?? '') .
'.' . ($this->securityCredentialsUrl ?? '') .
'.' . $this->regionUrl .
'.' . $this->regionalCredVerificationUrl;
}
/**
* Return HMAC hash in binary string
*/
private static function hmacSign(string $key, string $msg): string
{
return hash_hmac('sha256', self::utf8Encode($msg), $key, true);
}
/**
* @TODO add a fallback when mbstring is not available
*/
private static function utf8Encode(string $string): string
{
return mb_convert_encoding($string, 'UTF-8', 'ISO-8859-1');
}
private static function getSignatureKey(
string $key,
string $dateStamp,
string $regionName,
string $serviceName
): string {
$kDate = self::hmacSign(self::utf8Encode('AWS4' . $key), $dateStamp);
$kRegion = self::hmacSign($kDate, $regionName);
$kService = self::hmacSign($kRegion, $serviceName);
$kSigning = self::hmacSign($kService, 'aws4_request');
return $kSigning;
}
}

View File

@@ -0,0 +1,272 @@
<?php
/*
* Copyright 2024 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\CredentialSource;
use Google\Auth\ExecutableHandler\ExecutableHandler;
use Google\Auth\ExecutableHandler\ExecutableResponseError;
use Google\Auth\ExternalAccountCredentialSourceInterface;
use RuntimeException;
/**
* ExecutableSource enables the exchange of workload identity pool external credentials for
* Google access tokens by retrieving 3rd party tokens through a user supplied executable. These
* scripts/executables are completely independent of the Google Cloud Auth libraries. These
* credentials plug into ADC and will call the specified executable to retrieve the 3rd party token
* to be exchanged for a Google access token.
*
* To use these credentials, the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable
* must be set to '1'. This is for security reasons.
*
* Both OIDC and SAML are supported. The executable must adhere to a specific response format
* defined below.
*
* The executable must print out the 3rd party token to STDOUT in JSON format. When an
* output_file is specified in the credential configuration, the executable must also handle writing the
* JSON response to this file.
*
* <pre>
* OIDC response sample:
* {
* "version": 1,
* "success": true,
* "token_type": "urn:ietf:params:oauth:token-type:id_token",
* "id_token": "HEADER.PAYLOAD.SIGNATURE",
* "expiration_time": 1620433341
* }
*
* SAML2 response sample:
* {
* "version": 1,
* "success": true,
* "token_type": "urn:ietf:params:oauth:token-type:saml2",
* "saml_response": "...",
* "expiration_time": 1620433341
* }
*
* Error response sample:
* {
* "version": 1,
* "success": false,
* "code": "401",
* "message": "Error message."
* }
* </pre>
*
* The "expiration_time" field in the JSON response is only required for successful
* responses when an output file was specified in the credential configuration
*
* The auth libraries will populate certain environment variables that will be accessible by the
* executable, such as: GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE, GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE,
* GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE, GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL, and
* GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE.
*/
class ExecutableSource implements ExternalAccountCredentialSourceInterface
{
private const GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES = 'GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES';
private const SAML_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:saml2';
private const OIDC_SUBJECT_TOKEN_TYPE1 = 'urn:ietf:params:oauth:token-type:id_token';
private const OIDC_SUBJECT_TOKEN_TYPE2 = 'urn:ietf:params:oauth:token-type:jwt';
private string $command;
private ExecutableHandler $executableHandler;
private ?string $outputFile;
/**
* @param string $command The string command to run to get the subject token.
* @param string $outputFile
*/
public function __construct(
string $command,
?string $outputFile,
ExecutableHandler $executableHandler = null,
) {
$this->command = $command;
$this->outputFile = $outputFile;
$this->executableHandler = $executableHandler ?: new ExecutableHandler();
}
/**
* Gets the unique key for caching
* The format for the cache key is:
* Command.OutputFile
*
* @return ?string
*/
public function getCacheKey(): ?string
{
return $this->command . '.' . $this->outputFile;
}
/**
* @param callable $httpHandler unused.
* @return string
* @throws RuntimeException if the executable is not allowed to run.
* @throws ExecutableResponseError if the executable response is invalid.
*/
public function fetchSubjectToken(callable $httpHandler = null): string
{
// Check if the executable is allowed to run.
if (getenv(self::GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES) !== '1') {
throw new RuntimeException(
'Pluggable Auth executables need to be explicitly allowed to run by '
. 'setting the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment '
. 'Variable to 1.'
);
}
if (!$executableResponse = $this->getCachedExecutableResponse()) {
// Run the executable.
$exitCode = ($this->executableHandler)($this->command);
$output = $this->executableHandler->getOutput();
// If the exit code is not 0, throw an exception with the output as the error details
if ($exitCode !== 0) {
throw new ExecutableResponseError(
'The executable failed to run'
. ($output ? ' with the following error: ' . $output : '.'),
(string) $exitCode
);
}
$executableResponse = $this->parseExecutableResponse($output);
// Validate expiration.
if (isset($executableResponse['expiration_time']) && time() >= $executableResponse['expiration_time']) {
throw new ExecutableResponseError('Executable response is expired.');
}
}
// Throw error when the request was unsuccessful
if ($executableResponse['success'] === false) {
throw new ExecutableResponseError($executableResponse['message'], (string) $executableResponse['code']);
}
// Return subject token field based on the token type
return $executableResponse['token_type'] === self::SAML_SUBJECT_TOKEN_TYPE
? $executableResponse['saml_response']
: $executableResponse['id_token'];
}
/**
* @return array<string, mixed>|null
*/
private function getCachedExecutableResponse(): ?array
{
if (
$this->outputFile
&& file_exists($this->outputFile)
&& !empty(trim($outputFileContents = (string) file_get_contents($this->outputFile)))
) {
try {
$executableResponse = $this->parseExecutableResponse($outputFileContents);
} catch (ExecutableResponseError $e) {
throw new ExecutableResponseError(
'Error in output file: ' . $e->getMessage(),
'INVALID_OUTPUT_FILE'
);
}
if ($executableResponse['success'] === false) {
// If the cached token was unsuccessful, run the executable to get a new one.
return null;
}
if (isset($executableResponse['expiration_time']) && time() >= $executableResponse['expiration_time']) {
// If the cached token is expired, run the executable to get a new one.
return null;
}
return $executableResponse;
}
return null;
}
/**
* @return array<string, mixed>
*/
private function parseExecutableResponse(string $response): array
{
$executableResponse = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new ExecutableResponseError(
'The executable returned an invalid response: ' . $response,
'INVALID_RESPONSE'
);
}
if (!array_key_exists('version', $executableResponse)) {
throw new ExecutableResponseError('Executable response must contain a "version" field.');
}
if (!array_key_exists('success', $executableResponse)) {
throw new ExecutableResponseError('Executable response must contain a "success" field.');
}
// Validate required fields for a successful response.
if ($executableResponse['success']) {
// Validate token type field.
$tokenTypes = [self::SAML_SUBJECT_TOKEN_TYPE, self::OIDC_SUBJECT_TOKEN_TYPE1, self::OIDC_SUBJECT_TOKEN_TYPE2];
if (!isset($executableResponse['token_type'])) {
throw new ExecutableResponseError(
'Executable response must contain a "token_type" field when successful'
);
}
if (!in_array($executableResponse['token_type'], $tokenTypes)) {
throw new ExecutableResponseError(sprintf(
'Executable response "token_type" field must be one of %s.',
implode(', ', $tokenTypes)
));
}
// Validate subject token for SAML and OIDC.
if ($executableResponse['token_type'] === self::SAML_SUBJECT_TOKEN_TYPE) {
if (empty($executableResponse['saml_response'])) {
throw new ExecutableResponseError(sprintf(
'Executable response must contain a "saml_response" field when token_type=%s.',
self::SAML_SUBJECT_TOKEN_TYPE
));
}
} elseif (empty($executableResponse['id_token'])) {
throw new ExecutableResponseError(sprintf(
'Executable response must contain a "id_token" field when '
. 'token_type=%s.',
$executableResponse['token_type']
));
}
// Validate expiration exists when an output file is specified.
if ($this->outputFile) {
if (!isset($executableResponse['expiration_time'])) {
throw new ExecutableResponseError(
'The executable response must contain a "expiration_time" field for successful responses ' .
'when an output_file has been specified in the configuration.'
);
}
}
} else {
// Both code and message must be provided for unsuccessful responses.
if (!array_key_exists('code', $executableResponse)) {
throw new ExecutableResponseError('Executable response must contain a "code" field when unsuccessful.');
}
if (empty($executableResponse['message'])) {
throw new ExecutableResponseError('Executable response must contain a "message" field when unsuccessful.');
}
}
return $executableResponse;
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\CredentialSource;
use Google\Auth\ExternalAccountCredentialSourceInterface;
use InvalidArgumentException;
use UnexpectedValueException;
/**
* Retrieve a token from a file.
*/
class FileSource implements ExternalAccountCredentialSourceInterface
{
private string $file;
private ?string $format;
private ?string $subjectTokenFieldName;
/**
* @param string $file The file to read the subject token from.
* @param string $format The format of the token in the file. Can be null or "json".
* @param string $subjectTokenFieldName The name of the field containing the token in the file. This is required
* when format is "json".
*/
public function __construct(
string $file,
string $format = null,
string $subjectTokenFieldName = null
) {
$this->file = $file;
if ($format === 'json' && is_null($subjectTokenFieldName)) {
throw new InvalidArgumentException(
'subject_token_field_name must be set when format is JSON'
);
}
$this->format = $format;
$this->subjectTokenFieldName = $subjectTokenFieldName;
}
public function fetchSubjectToken(callable $httpHandler = null): string
{
$contents = file_get_contents($this->file);
if ($this->format === 'json') {
if (!$json = json_decode((string) $contents, true)) {
throw new UnexpectedValueException(
'Unable to decode JSON file'
);
}
if (!isset($json[$this->subjectTokenFieldName])) {
throw new UnexpectedValueException(
'subject_token_field_name not found in JSON file'
);
}
$contents = $json[$this->subjectTokenFieldName];
}
return $contents;
}
/**
* Gets the unique key for caching.
* The format for the cache key one of the following:
* Filename
*
* @return string
*/
public function getCacheKey(): ?string
{
return $this->file;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\CredentialSource;
use Google\Auth\ExternalAccountCredentialSourceInterface;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
use UnexpectedValueException;
/**
* Retrieve a token from a URL.
*/
class UrlSource implements ExternalAccountCredentialSourceInterface
{
private string $url;
private ?string $format;
private ?string $subjectTokenFieldName;
/**
* @var array<string, string|string[]>
*/
private ?array $headers;
/**
* @param string $url The URL to fetch the subject token from.
* @param string $format The format of the token in the response. Can be null or "json".
* @param string $subjectTokenFieldName The name of the field containing the token in the response. This is required
* when format is "json".
* @param array<string, string|string[]> $headers Request headers to send in with the request to the URL.
*/
public function __construct(
string $url,
string $format = null,
string $subjectTokenFieldName = null,
array $headers = null
) {
$this->url = $url;
if ($format === 'json' && is_null($subjectTokenFieldName)) {
throw new InvalidArgumentException(
'subject_token_field_name must be set when format is JSON'
);
}
$this->format = $format;
$this->subjectTokenFieldName = $subjectTokenFieldName;
$this->headers = $headers;
}
public function fetchSubjectToken(callable $httpHandler = null): string
{
if (is_null($httpHandler)) {
$httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
}
$request = new Request(
'GET',
$this->url,
$this->headers ?: []
);
$response = $httpHandler($request);
$body = (string) $response->getBody();
if ($this->format === 'json') {
if (!$json = json_decode((string) $body, true)) {
throw new UnexpectedValueException(
'Unable to decode JSON response'
);
}
if (!isset($json[$this->subjectTokenFieldName])) {
throw new UnexpectedValueException(
'subject_token_field_name not found in JSON file'
);
}
$body = $json[$this->subjectTokenFieldName];
}
return $body;
}
/**
* Get the cache key for the credentials.
* The format for the cache key is:
* URL
*
* @return ?string
*/
public function getCacheKey(): ?string
{
return $this->url;
}
}

View File

@@ -0,0 +1,238 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
/*
* The AppIdentityService class is automatically defined on App Engine,
* so including this dependency is not necessary, and will result in a
* PHP fatal error in the App Engine environment.
*/
use google\appengine\api\app_identity\AppIdentityService;
use Google\Auth\CredentialsLoader;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\SignBlobInterface;
/**
* @deprecated
*
* AppIdentityCredentials supports authorization on Google App Engine.
*
* It can be used to authorize requests using the AuthTokenMiddleware or
* AuthTokenSubscriber, but will only succeed if being run on App Engine:
*
* Example:
* ```
* use Google\Auth\Credentials\AppIdentityCredentials;
* use Google\Auth\Middleware\AuthTokenMiddleware;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $gae = new AppIdentityCredentials('https://www.googleapis.com/auth/books');
* $middleware = new AuthTokenMiddleware($gae);
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/books/v1',
* 'auth' => 'google_auth'
* ]);
*
* $res = $client->get('volumes?q=Henry+David+Thoreau&country=US');
* ```
*/
class AppIdentityCredentials extends CredentialsLoader implements
SignBlobInterface,
ProjectIdProviderInterface
{
/**
* Result of fetchAuthToken.
*
* @var array<mixed>
*/
protected $lastReceivedToken;
/**
* Array of OAuth2 scopes to be requested.
*
* @var string[]
*/
private $scope;
/**
* @var string
*/
private $clientName;
/**
* @param string|string[] $scope One or more scopes.
*/
public function __construct($scope = [])
{
$this->scope = is_array($scope) ? $scope : explode(' ', (string) $scope);
}
/**
* Determines if this an App Engine instance, by accessing the
* SERVER_SOFTWARE environment variable (prod) or the APPENGINE_RUNTIME
* environment variable (dev).
*
* @return bool true if this an App Engine Instance, false otherwise
*/
public static function onAppEngine()
{
$appEngineProduction = isset($_SERVER['SERVER_SOFTWARE']) &&
0 === strpos($_SERVER['SERVER_SOFTWARE'], 'Google App Engine');
if ($appEngineProduction) {
return true;
}
$appEngineDevAppServer = isset($_SERVER['APPENGINE_RUNTIME']) &&
$_SERVER['APPENGINE_RUNTIME'] == 'php';
if ($appEngineDevAppServer) {
return true;
}
return false;
}
/**
* Implements FetchAuthTokenInterface#fetchAuthToken.
*
* Fetches the auth tokens using the AppIdentityService if available.
* As the AppIdentityService uses protobufs to fetch the access token,
* the GuzzleHttp\ClientInterface instance passed in will not be used.
*
* @param callable $httpHandler callback which delivers psr7 request
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type string $expiration_time
* }
*/
public function fetchAuthToken(callable $httpHandler = null)
{
try {
$this->checkAppEngineContext();
} catch (\Exception $e) {
return [];
}
/** @phpstan-ignore-next-line */
$token = AppIdentityService::getAccessToken($this->scope);
$this->lastReceivedToken = $token;
return $token;
}
/**
* Sign a string using AppIdentityService.
*
* @param string $stringToSign The string to sign.
* @param bool $forceOpenSsl [optional] Does not apply to this credentials
* type.
* @return string The signature, base64-encoded.
* @throws \Exception If AppEngine SDK or mock is not available.
*/
public function signBlob($stringToSign, $forceOpenSsl = false)
{
$this->checkAppEngineContext();
/** @phpstan-ignore-next-line */
return base64_encode(AppIdentityService::signForApp($stringToSign)['signature']);
}
/**
* Get the project ID from AppIdentityService.
*
* Returns null if AppIdentityService is unavailable.
*
* @param callable $httpHandler Not used by this type.
* @return string|null
*/
public function getProjectId(callable $httpHandler = null)
{
try {
$this->checkAppEngineContext();
} catch (\Exception $e) {
return null;
}
/** @phpstan-ignore-next-line */
return AppIdentityService::getApplicationId();
}
/**
* Get the client name from AppIdentityService.
*
* Subsequent calls to this method will return a cached value.
*
* @param callable $httpHandler Not used in this implementation.
* @return string
* @throws \Exception If AppEngine SDK or mock is not available.
*/
public function getClientName(callable $httpHandler = null)
{
$this->checkAppEngineContext();
if (!$this->clientName) {
/** @phpstan-ignore-next-line */
$this->clientName = AppIdentityService::getServiceAccountName();
}
return $this->clientName;
}
/**
* @return array{access_token:string,expires_at:int}|null
*/
public function getLastReceivedToken()
{
if ($this->lastReceivedToken) {
return [
'access_token' => $this->lastReceivedToken['access_token'],
'expires_at' => $this->lastReceivedToken['expiration_time'],
];
}
return null;
}
/**
* Caching is handled by the underlying AppIdentityService, return empty string
* to prevent caching.
*
* @return string
*/
public function getCacheKey()
{
return '';
}
/**
* @return void
*/
private function checkAppEngineContext()
{
if (!self::onAppEngine() || !class_exists('google\appengine\api\app_identity\AppIdentityService')) {
throw new \Exception(
'This class must be run in App Engine, or you must include the AppIdentityService '
. 'mock class defined in tests/mocks/AppIdentityService.php'
);
}
}
}

View File

@@ -0,0 +1,378 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Google\Auth\CredentialSource\AwsNativeSource;
use Google\Auth\CredentialSource\ExecutableSource;
use Google\Auth\CredentialSource\FileSource;
use Google\Auth\CredentialSource\UrlSource;
use Google\Auth\ExecutableHandler\ExecutableHandler;
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,
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
* or as a space-delimited string.
* @param array<mixed> $jsonKey JSON credentials as an associative array.
*/
public function __construct(
$scope,
array $jsonKey
) {
if (!array_key_exists('type', $jsonKey)) {
throw new InvalidArgumentException('json key is missing the type field');
}
if ($jsonKey['type'] !== self::EXTERNAL_ACCOUNT_TYPE) {
throw new InvalidArgumentException(sprintf(
'expected "%s" type but received "%s"',
self::EXTERNAL_ACCOUNT_TYPE,
$jsonKey['type']
));
}
if (!array_key_exists('token_url', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the token_url field'
);
}
if (!array_key_exists('audience', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the audience field'
);
}
if (!array_key_exists('subject_token_type', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the subject_token_type field'
);
}
if (!array_key_exists('credential_source', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the credential_source field'
);
}
$this->serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'] ?? null;
$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.'
);
}
}
/**
* @param array<mixed> $jsonKey
*/
private static function buildCredentialSource(array $jsonKey): ExternalAccountCredentialSourceInterface
{
$credentialSource = $jsonKey['credential_source'];
if (isset($credentialSource['file'])) {
return new FileSource(
$credentialSource['file'],
$credentialSource['format']['type'] ?? null,
$credentialSource['format']['subject_token_field_name'] ?? null
);
}
if (
isset($credentialSource['environment_id'])
&& 1 === preg_match('/^aws(\d+)$/', $credentialSource['environment_id'], $matches)
) {
if ($matches[1] !== '1') {
throw new InvalidArgumentException(
"aws version \"$matches[1]\" is not supported in the current build."
);
}
if (!array_key_exists('regional_cred_verification_url', $credentialSource)) {
throw new InvalidArgumentException(
'The regional_cred_verification_url field is required for aws1 credential source.'
);
}
return new AwsNativeSource(
$jsonKey['audience'],
$credentialSource['regional_cred_verification_url'], // $regionalCredVerificationUrl
$credentialSource['region_url'] ?? null, // $regionUrl
$credentialSource['url'] ?? null, // $securityCredentialsUrl
$credentialSource['imdsv2_session_token_url'] ?? null, // $imdsV2TokenUrl
);
}
if (isset($credentialSource['url'])) {
return new UrlSource(
$credentialSource['url'],
$credentialSource['format']['type'] ?? null,
$credentialSource['format']['subject_token_field_name'] ?? null,
$credentialSource['headers'] ?? null,
);
}
if (isset($credentialSource['executable'])) {
if (!array_key_exists('command', $credentialSource['executable'])) {
throw new InvalidArgumentException(
'executable source requires a command to be set in the JSON file.'
);
}
// Build command environment variables
$env = [
'GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE' => $jsonKey['audience'],
'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE' => $jsonKey['subject_token_type'],
// Always set to 0 because interactive mode is not supported.
'GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE' => '0',
];
if ($outputFile = $credentialSource['executable']['output_file'] ?? null) {
$env['GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE'] = $outputFile;
}
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'];
}
}
$timeoutMs = $credentialSource['executable']['timeout_millis'] ?? null;
return new ExecutableSource(
$credentialSource['executable']['command'],
$outputFile,
$timeoutMs ? new ExecutableHandler($env, $timeoutMs) : new ExecutableHandler($env)
);
}
throw new InvalidArgumentException('Unable to determine credential source from json key.');
}
/**
* @param string $stsToken
* @param callable $httpHandler
*
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type int $expires_at
* }
*/
private function getImpersonatedAccessToken(string $stsToken, callable $httpHandler = null): array
{
if (!isset($this->serviceAccountImpersonationUrl)) {
throw new InvalidArgumentException(
'service_account_impersonation_url must be set in JSON credentials.'
);
}
$request = new Request(
'POST',
$this->serviceAccountImpersonationUrl,
[
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $stsToken,
],
(string) json_encode([
'lifetime' => sprintf('%ss', OAuth2::DEFAULT_EXPIRY_SECONDS),
'scope' => explode(' ', $this->auth->getScope()),
]),
);
if (is_null($httpHandler)) {
$httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
}
$response = $httpHandler($request);
$body = json_decode((string) $response->getBody(), true);
return [
'access_token' => $body['accessToken'],
'expires_at' => strtotime($body['expireTime']),
];
}
/**
* @param callable $httpHandler
*
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type int $expires_at (impersonated service accounts only)
* @type int $expires_in (identity pool only)
* @type string $issued_token_type (identity pool only)
* @type string $token_type (identity pool only)
* }
*/
public function fetchAuthToken(callable $httpHandler = null)
{
$stsToken = $this->auth->fetchAuthToken($httpHandler);
if (isset($this->serviceAccountImpersonationUrl)) {
return $this->getImpersonatedAccessToken($stsToken['access_token'], $httpHandler);
}
return $stsToken;
}
/**
* Get the cache token key for the credentials.
* The cache token key format depends on the type of source
* The format for the cache key one of the following:
* FetcherCacheKey.Scope.[ServiceAccount].[TokenType].[WorkforcePoolUserProject]
* FetcherCacheKey.Audience.[ServiceAccount].[TokenType].[WorkforcePoolUserProject]
*
* @return ?string;
*/
public function getCacheKey(): ?string
{
$scopeOrAudience = $this->auth->getAudience();
if (!$scopeOrAudience) {
$scopeOrAudience = $this->auth->getScope();
}
return $this->auth->getSubjectTokenFetcher()->getCacheKey() .
'.' . $scopeOrAudience .
'.' . ($this->serviceAccountImpersonationUrl ?? '') .
'.' . ($this->auth->getSubjectTokenType() ?? '') .
'.' . ($this->workforcePoolUserProject ?? '');
}
public function getLastReceivedToken()
{
return $this->auth->getLastReceivedToken();
}
/**
* Get the quota project used for this API request
*
* @return string|null
*/
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;
}
}

View File

@@ -0,0 +1,683 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use COM;
use com_exception;
use Google\Auth\CredentialsLoader;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\Iam;
use Google\Auth\IamSignerTrait;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\SignBlobInterface;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
/**
* GCECredentials supports authorization on Google Compute Engine.
*
* It can be used to authorize requests using the AuthTokenMiddleware, but will
* only succeed if being run on GCE:
*
* use Google\Auth\Credentials\GCECredentials;
* use Google\Auth\Middleware\AuthTokenMiddleware;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $gce = new GCECredentials();
* $middleware = new AuthTokenMiddleware($gce);
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
* 'auth' => 'google_auth'
* ]);
*
* $res = $client->get('myproject/taskqueues/myqueue');
*/
class GCECredentials extends CredentialsLoader implements
SignBlobInterface,
ProjectIdProviderInterface,
GetQuotaProjectInterface
{
use IamSignerTrait;
// phpcs:disable
const cacheKey = 'GOOGLE_AUTH_PHP_GCE';
// phpcs:enable
/**
* The metadata IP address on appengine instances.
*
* The IP is used instead of the domain 'metadata' to avoid slow responses
* when not on Compute Engine.
*/
const METADATA_IP = '169.254.169.254';
/**
* The metadata path of the default token.
*/
const TOKEN_URI_PATH = 'v1/instance/service-accounts/default/token';
/**
* The metadata path of the default id token.
*/
const ID_TOKEN_URI_PATH = 'v1/instance/service-accounts/default/identity';
/**
* The metadata path of the client ID.
*/
const CLIENT_ID_URI_PATH = 'v1/instance/service-accounts/default/email';
/**
* The metadata path of the project ID.
*/
const PROJECT_ID_URI_PATH = 'v1/project/project-id';
/**
* The metadata path of the project ID.
*/
const UNIVERSE_DOMAIN_URI_PATH = 'v1/universe/universe_domain';
/**
* The header whose presence indicates GCE presence.
*/
const FLAVOR_HEADER = 'Metadata-Flavor';
/**
* The Linux file which contains the product name.
*/
private const GKE_PRODUCT_NAME_FILE = '/sys/class/dmi/id/product_name';
/**
* The Windows Registry key path to the product name
*/
private const WINDOWS_REGISTRY_KEY_PATH = 'HKEY_LOCAL_MACHINE\\SYSTEM\\HardwareConfig\\Current\\';
/**
* The Windows registry key name for the product name
*/
private const WINDOWS_REGISTRY_KEY_NAME = 'SystemProductName';
/**
* The Name of the product expected from the windows registry
*/
private const PRODUCT_NAME = 'Google';
private const CRED_TYPE = 'mds';
/**
* Note: the explicit `timeout` and `tries` below is a workaround. The underlying
* issue is that resolving an unknown host on some networks will take
* 20-30 seconds; making this timeout short fixes the issue, but
* could lead to false negatives in the event that we are on GCE, but
* the metadata resolution was particularly slow. The latter case is
* "unlikely" since the expected 4-nines time is about 0.5 seconds.
* This allows us to limit the total ping maximum timeout to 1.5 seconds
* for developer desktop scenarios.
*/
const MAX_COMPUTE_PING_TRIES = 3;
const COMPUTE_PING_CONNECTION_TIMEOUT_S = 0.5;
/**
* Flag used to ensure that the onGCE test is only done once;.
*
* @var bool
*/
private $hasCheckedOnGce = false;
/**
* Flag that stores the value of the onGCE check.
*
* @var bool
*/
private $isOnGce = false;
/**
* Result of fetchAuthToken.
*
* @var array<mixed>
*/
protected $lastReceivedToken;
/**
* @var string|null
*/
private $clientName;
/**
* @var string|null
*/
private $projectId;
/**
* @var string
*/
private $tokenUri;
/**
* @var string
*/
private $targetAudience;
/**
* @var string|null
*/
private $quotaProject;
/**
* @var string|null
*/
private $serviceAccountIdentity;
/**
* @var string
*/
private ?string $universeDomain;
/**
* @param Iam $iam [optional] An IAM instance.
* @param string|string[] $scope [optional] the scope of the access request,
* expressed either as an array or as a space-delimited string.
* @param string $targetAudience [optional] The audience for the ID token.
* @param string $quotaProject [optional] Specifies a project to bill for access
* charges associated with the request.
* @param string $serviceAccountIdentity [optional] Specify a service
* account identity name to use instead of "default".
* @param string $universeDomain [optional] Specify a universe domain to use
* instead of fetching one from the metadata server.
*/
public function __construct(
Iam $iam = null,
$scope = null,
$targetAudience = null,
$quotaProject = null,
$serviceAccountIdentity = null,
string $universeDomain = null
) {
$this->iam = $iam;
if ($scope && $targetAudience) {
throw new InvalidArgumentException(
'Scope and targetAudience cannot both be supplied'
);
}
$tokenUri = self::getTokenUri($serviceAccountIdentity);
if ($scope) {
if (is_string($scope)) {
$scope = explode(' ', $scope);
}
$scope = implode(',', $scope);
$tokenUri = $tokenUri . '?scopes=' . $scope;
} elseif ($targetAudience) {
$tokenUri = self::getIdTokenUri($serviceAccountIdentity);
$tokenUri = $tokenUri . '?audience=' . $targetAudience;
$this->targetAudience = $targetAudience;
}
$this->tokenUri = $tokenUri;
$this->quotaProject = $quotaProject;
$this->serviceAccountIdentity = $serviceAccountIdentity;
$this->universeDomain = $universeDomain;
}
/**
* The full uri for accessing the default token.
*
* @param string $serviceAccountIdentity [optional] Specify a service
* account identity name to use instead of "default".
* @return string
*/
public static function getTokenUri($serviceAccountIdentity = null)
{
$base = 'http://' . self::METADATA_IP . '/computeMetadata/';
$base .= self::TOKEN_URI_PATH;
if ($serviceAccountIdentity) {
return str_replace(
'/default/',
'/' . $serviceAccountIdentity . '/',
$base
);
}
return $base;
}
/**
* The full uri for accessing the default service account.
*
* @param string $serviceAccountIdentity [optional] Specify a service
* account identity name to use instead of "default".
* @return string
*/
public static function getClientNameUri($serviceAccountIdentity = null)
{
$base = 'http://' . self::METADATA_IP . '/computeMetadata/';
$base .= self::CLIENT_ID_URI_PATH;
if ($serviceAccountIdentity) {
return str_replace(
'/default/',
'/' . $serviceAccountIdentity . '/',
$base
);
}
return $base;
}
/**
* The full uri for accesesing the default identity token.
*
* @param string $serviceAccountIdentity [optional] Specify a service
* account identity name to use instead of "default".
* @return string
*/
private static function getIdTokenUri($serviceAccountIdentity = null)
{
$base = 'http://' . self::METADATA_IP . '/computeMetadata/';
$base .= self::ID_TOKEN_URI_PATH;
if ($serviceAccountIdentity) {
return str_replace(
'/default/',
'/' . $serviceAccountIdentity . '/',
$base
);
}
return $base;
}
/**
* The full uri for accessing the default project ID.
*
* @return string
*/
private static function getProjectIdUri()
{
$base = 'http://' . self::METADATA_IP . '/computeMetadata/';
return $base . self::PROJECT_ID_URI_PATH;
}
/**
* The full uri for accessing the default universe domain.
*
* @return string
*/
private static function getUniverseDomainUri()
{
$base = 'http://' . self::METADATA_IP . '/computeMetadata/';
return $base . self::UNIVERSE_DOMAIN_URI_PATH;
}
/**
* Determines if this an App Engine Flexible instance, by accessing the
* GAE_INSTANCE environment variable.
*
* @return bool true if this an App Engine Flexible Instance, false otherwise
*/
public static function onAppEngineFlexible()
{
return substr((string) getenv('GAE_INSTANCE'), 0, 4) === 'aef-';
}
/**
* Determines if this a GCE instance, by accessing the expected metadata
* host.
* If $httpHandler is not specified a the default HttpHandler is used.
*
* @param callable $httpHandler callback which delivers psr7 request
* @return bool True if this a GCEInstance, false otherwise
*/
public static function onGce(callable $httpHandler = null)
{
$httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
$checkUri = 'http://' . self::METADATA_IP;
for ($i = 1; $i <= self::MAX_COMPUTE_PING_TRIES; $i++) {
try {
// Comment from: oauth2client/client.py
//
// Note: the explicit `timeout` below is a workaround. The underlying
// issue is that resolving an unknown host on some networks will take
// 20-30 seconds; making this timeout short fixes the issue, but
// could lead to false negatives in the event that we are on GCE, but
// the metadata resolution was particularly slow. The latter case is
// "unlikely".
$resp = $httpHandler(
new Request(
'GET',
$checkUri,
[
self::FLAVOR_HEADER => 'Google',
self::$metricMetadataKey => self::getMetricsHeader('', 'mds')
]
),
['timeout' => self::COMPUTE_PING_CONNECTION_TIMEOUT_S]
);
return $resp->getHeaderLine(self::FLAVOR_HEADER) == 'Google';
} catch (ClientException $e) {
} catch (ServerException $e) {
} catch (RequestException $e) {
} catch (ConnectException $e) {
}
}
if (PHP_OS === 'Windows' || PHP_OS === 'WINNT') {
return self::detectResidencyWindows(
self::WINDOWS_REGISTRY_KEY_PATH . self::WINDOWS_REGISTRY_KEY_NAME
);
}
// Detect GCE residency on Linux
return self::detectResidencyLinux(self::GKE_PRODUCT_NAME_FILE);
}
private static function detectResidencyLinux(string $productNameFile): bool
{
if (file_exists($productNameFile)) {
$productName = trim((string) file_get_contents($productNameFile));
return 0 === strpos($productName, self::PRODUCT_NAME);
}
return false;
}
private static function detectResidencyWindows(string $registryProductKey): bool
{
if (!class_exists(COM::class)) {
// the COM extension must be installed and enabled to detect Windows residency
// see https://www.php.net/manual/en/book.com.php
return false;
}
$shell = new COM('WScript.Shell');
$productName = null;
try {
$productName = $shell->regRead($registryProductKey);
} catch(com_exception) {
// This means that we tried to read a key that doesn't exist on the registry
// which might mean that it is a windows instance that is not on GCE
return false;
}
return 0 === strpos($productName, self::PRODUCT_NAME);
}
/**
* Implements FetchAuthTokenInterface#fetchAuthToken.
*
* Fetches the auth tokens from the GCE metadata host if it is available.
* If $httpHandler is not specified a the default HttpHandler is used.
*
* @param callable $httpHandler callback which delivers psr7 request
*
* @return array<mixed> {
* A set of auth related metadata, based on the token type.
*
* @type string $access_token for access tokens
* @type int $expires_in for access tokens
* @type string $token_type for access tokens
* @type string $id_token for ID tokens
* }
* @throws \Exception
*/
public function fetchAuthToken(callable $httpHandler = null)
{
$httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
if (!$this->hasCheckedOnGce) {
$this->isOnGce = self::onGce($httpHandler);
$this->hasCheckedOnGce = true;
}
if (!$this->isOnGce) {
return []; // return an empty array with no access token
}
$response = $this->getFromMetadata(
$httpHandler,
$this->tokenUri,
$this->applyTokenEndpointMetrics([], $this->targetAudience ? 'it' : 'at')
);
if ($this->targetAudience) {
return $this->lastReceivedToken = ['id_token' => $response];
}
if (null === $json = json_decode($response, true)) {
throw new \Exception('Invalid JSON response');
}
$json['expires_at'] = time() + $json['expires_in'];
// store this so we can retrieve it later
$this->lastReceivedToken = $json;
return $json;
}
/**
* Returns the Cache Key for the credential token.
* The format for the cache key is:
* TokenURI
*
* @return string
*/
public function getCacheKey()
{
return $this->tokenUri;
}
/**
* @return array<mixed>|null
*/
public function getLastReceivedToken()
{
if ($this->lastReceivedToken) {
if (array_key_exists('id_token', $this->lastReceivedToken)) {
return $this->lastReceivedToken;
}
return [
'access_token' => $this->lastReceivedToken['access_token'],
'expires_at' => $this->lastReceivedToken['expires_at']
];
}
return null;
}
/**
* Get the client name from GCE metadata.
*
* Subsequent calls will return a cached value.
*
* @param callable $httpHandler callback which delivers psr7 request
* @return string
*/
public function getClientName(callable $httpHandler = null)
{
if ($this->clientName) {
return $this->clientName;
}
$httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
if (!$this->hasCheckedOnGce) {
$this->isOnGce = self::onGce($httpHandler);
$this->hasCheckedOnGce = true;
}
if (!$this->isOnGce) {
return '';
}
$this->clientName = $this->getFromMetadata(
$httpHandler,
self::getClientNameUri($this->serviceAccountIdentity)
);
return $this->clientName;
}
/**
* Fetch the default Project ID from compute engine.
*
* Returns null if called outside GCE.
*
* @param callable $httpHandler Callback which delivers psr7 request
* @return string|null
*/
public function getProjectId(callable $httpHandler = null)
{
if ($this->projectId) {
return $this->projectId;
}
$httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
if (!$this->hasCheckedOnGce) {
$this->isOnGce = self::onGce($httpHandler);
$this->hasCheckedOnGce = true;
}
if (!$this->isOnGce) {
return null;
}
$this->projectId = $this->getFromMetadata($httpHandler, self::getProjectIdUri());
return $this->projectId;
}
/**
* Fetch the default universe domain from the metadata server.
*
* @param callable $httpHandler Callback which delivers psr7 request
* @return string
*/
public function getUniverseDomain(callable $httpHandler = null): string
{
if (null !== $this->universeDomain) {
return $this->universeDomain;
}
$httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
if (!$this->hasCheckedOnGce) {
$this->isOnGce = self::onGce($httpHandler);
$this->hasCheckedOnGce = true;
}
try {
$this->universeDomain = $this->getFromMetadata(
$httpHandler,
self::getUniverseDomainUri()
);
} catch (ClientException $e) {
// If the metadata server exists, but returns a 404 for the universe domain, the auth
// libraries should safely assume this is an older metadata server running in GCU, and
// should return the default universe domain.
if (!$e->hasResponse() || 404 != $e->getResponse()->getStatusCode()) {
throw $e;
}
$this->universeDomain = self::DEFAULT_UNIVERSE_DOMAIN;
}
// We expect in some cases the metadata server will return an empty string for the universe
// domain. In this case, the auth library MUST return the default universe domain.
if ('' === $this->universeDomain) {
$this->universeDomain = self::DEFAULT_UNIVERSE_DOMAIN;
}
return $this->universeDomain;
}
/**
* Fetch the value of a GCE metadata server URI.
*
* @param callable $httpHandler An HTTP Handler to deliver PSR7 requests.
* @param string $uri The metadata URI.
* @param array<mixed> $headers [optional] If present, add these headers to the token
* endpoint request.
*
* @return string
*/
private function getFromMetadata(callable $httpHandler, $uri, array $headers = [])
{
$resp = $httpHandler(
new Request(
'GET',
$uri,
[self::FLAVOR_HEADER => 'Google'] + $headers
)
);
return (string) $resp->getBody();
}
/**
* Get the quota project used for this API request
*
* @return string|null
*/
public function getQuotaProject()
{
return $this->quotaProject;
}
/**
* Set whether or not we've already checked the GCE environment.
*
* @param bool $isOnGce
*
* @return void
*/
public function setIsOnGce($isOnGce)
{
// Implicitly set hasCheckedGce to true
$this->hasCheckedOnGce = true;
// Set isOnGce
$this->isOnGce = $isOnGce;
}
protected function getCredType(): string
{
return self::CRED_TYPE;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
/**
* Authenticates requests using IAM credentials.
*/
class IAMCredentials
{
const SELECTOR_KEY = 'x-goog-iam-authority-selector';
const TOKEN_KEY = 'x-goog-iam-authorization-token';
/**
* @var string
*/
private $selector;
/**
* @var string
*/
private $token;
/**
* @param string $selector the IAM selector
* @param string $token the IAM token
*/
public function __construct($selector, $token)
{
if (!is_string($selector)) {
throw new \InvalidArgumentException(
'selector must be a string'
);
}
if (!is_string($token)) {
throw new \InvalidArgumentException(
'token must be a string'
);
}
$this->selector = $selector;
$this->token = $token;
}
/**
* export a callback function which updates runtime metadata.
*
* @return callable updateMetadata function
*/
public function getUpdateMetadataFunc()
{
return [$this, 'updateMetadata'];
}
/**
* Updates metadata with the appropriate header metadata.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $unusedAuthUri optional auth uri
* @param callable $httpHandler callback which delivers psr7 request
* Note: this param is unused here, only included here for
* consistency with other credentials class
*
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$unusedAuthUri = null,
callable $httpHandler = null
) {
$metadata_copy = $metadata;
$metadata_copy[self::SELECTOR_KEY] = $this->selector;
$metadata_copy[self::TOKEN_KEY] = $this->token;
return $metadata_copy;
}
}

View File

@@ -0,0 +1,156 @@
<?php
/*
* Copyright 2022 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Google\Auth\CredentialsLoader;
use Google\Auth\IamSignerTrait;
use Google\Auth\SignBlobInterface;
class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements SignBlobInterface
{
use IamSignerTrait;
private const CRED_TYPE = 'imp';
/**
* @var string
*/
protected $impersonatedServiceAccountName;
/**
* @var UserRefreshCredentials
*/
protected $sourceCredentials;
/**
* Instantiate an instance of ImpersonatedServiceAccountCredentials from a credentials file that
* has be created with the --impersonated-service-account flag.
*
* @param string|string[] $scope The scope of the access request, expressed either as an
* array or as a space-delimited string.
* @param string|array<mixed> $jsonKey JSON credential file path or JSON credentials
* as an associative array.
*/
public function __construct(
$scope,
$jsonKey
) {
if (is_string($jsonKey)) {
if (!file_exists($jsonKey)) {
throw new \InvalidArgumentException('file does not exist');
}
$json = file_get_contents($jsonKey);
if (!$jsonKey = json_decode((string) $json, true)) {
throw new \LogicException('invalid json for auth config');
}
}
if (!array_key_exists('service_account_impersonation_url', $jsonKey)) {
throw new \LogicException(
'json key is missing the service_account_impersonation_url field'
);
}
if (!array_key_exists('source_credentials', $jsonKey)) {
throw new \LogicException('json key is missing the source_credentials field');
}
$this->impersonatedServiceAccountName = $this->getImpersonatedServiceAccountNameFromUrl(
$jsonKey['service_account_impersonation_url']
);
$this->sourceCredentials = new UserRefreshCredentials(
$scope,
$jsonKey['source_credentials']
);
}
/**
* Helper function for extracting the Server Account Name from the URL saved in the account
* credentials file.
*
* @param $serviceAccountImpersonationUrl string URL from "service_account_impersonation_url"
* @return string Service account email or ID.
*/
private function getImpersonatedServiceAccountNameFromUrl(
string $serviceAccountImpersonationUrl
): string {
$fields = explode('/', $serviceAccountImpersonationUrl);
$lastField = end($fields);
$splitter = explode(':', $lastField);
return $splitter[0];
}
/**
* Get the client name from the keyfile
*
* In this implementation, it will return the issuers email from the oauth token.
*
* @param callable|null $unusedHttpHandler not used by this credentials type.
* @return string Token issuer email
*/
public function getClientName(callable $unusedHttpHandler = null)
{
return $this->impersonatedServiceAccountName;
}
/**
* @param callable $httpHandler
*
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type int $expires_in
* @type string $scope
* @type string $token_type
* @type string $id_token
* }
*/
public function fetchAuthToken(callable $httpHandler = null)
{
// We don't support id token endpoint requests as of now for Impersonated Cred
return $this->sourceCredentials->fetchAuthToken(
$httpHandler,
$this->applyTokenEndpointMetrics([], 'at')
);
}
/**
* Returns the Cache Key for the credentials
* The cache key is the same as the UserRefreshCredentials class
*
* @return string
*/
public function getCacheKey()
{
return $this->sourceCredentials->getCacheKey();
}
/**
* @return array<mixed>
*/
public function getLastReceivedToken()
{
return $this->sourceCredentials->getLastReceivedToken();
}
protected function getCredType(): string
{
return self::CRED_TYPE;
}
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* Copyright 2018 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Google\Auth\FetchAuthTokenInterface;
/**
* Provides a set of credentials that will always return an empty access token.
* This is useful for APIs which do not require authentication, for local
* service emulators, and for testing.
*/
class InsecureCredentials implements FetchAuthTokenInterface
{
/**
* @var array{access_token:string}
*/
private $token = [
'access_token' => ''
];
/**
* Fetches the auth token. In this case it returns an empty string.
*
* @param callable $httpHandler
* @return array{access_token:string} A set of auth related metadata
*/
public function fetchAuthToken(callable $httpHandler = null)
{
return $this->token;
}
/**
* Returns the cache key. In this case it returns a null value, disabling
* caching.
*
* @return string|null
*/
public function getCacheKey()
{
return null;
}
/**
* Fetches the last received token. In this case, it returns the same empty string
* auth token.
*
* @return array{access_token:string}
*/
public function getLastReceivedToken()
{
return $this->token;
}
}

View File

@@ -0,0 +1,419 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Google\Auth\CredentialsLoader;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\OAuth2;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\ServiceAccountSignerTrait;
use Google\Auth\SignBlobInterface;
use InvalidArgumentException;
/**
* ServiceAccountCredentials supports authorization using a Google service
* account.
*
* (cf https://developers.google.com/accounts/docs/OAuth2ServiceAccount)
*
* It's initialized using the json key file that's downloadable from developer
* console, which should contain a private_key and client_email fields that it
* uses.
*
* Use it with AuthTokenMiddleware to authorize http requests:
*
* use Google\Auth\Credentials\ServiceAccountCredentials;
* use Google\Auth\Middleware\AuthTokenMiddleware;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $sa = new ServiceAccountCredentials(
* 'https://www.googleapis.com/auth/taskqueue',
* '/path/to/your/json/key_file.json'
* );
* $middleware = new AuthTokenMiddleware($sa);
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
* 'auth' => 'google_auth' // authorize all requests
* ]);
*
* $res = $client->get('myproject/taskqueues/myqueue');
*/
class ServiceAccountCredentials extends CredentialsLoader implements
GetQuotaProjectInterface,
SignBlobInterface,
ProjectIdProviderInterface
{
use ServiceAccountSignerTrait;
/**
* Used in observability metric headers
*
* @var string
*/
private const CRED_TYPE = 'sa';
/**
* The OAuth2 instance used to conduct authorization.
*
* @var OAuth2
*/
protected $auth;
/**
* The quota project associated with the JSON credentials
*
* @var string
*/
protected $quotaProject;
/**
* @var string|null
*/
protected $projectId;
/**
* @var array<mixed>|null
*/
private $lastReceivedJwtAccessToken;
/**
* @var bool
*/
private $useJwtAccessWithScope = false;
/**
* @var ServiceAccountJwtAccessCredentials|null
*/
private $jwtAccessCredentials;
/**
* @var string
*/
private string $universeDomain;
/**
* Create a new ServiceAccountCredentials.
*
* @param string|string[]|null $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
* @param string|array<mixed> $jsonKey JSON credential file path or JSON credentials
* as an associative array
* @param string $sub an email address account to impersonate, in situations when
* the service account has been delegated domain wide access.
* @param string $targetAudience The audience for the ID token.
*/
public function __construct(
$scope,
$jsonKey,
$sub = null,
$targetAudience = null
) {
if (is_string($jsonKey)) {
if (!file_exists($jsonKey)) {
throw new \InvalidArgumentException('file does not exist');
}
$jsonKeyStream = file_get_contents($jsonKey);
if (!$jsonKey = json_decode((string) $jsonKeyStream, true)) {
throw new \LogicException('invalid json for auth config');
}
}
if (!array_key_exists('client_email', $jsonKey)) {
throw new \InvalidArgumentException(
'json key is missing the client_email field'
);
}
if (!array_key_exists('private_key', $jsonKey)) {
throw new \InvalidArgumentException(
'json key is missing the private_key field'
);
}
if (array_key_exists('quota_project_id', $jsonKey)) {
$this->quotaProject = (string) $jsonKey['quota_project_id'];
}
if ($scope && $targetAudience) {
throw new InvalidArgumentException(
'Scope and targetAudience cannot both be supplied'
);
}
$additionalClaims = [];
if ($targetAudience) {
$additionalClaims = ['target_audience' => $targetAudience];
}
$this->auth = new OAuth2([
'audience' => self::TOKEN_CREDENTIAL_URI,
'issuer' => $jsonKey['client_email'],
'scope' => $scope,
'signingAlgorithm' => 'RS256',
'signingKey' => $jsonKey['private_key'],
'sub' => $sub,
'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI,
'additionalClaims' => $additionalClaims,
]);
$this->projectId = $jsonKey['project_id'] ?? null;
$this->universeDomain = $jsonKey['universe_domain'] ?? self::DEFAULT_UNIVERSE_DOMAIN;
}
/**
* When called, the ServiceAccountCredentials will use an instance of
* ServiceAccountJwtAccessCredentials to fetch (self-sign) an access token
* even when only scopes are supplied. Otherwise,
* ServiceAccountJwtAccessCredentials is only called when no scopes and an
* authUrl (audience) is suppled.
*
* @return void
*/
public function useJwtAccessWithScope()
{
$this->useJwtAccessWithScope = true;
}
/**
* @param callable $httpHandler
*
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type int $expires_in
* @type string $token_type
* }
*/
public function fetchAuthToken(callable $httpHandler = null)
{
if ($this->useSelfSignedJwt()) {
$jwtCreds = $this->createJwtAccessCredentials();
$accessToken = $jwtCreds->fetchAuthToken($httpHandler);
if ($lastReceivedToken = $jwtCreds->getLastReceivedToken()) {
// Keep self-signed JWTs in memory as the last received token
$this->lastReceivedJwtAccessToken = $lastReceivedToken;
}
return $accessToken;
}
$authRequestType = empty($this->auth->getAdditionalClaims()['target_audience'])
? 'at' : 'it';
return $this->auth->fetchAuthToken($httpHandler, $this->applyTokenEndpointMetrics([], $authRequestType));
}
/**
* Return the Cache Key for the credentials.
* For the cache key format is one of the following:
* ClientEmail.Scope[.Sub]
* ClientEmail.Audience[.Sub]
*
* @return string
*/
public function getCacheKey()
{
$scopeOrAudience = $this->auth->getScope();
if (!$scopeOrAudience) {
$scopeOrAudience = $this->auth->getAudience();
}
$key = $this->auth->getIssuer() . '.' . $scopeOrAudience;
if ($sub = $this->auth->getSub()) {
$key .= '.' . $sub;
}
return $key;
}
/**
* @return array<mixed>
*/
public function getLastReceivedToken()
{
// If self-signed JWTs are being used, fetch the last received token
// from memory. Else, fetch it from OAuth2
return $this->useSelfSignedJwt()
? $this->lastReceivedJwtAccessToken
: $this->auth->getLastReceivedToken();
}
/**
* Get the project ID from the service account keyfile.
*
* Returns null if the project ID does not exist in the keyfile.
*
* @param callable $httpHandler Not used by this credentials type.
* @return string|null
*/
public function getProjectId(callable $httpHandler = null)
{
return $this->projectId;
}
/**
* Updates metadata with the authorization token.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable $httpHandler callback which delivers psr7 request
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$authUri = null,
callable $httpHandler = null
) {
// scope exists. use oauth implementation
if (!$this->useSelfSignedJwt()) {
return parent::updateMetadata($metadata, $authUri, $httpHandler);
}
$jwtCreds = $this->createJwtAccessCredentials();
if ($this->auth->getScope()) {
// Prefer user-provided "scope" to "audience"
$updatedMetadata = $jwtCreds->updateMetadata($metadata, null, $httpHandler);
} else {
$updatedMetadata = $jwtCreds->updateMetadata($metadata, $authUri, $httpHandler);
}
if ($lastReceivedToken = $jwtCreds->getLastReceivedToken()) {
// Keep self-signed JWTs in memory as the last received token
$this->lastReceivedJwtAccessToken = $lastReceivedToken;
}
return $updatedMetadata;
}
/**
* @return ServiceAccountJwtAccessCredentials
*/
private function createJwtAccessCredentials()
{
if (!$this->jwtAccessCredentials) {
// Create credentials for self-signing a JWT (JwtAccess)
$credJson = [
'private_key' => $this->auth->getSigningKey(),
'client_email' => $this->auth->getIssuer(),
];
$this->jwtAccessCredentials = new ServiceAccountJwtAccessCredentials(
$credJson,
$this->auth->getScope()
);
}
return $this->jwtAccessCredentials;
}
/**
* @param string $sub an email address account to impersonate, in situations when
* the service account has been delegated domain wide access.
* @return void
*/
public function setSub($sub)
{
$this->auth->setSub($sub);
}
/**
* Get the client name from the keyfile.
*
* In this case, it returns the keyfile's client_email key.
*
* @param callable $httpHandler Not used by this credentials type.
* @return string
*/
public function getClientName(callable $httpHandler = null)
{
return $this->auth->getIssuer();
}
/**
* Get the private key from the keyfile.
*
* In this case, it returns the keyfile's private_key key, needed for JWT signing.
*
* @return string
*/
public function getPrivateKey()
{
return $this->auth->getSigningKey();
}
/**
* Get the quota project used for this API request
*
* @return string|null
*/
public function getQuotaProject()
{
return $this->quotaProject;
}
/**
* Get the universe domain configured in the JSON credential.
*
* @return string
*/
public function getUniverseDomain(): string
{
return $this->universeDomain;
}
protected function getCredType(): string
{
return self::CRED_TYPE;
}
/**
* @return bool
*/
private function useSelfSignedJwt()
{
// When a sub is supplied, the user is using domain-wide delegation, which not available
// with self-signed JWTs
if (null !== $this->auth->getSub()) {
// If we are outside the GDU, we can't use domain-wide delegation
if ($this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) {
throw new \LogicException(sprintf(
'Service Account subject is configured for the credential. Domain-wide ' .
'delegation is not supported in universes other than %s.',
self::DEFAULT_UNIVERSE_DOMAIN
));
}
return false;
}
// If claims are set, this call is for "id_tokens"
if ($this->auth->getAdditionalClaims()) {
return false;
}
// When true, ServiceAccountCredentials will always use JwtAccess for access tokens
if ($this->useJwtAccessWithScope) {
return true;
}
// If the universe domain is outside the GDU, use JwtAccess for access tokens
if ($this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) {
return true;
}
return is_null($this->auth->getScope());
}
}

View File

@@ -0,0 +1,246 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Google\Auth\CredentialsLoader;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\OAuth2;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\ServiceAccountSignerTrait;
use Google\Auth\SignBlobInterface;
/**
* Authenticates requests using Google's Service Account credentials via
* JWT Access.
*
* This class allows authorizing requests for service accounts directly
* from credentials from a json key file downloaded from the developer
* console (via 'Generate new Json Key'). It is not part of any OAuth2
* flow, rather it creates a JWT and sends that as a credential.
*/
class ServiceAccountJwtAccessCredentials extends CredentialsLoader implements
GetQuotaProjectInterface,
SignBlobInterface,
ProjectIdProviderInterface
{
use ServiceAccountSignerTrait;
/**
* Used in observability metric headers
*
* @var string
*/
private const CRED_TYPE = 'jwt';
/**
* The OAuth2 instance used to conduct authorization.
*
* @var OAuth2
*/
protected $auth;
/**
* The quota project associated with the JSON credentials
*
* @var string
*/
protected $quotaProject;
/**
* @var string
*/
public $projectId;
/**
* Create a new ServiceAccountJwtAccessCredentials.
*
* @param string|array<mixed> $jsonKey JSON credential file path or JSON credentials
* as an associative array
* @param string|string[] $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
*/
public function __construct($jsonKey, $scope = null)
{
if (is_string($jsonKey)) {
if (!file_exists($jsonKey)) {
throw new \InvalidArgumentException('file does not exist');
}
$jsonKeyStream = file_get_contents($jsonKey);
if (!$jsonKey = json_decode((string) $jsonKeyStream, true)) {
throw new \LogicException('invalid json for auth config');
}
}
if (!array_key_exists('client_email', $jsonKey)) {
throw new \InvalidArgumentException(
'json key is missing the client_email field'
);
}
if (!array_key_exists('private_key', $jsonKey)) {
throw new \InvalidArgumentException(
'json key is missing the private_key field'
);
}
if (array_key_exists('quota_project_id', $jsonKey)) {
$this->quotaProject = (string) $jsonKey['quota_project_id'];
}
$this->auth = new OAuth2([
'issuer' => $jsonKey['client_email'],
'sub' => $jsonKey['client_email'],
'signingAlgorithm' => 'RS256',
'signingKey' => $jsonKey['private_key'],
'scope' => $scope,
]);
$this->projectId = $jsonKey['project_id'] ?? null;
}
/**
* Updates metadata with the authorization token.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable $httpHandler callback which delivers psr7 request
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$authUri = null,
callable $httpHandler = null
) {
$scope = $this->auth->getScope();
if (empty($authUri) && empty($scope)) {
return $metadata;
}
$this->auth->setAudience($authUri);
return parent::updateMetadata($metadata, $authUri, $httpHandler);
}
/**
* Implements FetchAuthTokenInterface#fetchAuthToken.
*
* @param callable $httpHandler
*
* @return null|array{access_token:string} A set of auth related metadata
*/
public function fetchAuthToken(callable $httpHandler = null)
{
$audience = $this->auth->getAudience();
$scope = $this->auth->getScope();
if (empty($audience) && empty($scope)) {
return null;
}
if (!empty($audience) && !empty($scope)) {
throw new \UnexpectedValueException(
'Cannot sign both audience and scope in JwtAccess'
);
}
$access_token = $this->auth->toJwt();
// Set the self-signed access token in OAuth2 for getLastReceivedToken
$this->auth->setAccessToken($access_token);
return [
'access_token' => $access_token,
'expires_in' => $this->auth->getExpiry(),
'token_type' => 'Bearer'
];
}
/**
* Return the cache key for the credentials.
* The format for the Cache Key one of the following:
* ClientEmail.Scope
* ClientEmail.Audience
*
* @return string
*/
public function getCacheKey()
{
$scopeOrAudience = $this->auth->getScope();
if (!$scopeOrAudience) {
$scopeOrAudience = $this->auth->getAudience();
}
return $this->auth->getIssuer() . '.' . $scopeOrAudience;
}
/**
* @return array<mixed>
*/
public function getLastReceivedToken()
{
return $this->auth->getLastReceivedToken();
}
/**
* Get the project ID from the service account keyfile.
*
* Returns null if the project ID does not exist in the keyfile.
*
* @param callable $httpHandler Not used by this credentials type.
* @return string|null
*/
public function getProjectId(callable $httpHandler = null)
{
return $this->projectId;
}
/**
* Get the client name from the keyfile.
*
* In this case, it returns the keyfile's client_email key.
*
* @param callable $httpHandler Not used by this credentials type.
* @return string
*/
public function getClientName(callable $httpHandler = null)
{
return $this->auth->getIssuer();
}
/**
* Get the private key from the keyfile.
*
* In this case, it returns the keyfile's private_key key, needed for JWT signing.
*
* @return string
*/
public function getPrivateKey()
{
return $this->auth->getSigningKey();
}
/**
* Get the quota project used for this API request
*
* @return string|null
*/
public function getQuotaProject()
{
return $this->quotaProject;
}
protected function getCredType(): string
{
return self::CRED_TYPE;
}
}

View File

@@ -0,0 +1,182 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Google\Auth\CredentialsLoader;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\OAuth2;
/**
* Authenticates requests using User Refresh credentials.
*
* This class allows authorizing requests from user refresh tokens.
*
* This the end of the result of a 3LO flow. E.g, the end result of
* 'gcloud auth login' saves a file with these contents in well known
* location
*
* @see [Application Default Credentials](http://goo.gl/mkAHpZ)
*/
class UserRefreshCredentials extends CredentialsLoader implements GetQuotaProjectInterface
{
/**
* Used in observability metric headers
*
* @var string
*/
private const CRED_TYPE = 'u';
/**
* The OAuth2 instance used to conduct authorization.
*
* @var OAuth2
*/
protected $auth;
/**
* The quota project associated with the JSON credentials
*
* @var string
*/
protected $quotaProject;
/**
* Create a new UserRefreshCredentials.
*
* @param string|string[] $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
* @param string|array<mixed> $jsonKey JSON credential file path or JSON credentials
* as an associative array
*/
public function __construct(
$scope,
$jsonKey
) {
if (is_string($jsonKey)) {
if (!file_exists($jsonKey)) {
throw new \InvalidArgumentException('file does not exist');
}
$json = file_get_contents($jsonKey);
if (!$jsonKey = json_decode((string) $json, true)) {
throw new \LogicException('invalid json for auth config');
}
}
if (!array_key_exists('client_id', $jsonKey)) {
throw new \InvalidArgumentException(
'json key is missing the client_id field'
);
}
if (!array_key_exists('client_secret', $jsonKey)) {
throw new \InvalidArgumentException(
'json key is missing the client_secret field'
);
}
if (!array_key_exists('refresh_token', $jsonKey)) {
throw new \InvalidArgumentException(
'json key is missing the refresh_token field'
);
}
$this->auth = new OAuth2([
'clientId' => $jsonKey['client_id'],
'clientSecret' => $jsonKey['client_secret'],
'refresh_token' => $jsonKey['refresh_token'],
'scope' => $scope,
'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI,
]);
if (array_key_exists('quota_project_id', $jsonKey)) {
$this->quotaProject = (string) $jsonKey['quota_project_id'];
}
}
/**
* @param callable $httpHandler
* @param array<mixed> $metricsHeader [optional] Metrics headers to be inserted
* into the token endpoint request present.
* This could be passed from ImersonatedServiceAccountCredentials as it uses
* UserRefreshCredentials as source credentials.
*
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type int $expires_in
* @type string $scope
* @type string $token_type
* @type string $id_token
* }
*/
public function fetchAuthToken(callable $httpHandler = null, array $metricsHeader = [])
{
// We don't support id token endpoint requests as of now for User Cred
return $this->auth->fetchAuthToken(
$httpHandler,
$this->applyTokenEndpointMetrics($metricsHeader, 'at')
);
}
/**
* Return the Cache Key for the credentials.
* The format for the Cache key is one of the following:
* ClientId.Scope
* ClientId.Audience
*
* @return string
*/
public function getCacheKey()
{
$scopeOrAudience = $this->auth->getScope();
if (!$scopeOrAudience) {
$scopeOrAudience = $this->auth->getAudience();
}
return $this->auth->getClientId() . '.' . $scopeOrAudience;
}
/**
* @return array<mixed>
*/
public function getLastReceivedToken()
{
return $this->auth->getLastReceivedToken();
}
/**
* Get the quota project used for this API request
*
* @return string|null
*/
public function getQuotaProject()
{
return $this->quotaProject;
}
/**
* Get the granted scopes (if they exist) for the last fetched token.
*
* @return string|null
*/
public function getGrantedScope()
{
return $this->auth->getGrantedScope();
}
protected function getCredType(): string
{
return self::CRED_TYPE;
}
}

View File

@@ -0,0 +1,288 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use Google\Auth\Credentials\ExternalAccountCredentials;
use Google\Auth\Credentials\ImpersonatedServiceAccountCredentials;
use Google\Auth\Credentials\InsecureCredentials;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\Credentials\UserRefreshCredentials;
use RuntimeException;
use UnexpectedValueException;
/**
* CredentialsLoader contains the behaviour used to locate and find default
* credentials files on the file system.
*/
abstract class CredentialsLoader implements
GetUniverseDomainInterface,
FetchAuthTokenInterface,
UpdateMetadataInterface
{
use UpdateMetadataTrait;
const TOKEN_CREDENTIAL_URI = 'https://oauth2.googleapis.com/token';
const ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS';
const QUOTA_PROJECT_ENV_VAR = 'GOOGLE_CLOUD_QUOTA_PROJECT';
const WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json';
const NON_WINDOWS_WELL_KNOWN_PATH_BASE = '.config';
const MTLS_WELL_KNOWN_PATH = '.secureConnect/context_aware_metadata.json';
const MTLS_CERT_ENV_VAR = 'GOOGLE_API_USE_CLIENT_CERTIFICATE';
/**
* @param string $cause
* @return string
*/
private static function unableToReadEnv($cause)
{
$msg = 'Unable to read the credential file specified by ';
$msg .= ' GOOGLE_APPLICATION_CREDENTIALS: ';
$msg .= $cause;
return $msg;
}
/**
* @return bool
*/
private static function isOnWindows()
{
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
}
/**
* Load a JSON key from the path specified in the environment.
*
* Load a JSON key from the path specified in the environment
* variable GOOGLE_APPLICATION_CREDENTIALS. Return null if
* GOOGLE_APPLICATION_CREDENTIALS is not specified.
*
* @return array<mixed>|null JSON key | null
*/
public static function fromEnv()
{
$path = getenv(self::ENV_VAR);
if (empty($path)) {
return null;
}
if (!file_exists($path)) {
$cause = 'file ' . $path . ' does not exist';
throw new \DomainException(self::unableToReadEnv($cause));
}
$jsonKey = file_get_contents($path);
return json_decode((string) $jsonKey, true);
}
/**
* Load a JSON key from a well known path.
*
* The well known path is OS dependent:
*
* * windows: %APPDATA%/gcloud/application_default_credentials.json
* * others: $HOME/.config/gcloud/application_default_credentials.json
*
* If the file does not exist, this returns null.
*
* @return array<mixed>|null JSON key | null
*/
public static function fromWellKnownFile()
{
$rootEnv = self::isOnWindows() ? 'APPDATA' : 'HOME';
$path = [getenv($rootEnv)];
if (!self::isOnWindows()) {
$path[] = self::NON_WINDOWS_WELL_KNOWN_PATH_BASE;
}
$path[] = self::WELL_KNOWN_PATH;
$path = implode(DIRECTORY_SEPARATOR, $path);
if (!file_exists($path)) {
return null;
}
$jsonKey = file_get_contents($path);
return json_decode((string) $jsonKey, true);
}
/**
* Create a new Credentials instance.
*
* @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 the JSON credentials.
* @param string|string[] $defaultScope The default scope to use if no
* user-defined scopes exist, expressed either as an Array or as a
* space-delimited string.
*
* @return ServiceAccountCredentials|UserRefreshCredentials|ImpersonatedServiceAccountCredentials|ExternalAccountCredentials
*/
public static function makeCredentials(
$scope,
array $jsonKey,
$defaultScope = null
) {
if (!array_key_exists('type', $jsonKey)) {
throw new \InvalidArgumentException('json key is missing the type field');
}
if ($jsonKey['type'] == 'service_account') {
// Do not pass $defaultScope to ServiceAccountCredentials
return new ServiceAccountCredentials($scope, $jsonKey);
}
if ($jsonKey['type'] == 'authorized_user') {
$anyScope = $scope ?: $defaultScope;
return new UserRefreshCredentials($anyScope, $jsonKey);
}
if ($jsonKey['type'] == 'impersonated_service_account') {
$anyScope = $scope ?: $defaultScope;
return new ImpersonatedServiceAccountCredentials($anyScope, $jsonKey);
}
if ($jsonKey['type'] == 'external_account') {
$anyScope = $scope ?: $defaultScope;
return new ExternalAccountCredentials($anyScope, $jsonKey);
}
throw new \InvalidArgumentException('invalid value in the type field');
}
/**
* Create an authorized HTTP Client from an instance of FetchAuthTokenInterface.
*
* @param FetchAuthTokenInterface $fetcher is used to fetch the auth token
* @param array<mixed> $httpClientOptions (optional) Array of request options to apply.
* @param callable $httpHandler (optional) http client to fetch the token.
* @param callable $tokenCallback (optional) function to be called when a new token is fetched.
* @return \GuzzleHttp\Client
*/
public static function makeHttpClient(
FetchAuthTokenInterface $fetcher,
array $httpClientOptions = [],
callable $httpHandler = null,
callable $tokenCallback = null
) {
$middleware = new Middleware\AuthTokenMiddleware(
$fetcher,
$httpHandler,
$tokenCallback
);
$stack = \GuzzleHttp\HandlerStack::create();
$stack->push($middleware);
return new \GuzzleHttp\Client([
'handler' => $stack,
'auth' => 'google_auth',
] + $httpClientOptions);
}
/**
* Create a new instance of InsecureCredentials.
*
* @return InsecureCredentials
*/
public static function makeInsecureCredentials()
{
return new InsecureCredentials();
}
/**
* Fetch a quota project from the environment variable
* GOOGLE_CLOUD_QUOTA_PROJECT. Return null if
* GOOGLE_CLOUD_QUOTA_PROJECT is not specified.
*
* @return string|null
*/
public static function quotaProjectFromEnv()
{
return getenv(self::QUOTA_PROJECT_ENV_VAR) ?: null;
}
/**
* Gets a callable which returns the default device certification.
*
* @throws UnexpectedValueException
* @return callable|null
*/
public static function getDefaultClientCertSource()
{
if (!$clientCertSourceJson = self::loadDefaultClientCertSourceFile()) {
return null;
}
$clientCertSourceCmd = $clientCertSourceJson['cert_provider_command'];
return function () use ($clientCertSourceCmd) {
$cmd = array_map('escapeshellarg', $clientCertSourceCmd);
exec(implode(' ', $cmd), $output, $returnVar);
if (0 === $returnVar) {
return implode(PHP_EOL, $output);
}
throw new RuntimeException(
'"cert_provider_command" failed with a nonzero exit code'
);
};
}
/**
* Determines whether or not the default device certificate should be loaded.
*
* @return bool
*/
public static function shouldLoadClientCertSource()
{
return filter_var(getenv(self::MTLS_CERT_ENV_VAR), FILTER_VALIDATE_BOOLEAN);
}
/**
* @return array{cert_provider_command:string[]}|null
*/
private static function loadDefaultClientCertSourceFile()
{
$rootEnv = self::isOnWindows() ? 'APPDATA' : 'HOME';
$path = sprintf('%s/%s', getenv($rootEnv), self::MTLS_WELL_KNOWN_PATH);
if (!file_exists($path)) {
return null;
}
$jsonKey = file_get_contents($path);
$clientCertSourceJson = json_decode((string) $jsonKey, true);
if (!$clientCertSourceJson) {
throw new UnexpectedValueException('Invalid client cert source JSON');
}
if (!isset($clientCertSourceJson['cert_provider_command'])) {
throw new UnexpectedValueException(
'cert source requires "cert_provider_command"'
);
}
if (!is_array($clientCertSourceJson['cert_provider_command'])) {
throw new UnexpectedValueException(
'cert source expects "cert_provider_command" to be an array'
);
}
return $clientCertSourceJson;
}
/**
* Get the universe domain from the credential. Defaults to "googleapis.com"
* for all credential types which do not support universe domain.
*
* @return string
*/
public function getUniverseDomain(): string
{
return self::DEFAULT_UNIVERSE_DOMAIN;
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* Copyright 2024 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\ExecutableHandler;
use RuntimeException;
use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Process;
class ExecutableHandler
{
private const DEFAULT_EXECUTABLE_TIMEOUT_MILLIS = 30 * 1000;
private int $timeoutMs;
/** @var array<string|\Stringable> */
private array $env = [];
private ?string $output = null;
/**
* @param array<string|\Stringable> $env
*/
public function __construct(
array $env = [],
int $timeoutMs = self::DEFAULT_EXECUTABLE_TIMEOUT_MILLIS,
) {
if (!class_exists(Process::class)) {
throw new RuntimeException(sprintf(
'The "symfony/process" package is required to use %s.',
self::class
));
}
$this->env = $env;
$this->timeoutMs = $timeoutMs;
}
/**
* @param string $command
* @return int
*/
public function __invoke(string $command): int
{
$process = Process::fromShellCommandline(
$command,
null,
$this->env,
null,
($this->timeoutMs / 1000)
);
try {
$process->run();
} catch (ProcessTimedOutException $e) {
throw new ExecutableResponseError(
'The executable failed to finish within the timeout specified.',
'TIMEOUT_EXCEEDED'
);
}
$this->output = $process->getOutput() . $process->getErrorOutput();
return $process->getExitCode();
}
public function getOutput(): ?string
{
return $this->output;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Copyright 2024 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\ExecutableHandler;
use Error;
class ExecutableResponseError extends Error
{
public function __construct(string $message, string $executableErrorCode = 'INVALID_EXECUTABLE_RESPONSE')
{
parent::__construct(sprintf('Error code %s: %s', $executableErrorCode, $message));
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
interface ExternalAccountCredentialSourceInterface
{
public function fetchSubjectToken(callable $httpHandler = null): string;
public function getCacheKey(): ?string;
}

View File

@@ -0,0 +1,339 @@
<?php
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use Psr\Cache\CacheItemPoolInterface;
/**
* A class to implement caching for any object implementing
* FetchAuthTokenInterface
*/
class FetchAuthTokenCache implements
FetchAuthTokenInterface,
GetQuotaProjectInterface,
GetUniverseDomainInterface,
SignBlobInterface,
ProjectIdProviderInterface,
UpdateMetadataInterface
{
use CacheTrait;
/**
* @var FetchAuthTokenInterface
*/
private $fetcher;
/**
* @var int
*/
private $eagerRefreshThresholdSeconds = 10;
/**
* @param FetchAuthTokenInterface $fetcher A credentials fetcher
* @param array<mixed> $cacheConfig Configuration for the cache
* @param CacheItemPoolInterface $cache
*/
public function __construct(
FetchAuthTokenInterface $fetcher,
array $cacheConfig = null,
CacheItemPoolInterface $cache
) {
$this->fetcher = $fetcher;
$this->cache = $cache;
$this->cacheConfig = array_merge([
'lifetime' => 1500,
'prefix' => '',
'cacheUniverseDomain' => $fetcher instanceof Credentials\GCECredentials,
], (array) $cacheConfig);
}
/**
* @return FetchAuthTokenInterface
*/
public function getFetcher()
{
return $this->fetcher;
}
/**
* Implements FetchAuthTokenInterface#fetchAuthToken.
*
* Checks the cache for a valid auth token and fetches the auth tokens
* from the supplied fetcher.
*
* @param callable $httpHandler callback which delivers psr7 request
* @return array<mixed> the response
* @throws \Exception
*/
public function fetchAuthToken(callable $httpHandler = null)
{
if ($cached = $this->fetchAuthTokenFromCache()) {
return $cached;
}
$auth_token = $this->fetcher->fetchAuthToken($httpHandler);
$this->saveAuthTokenInCache($auth_token);
return $auth_token;
}
/**
* @return string
*/
public function getCacheKey()
{
return $this->getFullCacheKey($this->fetcher->getCacheKey());
}
/**
* @return array<mixed>|null
*/
public function getLastReceivedToken()
{
return $this->fetcher->getLastReceivedToken();
}
/**
* Get the client name from the fetcher.
*
* @param callable $httpHandler An HTTP handler to deliver PSR7 requests.
* @return string
*/
public function getClientName(callable $httpHandler = null)
{
if (!$this->fetcher instanceof SignBlobInterface) {
throw new \RuntimeException(
'Credentials fetcher does not implement ' .
'Google\Auth\SignBlobInterface'
);
}
return $this->fetcher->getClientName($httpHandler);
}
/**
* Sign a blob using the fetcher.
*
* @param string $stringToSign The string to sign.
* @param bool $forceOpenSsl Require use of OpenSSL for local signing. Does
* not apply to signing done using external services. **Defaults to**
* `false`.
* @return string The resulting signature.
* @throws \RuntimeException If the fetcher does not implement
* `Google\Auth\SignBlobInterface`.
*/
public function signBlob($stringToSign, $forceOpenSsl = false)
{
if (!$this->fetcher instanceof SignBlobInterface) {
throw new \RuntimeException(
'Credentials fetcher does not implement ' .
'Google\Auth\SignBlobInterface'
);
}
// 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);
}
return $this->fetcher->signBlob($stringToSign, $forceOpenSsl);
}
/**
* Get the quota project used for this API request from the credentials
* fetcher.
*
* @return string|null
*/
public function getQuotaProject()
{
if ($this->fetcher instanceof GetQuotaProjectInterface) {
return $this->fetcher->getQuotaProject();
}
return null;
}
/*
* Get the Project ID from the fetcher.
*
* @param callable $httpHandler Callback which delivers psr7 request
* @return string|null
* @throws \RuntimeException If the fetcher does not implement
* `Google\Auth\ProvidesProjectIdInterface`.
*/
public function getProjectId(callable $httpHandler = null)
{
if (!$this->fetcher instanceof ProjectIdProviderInterface) {
throw new \RuntimeException(
'Credentials fetcher does not implement ' .
'Google\Auth\ProvidesProjectIdInterface'
);
}
// 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);
}
/*
* Get the Universe Domain from the fetcher.
*
* @return string
*/
public function getUniverseDomain(): string
{
if ($this->fetcher instanceof GetUniverseDomainInterface) {
if ($this->cacheConfig['cacheUniverseDomain']) {
return $this->getCachedUniverseDomain($this->fetcher);
}
return $this->fetcher->getUniverseDomain();
}
return GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN;
}
/**
* Updates metadata with the authorization token.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable $httpHandler callback which delivers psr7 request
* @return array<mixed> updated metadata hashmap
* @throws \RuntimeException If the fetcher does not implement
* `Google\Auth\UpdateMetadataInterface`.
*/
public function updateMetadata(
$metadata,
$authUri = null,
callable $httpHandler = null
) {
if (!$this->fetcher instanceof UpdateMetadataInterface) {
throw new \RuntimeException(
'Credentials fetcher does not implement ' .
'Google\Auth\UpdateMetadataInterface'
);
}
$cached = $this->fetchAuthTokenFromCache($authUri);
if ($cached) {
// Set the access token in the `Authorization` metadata header so
// the downstream call to updateMetadata know they don't need to
// fetch another token.
if (isset($cached['access_token'])) {
$metadata[self::AUTH_METADATA_KEY] = [
'Bearer ' . $cached['access_token']
];
} elseif (isset($cached['id_token'])) {
$metadata[self::AUTH_METADATA_KEY] = [
'Bearer ' . $cached['id_token']
];
}
}
$newMetadata = $this->fetcher->updateMetadata(
$metadata,
$authUri,
$httpHandler
);
if (!$cached && $token = $this->fetcher->getLastReceivedToken()) {
$this->saveAuthTokenInCache($token, $authUri);
}
return $newMetadata;
}
/**
* @param string|null $authUri
* @return array<mixed>|null
*/
private function fetchAuthTokenFromCache($authUri = null)
{
// Use the cached value if its available.
//
// TODO: correct caching; update the call to setCachedValue to set the expiry
// to the value returned with the auth token.
//
// TODO: correct caching; enable the cache to be cleared.
// if $authUri is set, use it as the cache key
$cacheKey = $authUri
? $this->getFullCacheKey($authUri)
: $this->fetcher->getCacheKey();
$cached = $this->getCachedValue($cacheKey);
if (is_array($cached)) {
if (empty($cached['expires_at'])) {
// If there is no expiration data, assume token is not expired.
// (for JwtAccess and ID tokens)
return $cached;
}
if ((time() + $this->eagerRefreshThresholdSeconds) < $cached['expires_at']) {
// access token is not expired
return $cached;
}
}
return null;
}
/**
* @param array<mixed> $authToken
* @param string|null $authUri
* @return void
*/
private function saveAuthTokenInCache($authToken, $authUri = null)
{
if (isset($authToken['access_token']) ||
isset($authToken['id_token'])) {
// if $authUri is set, use it as the cache key
$cacheKey = $authUri
? $this->getFullCacheKey($authUri)
: $this->fetcher->getCacheKey();
$this->setCachedValue($cacheKey, $authToken);
}
}
private function getCachedUniverseDomain(GetUniverseDomainInterface $fetcher): string
{
$cacheKey = $this->getFullCacheKey($fetcher->getCacheKey() . 'universe_domain'); // @phpstan-ignore-line
if ($universeDomain = $this->getCachedValue($cacheKey)) {
return $universeDomain;
}
$universeDomain = $fetcher->getUniverseDomain();
$this->setCachedValue($cacheKey, $universeDomain);
return $universeDomain;
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
/**
* An interface implemented by objects that can fetch auth tokens.
*/
interface FetchAuthTokenInterface
{
/**
* Fetches the auth tokens based on the current state.
*
* @param callable $httpHandler callback which delivers psr7 request
* @return array<mixed> a hash of auth tokens
*/
public function fetchAuthToken(callable $httpHandler = null);
/**
* Obtains a key that can used to cache the results of #fetchAuthToken.
*
* If the value is empty, the auth token is not cached.
*
* @return string a key that may be used to cache the auth token.
*/
public function getCacheKey();
/**
* Returns an associative array with the token and
* expiration time.
*
* @return null|array<mixed> {
* The last received access token.
*
* @type string $access_token The access token string.
* @type int $expires_at The time the token expires as a UNIX timestamp.
* }
*/
public function getLastReceivedToken();
}

View File

@@ -0,0 +1,82 @@
<?php
/*
* Copyright 2020 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use Google\Auth\Credentials\GCECredentials;
use Psr\Cache\CacheItemPoolInterface;
/**
* A class to implement caching for calls to GCECredentials::onGce. This class
* is used automatically when you pass a `Psr\Cache\CacheItemPoolInterface`
* cache object to `ApplicationDefaultCredentials::getCredentials`.
*
* ```
* $sysvCache = new Google\Auth\SysvCacheItemPool();
* $creds = Google\Auth\ApplicationDefaultCredentials::getCredentials(
* $scope,
* null,
* null,
* $sysvCache
* );
* ```
*/
class GCECache
{
const GCE_CACHE_KEY = 'google_auth_on_gce_cache';
use CacheTrait;
/**
* @param array<mixed> $cacheConfig Configuration for the cache
* @param CacheItemPoolInterface $cache
*/
public function __construct(
array $cacheConfig = null,
CacheItemPoolInterface $cache = null
) {
$this->cache = $cache;
$this->cacheConfig = array_merge([
'lifetime' => 1500,
'prefix' => '',
], (array) $cacheConfig);
}
/**
* Caches the result of onGce so the metadata server is not called multiple
* times.
*
* @param callable $httpHandler callback which delivers psr7 request
* @return bool True if this a GCEInstance, false otherwise
*/
public function onGce(callable $httpHandler = null)
{
if (is_null($this->cache)) {
return GCECredentials::onGce($httpHandler);
}
$cacheKey = self::GCE_CACHE_KEY;
$onGce = $this->getCachedValue($cacheKey);
if (is_null($onGce)) {
$onGce = GCECredentials::onGce($httpHandler);
$this->setCachedValue($cacheKey, $onGce);
}
return $onGce;
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
/**
* An interface implemented by objects that can get quota projects.
*/
interface GetQuotaProjectInterface
{
const X_GOOG_USER_PROJECT_HEADER = 'X-Goog-User-Project';
/**
* Get the quota project used for this API request
*
* @return string|null
*/
public function getQuotaProject();
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
/**
* An interface implemented by objects that can get universe domain for Google Cloud APIs.
*/
interface GetUniverseDomainInterface
{
const DEFAULT_UNIVERSE_DOMAIN = 'googleapis.com';
/**
* Get the universe domain from the credential. This should always return
* a string, and default to "googleapis.com" if no universe domain is
* configured.
*
* @return string
*/
public function getUniverseDomain(): string;
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\HttpHandler;
use GuzzleHttp\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class Guzzle6HttpHandler
{
/**
* @var ClientInterface
*/
private $client;
/**
* @param ClientInterface $client
*/
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
/**
* Accepts a PSR-7 request and an array of options and returns a PSR-7 response.
*
* @param RequestInterface $request
* @param array<mixed> $options
* @return ResponseInterface
*/
public function __invoke(RequestInterface $request, array $options = [])
{
return $this->client->send($request, $options);
}
/**
* Accepts a PSR-7 request and an array of options and returns a PromiseInterface
*
* @param RequestInterface $request
* @param array<mixed> $options
*
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function async(RequestInterface $request, array $options = [])
{
return $this->client->sendAsync($request, $options);
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\HttpHandler;
class Guzzle7HttpHandler extends Guzzle6HttpHandler
{
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\HttpHandler;
use GuzzleHttp\ClientInterface;
/**
* Stores an HTTP Client in order to prevent multiple instantiations.
*/
class HttpClientCache
{
/**
* @var ClientInterface|null
*/
private static $httpClient;
/**
* Cache an HTTP Client for later calls.
*
* Passing null will unset the cached client.
*
* @param ClientInterface|null $client
* @return void
*/
public static function setHttpClient(ClientInterface $client = null)
{
self::$httpClient = $client;
}
/**
* Get the stored HTTP Client, or null.
*
* @return ClientInterface|null
*/
public static function getHttpClient()
{
return self::$httpClient;
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\HttpHandler;
use GuzzleHttp\BodySummarizer;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
class HttpHandlerFactory
{
/**
* Builds out a default http handler for the installed version of guzzle.
*
* @param ClientInterface $client
* @return Guzzle6HttpHandler|Guzzle7HttpHandler
* @throws \Exception
*/
public static function build(ClientInterface $client = null)
{
if (is_null($client)) {
$stack = null;
if (class_exists(BodySummarizer::class)) {
// double the # of characters before truncation by default
$bodySummarizer = new BodySummarizer(240);
$stack = HandlerStack::create();
$stack->remove('http_errors');
$stack->unshift(Middleware::httpErrors($bodySummarizer), 'http_errors');
}
$client = new Client(['handler' => $stack]);
}
$version = null;
if (defined('GuzzleHttp\ClientInterface::MAJOR_VERSION')) {
$version = ClientInterface::MAJOR_VERSION;
} elseif (defined('GuzzleHttp\ClientInterface::VERSION')) {
$version = (int) substr(ClientInterface::VERSION, 0, 1);
}
switch ($version) {
case 6:
return new Guzzle6HttpHandler($client);
case 7:
return new Guzzle7HttpHandler($client);
default:
throw new \Exception('Version not supported');
}
}
}

View File

@@ -0,0 +1,110 @@
<?php
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Utils;
/**
* Tools for using the IAM API.
*
* @see https://cloud.google.com/iam/docs IAM Documentation
*/
class Iam
{
/**
* @deprecated
*/
const IAM_API_ROOT = 'https://iamcredentials.googleapis.com/v1';
const SIGN_BLOB_PATH = '%s:signBlob?alt=json';
const SERVICE_ACCOUNT_NAME = 'projects/-/serviceAccounts/%s';
private const IAM_API_ROOT_TEMPLATE = 'https://iamcredentials.UNIVERSE_DOMAIN/v1';
/**
* @var callable
*/
private $httpHandler;
private string $universeDomain;
/**
* @param callable $httpHandler [optional] The HTTP Handler to send requests.
*/
public function __construct(
callable $httpHandler = null,
string $universeDomain = GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN
) {
$this->httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
$this->universeDomain = $universeDomain;
}
/**
* Sign a string using the IAM signBlob API.
*
* Note that signing using IAM requires your service account to have the
* `iam.serviceAccounts.signBlob` permission, part of the "Service Account
* Token Creator" IAM role.
*
* @param string $email The service account email.
* @param string $accessToken An access token from the service account.
* @param string $stringToSign The string to be signed.
* @param array<string> $delegates [optional] A list of service account emails to
* add to the delegate chain. If omitted, the value of `$email` will
* be used.
* @return string The signed string, base64-encoded.
*/
public function signBlob($email, $accessToken, $stringToSign, array $delegates = [])
{
$httpHandler = $this->httpHandler;
$name = sprintf(self::SERVICE_ACCOUNT_NAME, $email);
$apiRoot = str_replace('UNIVERSE_DOMAIN', $this->universeDomain, self::IAM_API_ROOT_TEMPLATE);
$uri = $apiRoot . '/' . sprintf(self::SIGN_BLOB_PATH, $name);
if ($delegates) {
foreach ($delegates as &$delegate) {
$delegate = sprintf(self::SERVICE_ACCOUNT_NAME, $delegate);
}
} else {
$delegates = [$name];
}
$body = [
'delegates' => $delegates,
'payload' => base64_encode($stringToSign),
];
$headers = [
'Authorization' => 'Bearer ' . $accessToken
];
$request = new Psr7\Request(
'POST',
$uri,
$headers,
Utils::streamFor(json_encode($body))
);
$res = $httpHandler($request);
$body = json_decode((string) $res->getBody(), true);
return $body['signedBlob'];
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* Copyright 2022 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use Exception;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
trait IamSignerTrait
{
/**
* @var Iam|null
*/
private $iam;
/**
* Sign a string using the default service account private key.
*
* This implementation uses IAM's signBlob API.
*
* @see https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/signBlob SignBlob
*
* @param string $stringToSign The string to sign.
* @param bool $forceOpenSsl [optional] Does not apply to this credentials
* type.
* @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
* @throws Exception
*/
public function signBlob($stringToSign, $forceOpenSsl = false, $accessToken = null)
{
$httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
// Providing a signer is useful for testing, but it's undocumented
// because it's not something a user would generally need to do.
$signer = $this->iam;
if (!$signer) {
$signer = $this instanceof GetUniverseDomainInterface
? new Iam($httpHandler, $this->getUniverseDomain())
: new Iam($httpHandler);
}
$email = $this->getClientName($httpHandler);
if (is_null($accessToken)) {
$previousToken = $this->getLastReceivedToken();
$accessToken = $previousToken
? $previousToken['access_token']
: $this->fetchAuthToken($httpHandler)['access_token'];
}
return $signer->signBlob($email, $accessToken, $stringToSign);
}
}

View File

@@ -0,0 +1,120 @@
<?php
/*
* Copyright 2024 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
/**
* Trait containing helper methods required for enabling
* observability metrics in the library.
*
* @internal
*/
trait MetricsTrait
{
/**
* @var string The version of the auth library php.
*/
private static $version;
/**
* @var string The header key for the observability metrics.
*/
protected static $metricMetadataKey = 'x-goog-api-client';
/**
* @param string $credType [Optional] The credential type.
* Empty value will not add any credential type to the header.
* Should be one of `'sa'`, `'jwt'`, `'imp'`, `'mds'`, `'u'`.
* @param string $authRequestType [Optional] The auth request type.
* Empty value will not add any auth request type to the header.
* Should be one of `'at'`, `'it'`, `'mds'`.
* @return string The header value for the observability metrics.
*/
protected static function getMetricsHeader(
$credType = '',
$authRequestType = ''
): string {
$value = sprintf(
'gl-php/%s auth/%s',
PHP_VERSION,
self::getVersion()
);
if (!empty($authRequestType)) {
$value .= ' auth-request-type/' . $authRequestType;
}
if (!empty($credType)) {
$value .= ' cred-type/' . $credType;
}
return $value;
}
/**
* @param array<mixed> $metadata The metadata to update and return.
* @return array<mixed> The updated metadata.
*/
protected function applyServiceApiUsageMetrics($metadata)
{
if ($credType = $this->getCredType()) {
// Add service api usage observability metrics info into metadata
// We expect upstream libries to have the metadata key populated already
$value = 'cred-type/' . $credType;
if (!isset($metadata[self::$metricMetadataKey])) {
// This case will happen only when someone invokes the updateMetadata
// method on the credentials fetcher themselves.
$metadata[self::$metricMetadataKey] = [$value];
} elseif (is_array($metadata[self::$metricMetadataKey])) {
$metadata[self::$metricMetadataKey][0] .= ' ' . $value;
} else {
$metadata[self::$metricMetadataKey] .= ' ' . $value;
}
}
return $metadata;
}
/**
* @param array<mixed> $metadata The metadata to update and return.
* @param string $authRequestType The auth request type. Possible values are
* `'at'`, `'it'`, `'mds'`.
* @return array<mixed> The updated metadata.
*/
protected function applyTokenEndpointMetrics($metadata, $authRequestType)
{
$metricsHeader = self::getMetricsHeader($this->getCredType(), $authRequestType);
if (!isset($metadata[self::$metricMetadataKey])) {
$metadata[self::$metricMetadataKey] = $metricsHeader;
}
return $metadata;
}
protected static function getVersion(): string
{
if (is_null(self::$version)) {
$versionFilePath = __DIR__ . '/../VERSION';
self::$version = trim((string) file_get_contents($versionFilePath));
}
return self::$version;
}
protected function getCredType(): string
{
return '';
}
}

View File

@@ -0,0 +1,162 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Middleware;
use Google\Auth\FetchAuthTokenCache;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\UpdateMetadataInterface;
use GuzzleHttp\Psr7\Utils;
use Psr\Http\Message\RequestInterface;
/**
* AuthTokenMiddleware is a Guzzle Middleware that adds an Authorization header
* provided by an object implementing FetchAuthTokenInterface.
*
* The FetchAuthTokenInterface#fetchAuthToken is used to obtain a hash; one of
* the values value in that hash is added as the authorization header.
*
* Requests will be accessed with the authorization header:
*
* 'authorization' 'Bearer <value of auth_token>'
*/
class AuthTokenMiddleware
{
/**
* @var callable
*/
private $httpHandler;
/**
* It must be an implementation of FetchAuthTokenInterface.
* It may also implement UpdateMetadataInterface allowing direct
* retrieval of auth related headers
* @var FetchAuthTokenInterface
*/
private $fetcher;
/**
* @var ?callable
*/
private $tokenCallback;
/**
* Creates a new AuthTokenMiddleware.
*
* @param FetchAuthTokenInterface $fetcher is used to fetch the auth token
* @param callable $httpHandler (optional) callback which delivers psr7 request
* @param callable $tokenCallback (optional) function to be called when a new token is fetched.
*/
public function __construct(
FetchAuthTokenInterface $fetcher,
callable $httpHandler = null,
callable $tokenCallback = null
) {
$this->fetcher = $fetcher;
$this->httpHandler = $httpHandler;
$this->tokenCallback = $tokenCallback;
}
/**
* Updates the request with an Authorization header when auth is 'google_auth'.
*
* use Google\Auth\Middleware\AuthTokenMiddleware;
* use Google\Auth\OAuth2;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $config = [..<oauth config param>.];
* $oauth2 = new OAuth2($config)
* $middleware = new AuthTokenMiddleware($oauth2);
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
* 'auth' => 'google_auth' // authorize all requests
* ]);
*
* $res = $client->get('myproject/taskqueues/myqueue');
*
* @param callable $handler
* @return \Closure
*/
public function __invoke(callable $handler)
{
return function (RequestInterface $request, array $options) use ($handler) {
// Requests using "auth"="google_auth" will be authorized.
if (!isset($options['auth']) || $options['auth'] !== 'google_auth') {
return $handler($request, $options);
}
$request = $this->addAuthHeaders($request);
if ($quotaProject = $this->getQuotaProject()) {
$request = $request->withHeader(
GetQuotaProjectInterface::X_GOOG_USER_PROJECT_HEADER,
$quotaProject
);
}
return $handler($request, $options);
};
}
/**
* Adds auth related headers to the request.
*
* @param RequestInterface $request
* @return RequestInterface
*/
private function addAuthHeaders(RequestInterface $request)
{
if (!$this->fetcher instanceof UpdateMetadataInterface ||
($this->fetcher instanceof FetchAuthTokenCache &&
!$this->fetcher->getFetcher() instanceof UpdateMetadataInterface)
) {
$token = $this->fetcher->fetchAuthToken();
$request = $request->withHeader(
'authorization', 'Bearer ' . ($token['access_token'] ?? $token['id_token'] ?? '')
);
} else {
$headers = $this->fetcher->updateMetadata($request->getHeaders(), null, $this->httpHandler);
$request = Utils::modifyRequest($request, ['set_headers' => $headers]);
}
if ($this->tokenCallback && ($token = $this->fetcher->getLastReceivedToken())) {
if (array_key_exists('access_token', $token)) {
call_user_func($this->tokenCallback, $this->fetcher->getCacheKey(), $token['access_token']);
}
}
return $request;
}
/**
* @return string|null
*/
private function getQuotaProject()
{
if ($this->fetcher instanceof GetQuotaProjectInterface) {
return $this->fetcher->getQuotaProject();
}
return null;
}
}

View File

@@ -0,0 +1,155 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Middleware;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\GetQuotaProjectInterface;
use Psr\Http\Message\RequestInterface;
/**
* ProxyAuthTokenMiddleware is a Guzzle Middleware that adds an Authorization header
* provided by an object implementing FetchAuthTokenInterface.
*
* The FetchAuthTokenInterface#fetchAuthToken is used to obtain a hash; one of
* the values value in that hash is added as the authorization header.
*
* Requests will be accessed with the authorization header:
*
* 'proxy-authorization' 'Bearer <value of auth_token>'
*/
class ProxyAuthTokenMiddleware
{
/**
* @var callable
*/
private $httpHandler;
/**
* @var FetchAuthTokenInterface
*/
private $fetcher;
/**
* @var ?callable
*/
private $tokenCallback;
/**
* Creates a new ProxyAuthTokenMiddleware.
*
* @param FetchAuthTokenInterface $fetcher is used to fetch the auth token
* @param callable $httpHandler (optional) callback which delivers psr7 request
* @param callable $tokenCallback (optional) function to be called when a new token is fetched.
*/
public function __construct(
FetchAuthTokenInterface $fetcher,
callable $httpHandler = null,
callable $tokenCallback = null
) {
$this->fetcher = $fetcher;
$this->httpHandler = $httpHandler;
$this->tokenCallback = $tokenCallback;
}
/**
* Updates the request with an Authorization header when auth is 'google_auth'.
*
* use Google\Auth\Middleware\ProxyAuthTokenMiddleware;
* use Google\Auth\OAuth2;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $config = [..<oauth config param>.];
* $oauth2 = new OAuth2($config)
* $middleware = new ProxyAuthTokenMiddleware($oauth2);
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
* 'proxy_auth' => 'google_auth' // authorize all requests
* ]);
*
* $res = $client->get('myproject/taskqueues/myqueue');
*
* @param callable $handler
* @return \Closure
*/
public function __invoke(callable $handler)
{
return function (RequestInterface $request, array $options) use ($handler) {
// Requests using "proxy_auth"="google_auth" will be authorized.
if (!isset($options['proxy_auth']) || $options['proxy_auth'] !== 'google_auth') {
return $handler($request, $options);
}
$request = $request->withHeader('proxy-authorization', 'Bearer ' . $this->fetchToken());
if ($quotaProject = $this->getQuotaProject()) {
$request = $request->withHeader(
GetQuotaProjectInterface::X_GOOG_USER_PROJECT_HEADER,
$quotaProject
);
}
return $handler($request, $options);
};
}
/**
* Call fetcher to fetch the token.
*
* @return string|null
*/
private function fetchToken()
{
$auth_tokens = $this->fetcher->fetchAuthToken($this->httpHandler);
if (array_key_exists('access_token', $auth_tokens)) {
// notify the callback if applicable
if ($this->tokenCallback) {
call_user_func(
$this->tokenCallback,
$this->fetcher->getCacheKey(),
$auth_tokens['access_token']
);
}
return $auth_tokens['access_token'];
}
if (array_key_exists('id_token', $auth_tokens)) {
return $auth_tokens['id_token'];
}
return null;
}
/**
* @return string|null;
*/
private function getQuotaProject()
{
if ($this->fetcher instanceof GetQuotaProjectInterface) {
return $this->fetcher->getQuotaProject();
}
return null;
}
}

View File

@@ -0,0 +1,165 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Middleware;
use Google\Auth\CacheTrait;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\RequestInterface;
/**
* ScopedAccessTokenMiddleware is a Guzzle Middleware that adds an Authorization
* header provided by a closure.
*
* The closure returns an access token, taking the scope, either a single
* string or an array of strings, as its value. If provided, a cache will be
* used to preserve the access token for a given lifetime.
*
* Requests will be accessed with the authorization header:
*
* 'authorization' 'Bearer <value of auth_token>'
*/
class ScopedAccessTokenMiddleware
{
use CacheTrait;
const DEFAULT_CACHE_LIFETIME = 1500;
/**
* @var callable
*/
private $tokenFunc;
/**
* @var array<string>|string
*/
private $scopes;
/**
* Creates a new ScopedAccessTokenMiddleware.
*
* @param callable $tokenFunc a token generator function
* @param array<string>|string $scopes the token authentication scopes
* @param array<mixed> $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface $cache an implementation of CacheItemPoolInterface
*/
public function __construct(
callable $tokenFunc,
$scopes,
array $cacheConfig = null,
CacheItemPoolInterface $cache = null
) {
$this->tokenFunc = $tokenFunc;
if (!(is_string($scopes) || is_array($scopes))) {
throw new \InvalidArgumentException(
'wants scope should be string or array'
);
}
$this->scopes = $scopes;
if (!is_null($cache)) {
$this->cache = $cache;
$this->cacheConfig = array_merge([
'lifetime' => self::DEFAULT_CACHE_LIFETIME,
'prefix' => '',
], $cacheConfig);
}
}
/**
* Updates the request with an Authorization header when auth is 'scoped'.
*
* E.g this could be used to authenticate using the AppEngine
* AppIdentityService.
*
* use google\appengine\api\app_identity\AppIdentityService;
* use Google\Auth\Middleware\ScopedAccessTokenMiddleware;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $scope = 'https://www.googleapis.com/auth/taskqueue'
* $middleware = new ScopedAccessTokenMiddleware(
* 'AppIdentityService::getAccessToken',
* $scope,
* [ 'prefix' => 'Google\Auth\ScopedAccessToken::' ],
* $cache = new Memcache()
* );
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_url' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
* 'auth' => 'scoped' // authorize all requests
* ]);
*
* $res = $client->get('myproject/taskqueues/myqueue');
*
* @param callable $handler
* @return \Closure
*/
public function __invoke(callable $handler)
{
return function (RequestInterface $request, array $options) use ($handler) {
// Requests using "auth"="scoped" will be authorized.
if (!isset($options['auth']) || $options['auth'] !== 'scoped') {
return $handler($request, $options);
}
$request = $request->withHeader('authorization', 'Bearer ' . $this->fetchToken());
return $handler($request, $options);
};
}
/**
* @return string
*/
private function getCacheKey()
{
$key = null;
if (is_string($this->scopes)) {
$key .= $this->scopes;
} elseif (is_array($this->scopes)) {
$key .= implode(':', $this->scopes);
}
return $key;
}
/**
* Determine if token is available in the cache, if not call tokenFunc to
* fetch it.
*
* @return string
*/
private function fetchToken()
{
$cacheKey = $this->getCacheKey();
$cached = $this->getCachedValue($cacheKey);
if (!empty($cached)) {
return $cached;
}
$token = call_user_func($this->tokenFunc, $this->scopes);
$this->setCachedValue($cacheKey, $token);
return $token;
}
}

View File

@@ -0,0 +1,92 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Middleware;
use GuzzleHttp\Psr7\Query;
use Psr\Http\Message\RequestInterface;
/**
* SimpleMiddleware is a Guzzle Middleware that implements Google's Simple API
* access.
*
* Requests are accessed using the Simple API access developer key.
*/
class SimpleMiddleware
{
/**
* @var array<mixed>
*/
private $config;
/**
* Create a new Simple plugin.
*
* The configuration array expects one option
* - key: required, otherwise InvalidArgumentException is thrown
*
* @param array<mixed> $config Configuration array
*/
public function __construct(array $config)
{
if (!isset($config['key'])) {
throw new \InvalidArgumentException('requires a key to have been set');
}
$this->config = array_merge(['key' => null], $config);
}
/**
* Updates the request query with the developer key if auth is set to simple.
*
* use Google\Auth\Middleware\SimpleMiddleware;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $my_key = 'is not the same as yours';
* $middleware = new SimpleMiddleware(['key' => $my_key]);
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/discovery/v1/',
* 'auth' => 'simple'
* ]);
*
* $res = $client->get('drive/v2/rest');
*
* @param callable $handler
* @return \Closure
*/
public function __invoke(callable $handler)
{
return function (RequestInterface $request, array $options) use ($handler) {
// Requests using "auth"="scoped" will be authorized.
if (!isset($options['auth']) || $options['auth'] !== 'simple') {
return $handler($request, $options);
}
$query = Query::parse($request->getUri()->getQuery());
$params = array_merge($query, $this->config);
$uri = $request->getUri()->withQuery(Query::build($params));
$request = $request->withUri($uri);
return $handler($request, $options);
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
<?php
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
/**
* Describes a Credentials object which supports fetching the project ID.
*/
interface ProjectIdProviderInterface
{
/**
* Get the project ID.
*
* @param callable $httpHandler Callback which delivers psr7 request
* @return string|null
*/
public function getProjectId(callable $httpHandler = null);
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\RSA;
/**
* Sign a string using a Service Account private key.
*/
trait ServiceAccountSignerTrait
{
/**
* Sign a string using the service account private key.
*
* @param string $stringToSign
* @param bool $forceOpenssl Whether to use OpenSSL regardless of
* whether phpseclib is installed. **Defaults to** `false`.
* @return string
*/
public function signBlob($stringToSign, $forceOpenssl = false)
{
$privateKey = $this->auth->getSigningKey();
$signedString = '';
if (class_exists(phpseclib3\Crypt\RSA::class) && !$forceOpenssl) {
$key = PublicKeyLoader::load($privateKey);
$rsa = $key->withHash('sha256')->withPadding(RSA::SIGNATURE_PKCS1);
$signedString = $rsa->sign($stringToSign);
} elseif (extension_loaded('openssl')) {
openssl_sign($stringToSign, $signedString, $privateKey, 'sha256WithRSAEncryption');
} else {
// @codeCoverageIgnoreStart
throw new \RuntimeException('OpenSSL is not installed.');
}
// @codeCoverageIgnoreEnd
return base64_encode($signedString);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
/**
* Describes a class which supports signing arbitrary strings.
*/
interface SignBlobInterface extends FetchAuthTokenInterface
{
/**
* Sign a string using the method which is best for a given credentials type.
*
* @param string $stringToSign The string to sign.
* @param bool $forceOpenssl Require use of OpenSSL for local signing. Does
* not apply to signing done using external services. **Defaults to**
* `false`.
* @return string The resulting signature. Value should be base64-encoded.
*/
public function signBlob($stringToSign, $forceOpenssl = false);
/**
* Returns the current Client Name.
*
* @param callable $httpHandler callback which delivers psr7 request, if
* one is required to obtain a client name.
* @return string
*/
public function getClientName(callable $httpHandler = null);
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
/**
* Describes a Credentials object which supports updating request metadata
* (request headers).
*/
interface UpdateMetadataInterface
{
const AUTH_METADATA_KEY = 'authorization';
/**
* Updates metadata with the authorization token.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable $httpHandler callback which delivers psr7 request
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$authUri = null,
callable $httpHandler = null
);
}

View File

@@ -0,0 +1,74 @@
<?php
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
/**
* Provides shared methods for updating request metadata (request headers).
*
* Should implement {@see UpdateMetadataInterface} and {@see FetchAuthTokenInterface}.
*
* @internal
*/
trait UpdateMetadataTrait
{
use MetricsTrait;
/**
* export a callback function which updates runtime metadata.
*
* @return callable updateMetadata function
* @deprecated
*/
public function getUpdateMetadataFunc()
{
return [$this, 'updateMetadata'];
}
/**
* Updates metadata with the authorization token.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable $httpHandler callback which delivers psr7 request
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$authUri = null,
callable $httpHandler = null
) {
$metadata_copy = $metadata;
// We do need to set the service api usage metrics irrespective even if
// the auth token is set because invoking this method with auth tokens
// would mean the intention is to just explicitly set the metrics metadata.
$metadata_copy = $this->applyServiceApiUsageMetrics($metadata_copy);
if (isset($metadata_copy[self::AUTH_METADATA_KEY])) {
// Auth metadata has already been set
return $metadata_copy;
}
$result = $this->fetchAuthToken($httpHandler);
if (isset($result['access_token'])) {
$metadata_copy[self::AUTH_METADATA_KEY] = ['Bearer ' . $result['access_token']];
} elseif (isset($result['id_token'])) {
$metadata_copy[self::AUTH_METADATA_KEY] = ['Bearer ' . $result['id_token']];
}
return $metadata_copy;
}
}