From 9db00d279b959c08c9ad867e5714e9b0ab827c8f Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 1 Oct 2024 13:47:04 -0700 Subject: [PATCH 1/4] feat: use IamCredentials endpoint for generating ID tokens outside GDU --- src/Credentials/ServiceAccountCredentials.php | 24 ++++++-- src/Iam.php | 61 ++++++++++++++++++- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/Credentials/ServiceAccountCredentials.php b/src/Credentials/ServiceAccountCredentials.php index 5e7915333..f4844e151 100644 --- a/src/Credentials/ServiceAccountCredentials.php +++ b/src/Credentials/ServiceAccountCredentials.php @@ -19,6 +19,7 @@ use Google\Auth\CredentialsLoader; use Google\Auth\GetQuotaProjectInterface; +use Google\Auth\Iam; use Google\Auth\OAuth2; use Google\Auth\ProjectIdProviderInterface; use Google\Auth\ServiceAccountSignerTrait; @@ -165,6 +166,7 @@ public function __construct( 'scope' => $scope, 'signingAlgorithm' => 'RS256', 'signingKey' => $jsonKey['private_key'], + 'signingKeyId' => $jsonKey['private_key_id'] ?? null, 'sub' => $sub, 'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI, 'additionalClaims' => $additionalClaims, @@ -213,8 +215,17 @@ public function fetchAuthToken(callable $httpHandler = null) return $accessToken; } - $authRequestType = empty($this->auth->getAdditionalClaims()['target_audience']) - ? 'at' : 'it'; + $authRequestType = $this->isIdTokenRequest() ? 'it' : 'at'; + if ($this->isIdTokenRequest() && $this->getUniverseDomain() != self::DEFAULT_UNIVERSE_DOMAIN) { + $idToken = (new Iam($httpHandler, $this->getUniverseDomain()))->generateIdToken( + $this->auth->getIssuer(), + $this->auth->getAdditionalClaims()['target_audience'], + $this->auth->getSigningKey(), + $this->auth->getSigningAlgorithm(), + $this->auth->getSigningKeyId() + ); + return ['id_token' => $idToken]; + } return $this->auth->fetchAuthToken($httpHandler, $this->applyTokenEndpointMetrics([], $authRequestType)); } @@ -399,8 +410,8 @@ private function useSelfSignedJwt() return false; } - // If claims are set, this call is for "id_tokens" - if ($this->auth->getAdditionalClaims()) { + // Do not use self-signed JWT for ID tokens + if ($this->isIdTokenRequest()) { return false; } @@ -416,4 +427,9 @@ private function useSelfSignedJwt() return is_null($this->auth->getScope()); } + + private function isIdTokenRequest(): bool + { + return !empty($this->auth->getAdditionalClaims()['target_audience']); + } } diff --git a/src/Iam.php b/src/Iam.php index 2f67f0009..a2850fa5c 100644 --- a/src/Iam.php +++ b/src/Iam.php @@ -36,6 +36,7 @@ class Iam 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'; + private const GENERATE_ID_TOKEN_PATH = '%s:generateIdToken'; /** * @var callable @@ -73,7 +74,6 @@ public function __construct( */ 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); @@ -102,9 +102,66 @@ public function signBlob($email, $accessToken, $stringToSign, array $delegates = Utils::streamFor(json_encode($body)) ); - $res = $httpHandler($request); + $res = ($this->httpHandler)($request); $body = json_decode((string) $res->getBody(), true); return $body['signedBlob']; } + + /** + * 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 $clientEmail The service account email. + * @param string $targetAudience The audience for the ID token. + * @param string $signingKey The private key to sign the ID token. + * @param string $signingAlg The algorithm to sign the ID token. + * @param string $signingKeyId The key ID to sign the ID token. + * @return string The signed string, base64-encoded. + */ + public function generateIdToken( + string $clientEmail, + string $targetAudience, + string $signingKey, + string $signingAlg, + string $signingKeyId + ) { + $auth = new OAuth2([ + 'issuer' => $clientEmail, + 'sub' => $clientEmail, + 'scope' => 'https://www.googleapis.com/auth/iam', + 'signingKey' => $signingKey, + 'signingKeyId' => $signingKeyId, + 'signingAlgorithm' => $signingAlg, + ]); + + $name = sprintf(self::SERVICE_ACCOUNT_NAME, $clientEmail); + $apiRoot = str_replace('UNIVERSE_DOMAIN', $this->universeDomain, self::IAM_API_ROOT_TEMPLATE); + $uri = $apiRoot . '/' . sprintf(self::GENERATE_ID_TOKEN_PATH, $name); + + $body = [ + 'audience' => $targetAudience, + 'includeEmail' => true, + 'useEmailAzp' => true, + ]; + + $headers = [ + 'Authorization' => 'Bearer ' . $auth->toJwt(), + ]; + + $request = new Psr7\Request( + 'POST', + $uri, + $headers, + Utils::streamFor(json_encode($body)) + ); + + $res = ($this->httpHandler)($request); + $body = json_decode((string) $res->getBody(), true); + + return $body['token']; + } } From 6070c6d5a6756247aede774ad35eefe2c3c0dcb1 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 4 Oct 2024 10:48:10 -0700 Subject: [PATCH 2/4] fix cs --- src/CredentialSource/AwsNativeSource.php | 2 +- src/Credentials/GCECredentials.php | 4 ++-- src/Middleware/AuthTokenMiddleware.php | 3 ++- src/OAuth2.php | 13 ++++++----- tests/ApplicationDefaultCredentialsTest.php | 6 +++-- tests/Cache/FileSystemCacheItemPoolTest.php | 2 +- tests/CredentialSource/FileSourceTest.php | 1 - .../ExternalAccountCredentialsTest.php | 2 +- tests/Credentials/GCECredentialsTest.php | 2 +- tests/OAuth2Test.php | 22 +++++++++---------- tests/mocks/TestFileCacheItemPool.php | 1 - tests/phpstan-autoload.php | 2 +- 12 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/CredentialSource/AwsNativeSource.php b/src/CredentialSource/AwsNativeSource.php index 6d9244ba2..e99d0ee6f 100644 --- a/src/CredentialSource/AwsNativeSource.php +++ b/src/CredentialSource/AwsNativeSource.php @@ -103,7 +103,7 @@ public function fetchSubjectToken(callable $httpHandler = null): string $headers['x-goog-cloud-target-resource'] = $this->audience; // Format headers as they're expected in the subject token - $formattedHeaders= array_map( + $formattedHeaders = array_map( fn ($k, $v) => ['key' => $k, 'value' => $v], array_keys($headers), $headers, diff --git a/src/Credentials/GCECredentials.php b/src/Credentials/GCECredentials.php index 8b7547816..49030a845 100644 --- a/src/Credentials/GCECredentials.php +++ b/src/Credentials/GCECredentials.php @@ -426,12 +426,12 @@ private static function detectResidencyWindows(string $registryProductKey): bool try { $productName = $shell->regRead($registryProductKey); - } catch(com_exception) { + } 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); } diff --git a/src/Middleware/AuthTokenMiddleware.php b/src/Middleware/AuthTokenMiddleware.php index 798766efa..3bbda7a23 100644 --- a/src/Middleware/AuthTokenMiddleware.php +++ b/src/Middleware/AuthTokenMiddleware.php @@ -132,7 +132,8 @@ private function addAuthHeaders(RequestInterface $request) ) { $token = $this->fetcher->fetchAuthToken(); $request = $request->withHeader( - 'authorization', 'Bearer ' . ($token['access_token'] ?? $token['id_token'] ?? '') + 'authorization', + 'Bearer ' . ($token['access_token'] ?? $token['id_token'] ?? '') ); } else { $headers = $this->fetcher->updateMetadata($request->getHeaders(), null, $this->httpHandler); diff --git a/src/OAuth2.php b/src/OAuth2.php index 4019e258a..2463854e0 100644 --- a/src/OAuth2.php +++ b/src/OAuth2.php @@ -724,7 +724,7 @@ public function getSubjectTokenFetcher(): ?ExternalAccountCredentialSourceInterf */ public function parseTokenResponse(ResponseInterface $resp) { - $body = (string)$resp->getBody(); + $body = (string) $resp->getBody(); if ($resp->hasHeader('Content-Type') && $resp->getHeaderLine('Content-Type') == 'application/x-www-form-urlencoded' ) { @@ -1009,13 +1009,13 @@ public function setRedirectUri($uri) if (!$this->isAbsoluteUri($uri)) { // "postmessage" is a reserved URI string in Google-land // @see https://developers.google.com/identity/sign-in/web/server-side-flow - if ('postmessage' !== (string)$uri) { + if ('postmessage' !== (string) $uri) { throw new InvalidArgumentException( 'Redirect URI must be absolute' ); } } - $this->redirectUri = (string)$uri; + $this->redirectUri = (string) $uri; } /** @@ -1127,7 +1127,7 @@ public function setGrantType($grantType) 'invalid grant type' ); } - $this->grantType = (string)$grantType; + $this->grantType = (string) $grantType; } } @@ -1460,7 +1460,7 @@ public function setExpiresIn($expiresIn) $this->issuedAt = null; } else { $this->issuedAt = time(); - $this->expiresIn = (int)$expiresIn; + $this->expiresIn = (int) $expiresIn; } } @@ -1768,7 +1768,8 @@ private function getFirebaseJwtKeys($publicKey, $allowedAlgs) throw new \InvalidArgumentException( 'To have multiple allowed algorithms, You must provide an' . ' array of Firebase\JWT\Key objects.' - . ' See https://github.com/firebase/php-jwt for more information.'); + . ' See https://github.com/firebase/php-jwt for more information.' + ); } $allowedAlg = array_pop($allowedAlgs); } else { diff --git a/tests/ApplicationDefaultCredentialsTest.php b/tests/ApplicationDefaultCredentialsTest.php index fa537691f..c1583ed06 100644 --- a/tests/ApplicationDefaultCredentialsTest.php +++ b/tests/ApplicationDefaultCredentialsTest.php @@ -168,7 +168,8 @@ public function testImpersonatedServiceAccountCredentials() ); $this->assertInstanceOf( 'Google\Auth\Credentials\ImpersonatedServiceAccountCredentials', - $creds); + $creds + ); $this->assertEquals('service_account_name@namespace.iam.gserviceaccount.com', $creds->getClientName()); @@ -179,7 +180,8 @@ public function testImpersonatedServiceAccountCredentials() $sourceCredentials = $sourceCredentialsProperty->getValue($creds); $this->assertInstanceOf( 'Google\Auth\Credentials\UserRefreshCredentials', - $sourceCredentials); + $sourceCredentials + ); } public function testUserRefreshCredentials() diff --git a/tests/Cache/FileSystemCacheItemPoolTest.php b/tests/Cache/FileSystemCacheItemPoolTest.php index a3214587a..86b3e4eb5 100644 --- a/tests/Cache/FileSystemCacheItemPoolTest.php +++ b/tests/Cache/FileSystemCacheItemPoolTest.php @@ -43,7 +43,7 @@ public function tearDown(): void { $files = scandir($this->defaultCacheDirectory); - foreach($files as $fileName) { + foreach ($files as $fileName) { if ($fileName === '.' || $fileName === '..') { continue; } diff --git a/tests/CredentialSource/FileSourceTest.php b/tests/CredentialSource/FileSourceTest.php index e2c79bde7..9cdcbb9cc 100644 --- a/tests/CredentialSource/FileSourceTest.php +++ b/tests/CredentialSource/FileSourceTest.php @@ -45,7 +45,6 @@ public function provideFetchSubjectToken() $file1 = tempnam(sys_get_temp_dir(), 'test1'); file_put_contents($file1, 'abc'); - $file2 = tempnam(sys_get_temp_dir(), 'test2'); file_put_contents($file2, json_encode(['token' => 'def'])); diff --git a/tests/Credentials/ExternalAccountCredentialsTest.php b/tests/Credentials/ExternalAccountCredentialsTest.php index 09cac05db..4d1f8ae0e 100644 --- a/tests/Credentials/ExternalAccountCredentialsTest.php +++ b/tests/Credentials/ExternalAccountCredentialsTest.php @@ -561,7 +561,7 @@ public function testUrlSourceCacheKey() $expectedKey = 'fakeUrl.scope1...'; $this->assertEquals($expectedKey, $cacheKey); } - + public function testExecutableSourceCacheKey() { $this->baseCreds['credential_source'] = [ diff --git a/tests/Credentials/GCECredentialsTest.php b/tests/Credentials/GCECredentialsTest.php index 7aca40510..f6d9c2266 100644 --- a/tests/Credentials/GCECredentialsTest.php +++ b/tests/Credentials/GCECredentialsTest.php @@ -138,7 +138,7 @@ public function testOnWindowsGceWithResidencyWithNoCom() $method = (new ReflectionClass(GCECredentials::class)) ->getMethod('detectResidencyWindows'); - + $method->setAccessible(true); $this->assertFalse($method->invoke(null, 'thisShouldBeFalse')); diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index e00ab647f..14263fce0 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -290,7 +290,7 @@ public function testRedirectUriPostmessageIsAllowed() ]); $this->assertEquals('postmessage', $o->getRedirectUri()); $url = $o->buildFullAuthorizationUri(); - $parts = parse_url((string)$url); + $parts = parse_url((string) $url); parse_str($parts['query'], $query); $this->assertArrayHasKey('redirect_uri', $query); $this->assertEquals('postmessage', $query['redirect_uri']); @@ -726,7 +726,7 @@ public function testGeneratesAuthorizationCodeRequests() $req = $o->generateCredentialsRequest(); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $req); $this->assertEquals('POST', $req->getMethod()); - $fields = Query::parse((string)$req->getBody()); + $fields = Query::parse((string) $req->getBody()); $this->assertEquals('authorization_code', $fields['grant_type']); $this->assertEquals('an_auth_code', $fields['code']); } @@ -745,7 +745,7 @@ public function testGeneratesPasswordRequests() $req = $o->generateCredentialsRequest(); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $req); $this->assertEquals('POST', $req->getMethod()); - $fields = Query::parse((string)$req->getBody()); + $fields = Query::parse((string) $req->getBody()); $this->assertEquals('password', $fields['grant_type']); $this->assertEquals('a_password', $fields['password']); $this->assertEquals('a_username', $fields['username']); @@ -764,7 +764,7 @@ public function testGeneratesRefreshTokenRequests() $req = $o->generateCredentialsRequest(); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $req); $this->assertEquals('POST', $req->getMethod()); - $fields = Query::parse((string)$req->getBody()); + $fields = Query::parse((string) $req->getBody()); $this->assertEquals('refresh_token', $fields['grant_type']); $this->assertEquals('a_refresh_token', $fields['refresh_token']); } @@ -780,7 +780,7 @@ public function testClientSecretAddedIfSetForAuthorizationCodeRequests() $o = new OAuth2($testConfig); $o->setCode('an_auth_code'); $request = $o->generateCredentialsRequest(); - $fields = Query::parse((string)$request->getBody()); + $fields = Query::parse((string) $request->getBody()); $this->assertEquals('a_client_secret', $fields['client_secret']); } @@ -794,7 +794,7 @@ public function testClientSecretAddedIfSetForRefreshTokenRequests() $o = new OAuth2($testConfig); $o->setRefreshToken('a_refresh_token'); $request = $o->generateCredentialsRequest(); - $fields = Query::parse((string)$request->getBody()); + $fields = Query::parse((string) $request->getBody()); $this->assertEquals('a_client_secret', $fields['client_secret']); } @@ -809,7 +809,7 @@ public function testClientSecretAddedIfSetForPasswordRequests() $o->setUsername('a_username'); $o->setPassword('a_password'); $request = $o->generateCredentialsRequest(); - $fields = Query::parse((string)$request->getBody()); + $fields = Query::parse((string) $request->getBody()); $this->assertEquals('a_client_secret', $fields['client_secret']); } @@ -827,7 +827,7 @@ public function testGeneratesAssertionRequests() $req = $o->generateCredentialsRequest(); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $req); $this->assertEquals('POST', $req->getMethod()); - $fields = Query::parse((string)$req->getBody()); + $fields = Query::parse((string) $req->getBody()); $this->assertEquals(OAuth2::JWT_URN, $fields['grant_type']); $this->assertArrayHasKey('assertion', $fields); } @@ -846,7 +846,7 @@ public function testGeneratesExtendedRequests() $req = $o->generateCredentialsRequest(); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $req); $this->assertEquals('POST', $req->getMethod()); - $fields = Query::parse((string)$req->getBody()); + $fields = Query::parse((string) $req->getBody()); $this->assertEquals('my_value', $fields['my_param']); $this->assertEquals('urn:my_test_grant_type', $fields['grant_type']); } @@ -1289,7 +1289,7 @@ public function testStsCredentialsRequestMinimal() $request = $o->generateCredentialsRequest(); $this->assertEquals('POST', $request->getMethod()); $this->assertEquals($this->stsMinimal['tokenCredentialUri'], (string) $request->getUri()); - parse_str((string)$request->getBody(), $requestParams); + parse_str((string) $request->getBody(), $requestParams); $this->assertCount(4, $requestParams); $this->assertEquals(OAuth2::STS_URN, $requestParams['grant_type']); $this->assertEquals('xyz', $requestParams['subject_token']); @@ -1314,7 +1314,7 @@ public function testStsCredentialsRequestFull() $request = $o->generateCredentialsRequest(); $this->assertEquals('POST', $request->getMethod()); $this->assertEquals($this->stsMinimal['tokenCredentialUri'], (string) $request->getUri()); - parse_str((string)$request->getBody(), $requestParams); + parse_str((string) $request->getBody(), $requestParams); $this->assertCount(9, $requestParams); $this->assertEquals(OAuth2::STS_URN, $requestParams['grant_type']); diff --git a/tests/mocks/TestFileCacheItemPool.php b/tests/mocks/TestFileCacheItemPool.php index 42c8d5a5c..65fbc8a77 100644 --- a/tests/mocks/TestFileCacheItemPool.php +++ b/tests/mocks/TestFileCacheItemPool.php @@ -37,7 +37,6 @@ final class TestFileCacheItemPool implements CacheItemPoolInterface */ private $deferredItems; - public function __construct(string $cacheDir) { $this->cacheDir = $cacheDir; diff --git a/tests/phpstan-autoload.php b/tests/phpstan-autoload.php index 22a38a245..38e79cbcc 100644 --- a/tests/phpstan-autoload.php +++ b/tests/phpstan-autoload.php @@ -10,7 +10,7 @@ public function __construct(string $command) { //do nothing } - + public function regRead(string $key): string { // do nothing From aefc2fe7e7faca8b32eea55ea69ed6a0dd6bae9f Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sat, 5 Oct 2024 10:19:40 -0700 Subject: [PATCH 3/4] add test, misc cleanup --- src/Credentials/ServiceAccountCredentials.php | 29 +++++++++++++---- src/Iam.php | 26 ++++------------ .../ServiceAccountCredentialsTest.php | 31 +++++++++++++++++++ 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/src/Credentials/ServiceAccountCredentials.php b/src/Credentials/ServiceAccountCredentials.php index f4844e151..2b88b1e75 100644 --- a/src/Credentials/ServiceAccountCredentials.php +++ b/src/Credentials/ServiceAccountCredentials.php @@ -17,6 +17,7 @@ namespace Google\Auth\Credentials; +use Firebase\JWT\JWT; use Google\Auth\CredentialsLoader; use Google\Auth\GetQuotaProjectInterface; use Google\Auth\Iam; @@ -72,6 +73,7 @@ class ServiceAccountCredentials extends CredentialsLoader implements * @var string */ private const CRED_TYPE = 'sa'; + private const IAM_SCOPE = 'https://www.googleapis.com/auth/iam'; /** * The OAuth2 instance used to conduct authorization. @@ -215,18 +217,33 @@ public function fetchAuthToken(callable $httpHandler = null) return $accessToken; } - $authRequestType = $this->isIdTokenRequest() ? 'it' : 'at'; - if ($this->isIdTokenRequest() && $this->getUniverseDomain() != self::DEFAULT_UNIVERSE_DOMAIN) { - $idToken = (new Iam($httpHandler, $this->getUniverseDomain()))->generateIdToken( - $this->auth->getIssuer(), - $this->auth->getAdditionalClaims()['target_audience'], + + if ($this->isIdTokenRequest() && $this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) { + $now = time(); + $jwt = Jwt::encode( + [ + 'iss' => $this->auth->getIssuer(), + 'sub' => $this->auth->getIssuer(), + 'scope' => self::IAM_SCOPE, + 'exp' => ($now + $this->auth->getExpiry()), + 'iat' => ($now - OAuth2::DEFAULT_SKEW_SECONDS), + ], $this->auth->getSigningKey(), $this->auth->getSigningAlgorithm(), $this->auth->getSigningKeyId() ); + // We create a new instance of Iam each time because the `$httpHandler` might change. + $idToken = (new Iam($httpHandler, $this->getUniverseDomain()))->generateIdToken( + $this->auth->getIssuer(), + $this->auth->getAdditionalClaims()['target_audience'], + $jwt + ); return ['id_token' => $idToken]; } - return $this->auth->fetchAuthToken($httpHandler, $this->applyTokenEndpointMetrics([], $authRequestType)); + return $this->auth->fetchAuthToken( + $httpHandler, + $this->applyTokenEndpointMetrics([], $this->isIdTokenRequest() ? 'it' : 'at') + ); } /** diff --git a/src/Iam.php b/src/Iam.php index a2850fa5c..a5eaaec65 100644 --- a/src/Iam.php +++ b/src/Iam.php @@ -117,41 +117,27 @@ public function signBlob($email, $accessToken, $stringToSign, array $delegates = * * @param string $clientEmail The service account email. * @param string $targetAudience The audience for the ID token. - * @param string $signingKey The private key to sign the ID token. - * @param string $signingAlg The algorithm to sign the ID token. - * @param string $signingKeyId The key ID to sign the ID token. + * @param string $bearerToken The token to authenticate the IAM request. + * * @return string The signed string, base64-encoded. */ public function generateIdToken( string $clientEmail, string $targetAudience, - string $signingKey, - string $signingAlg, - string $signingKeyId - ) { - $auth = new OAuth2([ - 'issuer' => $clientEmail, - 'sub' => $clientEmail, - 'scope' => 'https://www.googleapis.com/auth/iam', - 'signingKey' => $signingKey, - 'signingKeyId' => $signingKeyId, - 'signingAlgorithm' => $signingAlg, - ]); - + string $bearerToken + ): string { $name = sprintf(self::SERVICE_ACCOUNT_NAME, $clientEmail); $apiRoot = str_replace('UNIVERSE_DOMAIN', $this->universeDomain, self::IAM_API_ROOT_TEMPLATE); $uri = $apiRoot . '/' . sprintf(self::GENERATE_ID_TOKEN_PATH, $name); + $headers = ['Authorization' => 'Bearer ' . $bearerToken]; + $body = [ 'audience' => $targetAudience, 'includeEmail' => true, 'useEmailAzp' => true, ]; - $headers = [ - 'Authorization' => 'Bearer ' . $auth->toJwt(), - ]; - $request = new Psr7\Request( 'POST', $uri, diff --git a/tests/Credentials/ServiceAccountCredentialsTest.php b/tests/Credentials/ServiceAccountCredentialsTest.php index 4352af154..41d0b40e1 100644 --- a/tests/Credentials/ServiceAccountCredentialsTest.php +++ b/tests/Credentials/ServiceAccountCredentialsTest.php @@ -23,6 +23,7 @@ use Google\Auth\CredentialsLoader; use Google\Auth\OAuth2; use GuzzleHttp\Psr7; +use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Utils; use InvalidArgumentException; @@ -307,6 +308,36 @@ public function testShouldBeIdTokenWhenTargetAudienceIsSet() $this->assertEquals(1, $timesCalled); } + public function testShouldUseIamWhenTargetAudienceAndUniverseDomainIsSet() + { + $testJson = $this->createTestJson(); + $testJson['universe_domain'] = 'abc.xyz'; + + $timesCalled = 0; + $httpHandler = function (Request $request) use (&$timesCalled) { + $timesCalled++; + + // Verify Request + $this->assertStringContainsString(':generateIdToken', $request->getUri()); + $json = json_decode($request->getBody(), true); + $this->assertArrayHasKey('audience', $json); + $this->assertEquals('a target audience', $json['audience']); + + // Verify JWT Bearer Token + $jwt = str_replace('Bearer ', '', $request->getHeaderLine('Authorization')); + list($header, $payload, $sig) = explode('.', $jwt); + $jwtParams = json_decode(base64_decode($payload), true); + $this->assertArrayHasKey('iss', $jwtParams); + $this->assertEquals('test@example.com', $jwtParams['iss']); + + // return expected IAM ID token response + return new Psr7\Response(200, [], json_encode(['token' => 'idtoken12345'])); + }; + $sa = new ServiceAccountCredentials(null, $testJson, null, 'a target audience'); + $this->assertEquals('idtoken12345', $sa->fetchAuthToken($httpHandler)['id_token']); + $this->assertEquals(1, $timesCalled); + } + public function testShouldBeOAuthRequestWhenSubIsSet() { $testJson = $this->createTestJson(); From 475bc742d2302d6d2abcc2d7d14d9c636a6657b9 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 5 Nov 2024 10:54:16 -0800 Subject: [PATCH 4/4] add token endpoint metric --- src/Credentials/ServiceAccountCredentials.php | 3 ++- src/Iam.php | 6 ++++-- tests/Credentials/ServiceAccountCredentialsTest.php | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Credentials/ServiceAccountCredentials.php b/src/Credentials/ServiceAccountCredentials.php index 2b88b1e75..5c651bb71 100644 --- a/src/Credentials/ServiceAccountCredentials.php +++ b/src/Credentials/ServiceAccountCredentials.php @@ -236,7 +236,8 @@ public function fetchAuthToken(callable $httpHandler = null) $idToken = (new Iam($httpHandler, $this->getUniverseDomain()))->generateIdToken( $this->auth->getIssuer(), $this->auth->getAdditionalClaims()['target_audience'], - $jwt + $jwt, + $this->applyTokenEndpointMetrics([], 'it') ); return ['id_token' => $idToken]; } diff --git a/src/Iam.php b/src/Iam.php index a5eaaec65..46bee30d2 100644 --- a/src/Iam.php +++ b/src/Iam.php @@ -118,19 +118,21 @@ public function signBlob($email, $accessToken, $stringToSign, array $delegates = * @param string $clientEmail The service account email. * @param string $targetAudience The audience for the ID token. * @param string $bearerToken The token to authenticate the IAM request. + * @param array $headers [optional] Additional headers to send with the request. * * @return string The signed string, base64-encoded. */ public function generateIdToken( string $clientEmail, string $targetAudience, - string $bearerToken + string $bearerToken, + array $headers = [] ): string { $name = sprintf(self::SERVICE_ACCOUNT_NAME, $clientEmail); $apiRoot = str_replace('UNIVERSE_DOMAIN', $this->universeDomain, self::IAM_API_ROOT_TEMPLATE); $uri = $apiRoot . '/' . sprintf(self::GENERATE_ID_TOKEN_PATH, $name); - $headers = ['Authorization' => 'Bearer ' . $bearerToken]; + $headers['Authorization'] = 'Bearer ' . $bearerToken; $body = [ 'audience' => $targetAudience, diff --git a/tests/Credentials/ServiceAccountCredentialsTest.php b/tests/Credentials/ServiceAccountCredentialsTest.php index 41d0b40e1..63110448e 100644 --- a/tests/Credentials/ServiceAccountCredentialsTest.php +++ b/tests/Credentials/ServiceAccountCredentialsTest.php @@ -330,6 +330,10 @@ public function testShouldUseIamWhenTargetAudienceAndUniverseDomainIsSet() $this->assertArrayHasKey('iss', $jwtParams); $this->assertEquals('test@example.com', $jwtParams['iss']); + // Verify header contains the auth headers + $parts = explode(' ', $request->getHeaderLine('x-goog-api-client')); + $this->assertContains('auth-request-type/it', $parts); + // return expected IAM ID token response return new Psr7\Response(200, [], json_encode(['token' => 'idtoken12345'])); };