From 17d2d47d8b8d7ad562e3d7f3d17b5a59ed097a27 Mon Sep 17 00:00:00 2001 From: Mahmoud Ashraf <182176867+SNO7E-G@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:10:27 +0500 Subject: [PATCH] feat: support full-format identity tokens on GCE Adds an optional $idTokenFormat constructor argument to GCECredentials. When a target audience is set, passing 'full' appends '&format=full' to the metadata server identity request so the returned ID token includes the full VM instance payload (for example, the authorized party's email), matching the behavior of the Python auth library. The value is validated to 'standard' or 'full'. Fixes #526 --- Auth/src/Credentials/GCECredentials.php | 16 ++++++++++- Auth/tests/Credentials/GCECredentialsTest.php | 28 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/Auth/src/Credentials/GCECredentials.php b/Auth/src/Credentials/GCECredentials.php index 334c002eaf9..097a2fd5e06 100644 --- a/Auth/src/Credentials/GCECredentials.php +++ b/Auth/src/Credentials/GCECredentials.php @@ -218,6 +218,10 @@ class GCECredentials extends CredentialsLoader implements * @param string|null $universeDomain [optional] Specify a universe domain to use * instead of fetching one from the metadata server. * @param bool $enableRegionalAccessBoundary Lookup and include the regional access boundary header. + * @param string|null $idTokenFormat [optional] The format of the identity token requested + * from the metadata server when $targetAudience is set. One of "standard" or "full". + * Use "full" to include the full VM instance details in the token payload (for example, + * the authorized party's email). Defaults to the metadata server's default ("standard"). */ public function __construct( ?Iam $iam = null, @@ -226,7 +230,8 @@ public function __construct( $quotaProject = null, $serviceAccountIdentity = null, ?string $universeDomain = null, - bool $enableRegionalAccessBoundary = false + bool $enableRegionalAccessBoundary = false, + ?string $idTokenFormat = null ) { $this->iam = $iam; @@ -236,6 +241,12 @@ public function __construct( ); } + if ($idTokenFormat !== null && !in_array($idTokenFormat, ['standard', 'full'], true)) { + throw new InvalidArgumentException( + 'Invalid idTokenFormat, must be one of "standard" or "full"' + ); + } + $tokenUri = self::getTokenUri($serviceAccountIdentity); if ($scope) { if (is_string($scope)) { @@ -248,6 +259,9 @@ public function __construct( } elseif ($targetAudience) { $tokenUri = self::getIdTokenUri($serviceAccountIdentity); $tokenUri = $tokenUri . '?audience=' . $targetAudience; + if ($idTokenFormat !== null) { + $tokenUri = $tokenUri . '&format=' . $idTokenFormat; + } $this->targetAudience = $targetAudience; } diff --git a/Auth/tests/Credentials/GCECredentialsTest.php b/Auth/tests/Credentials/GCECredentialsTest.php index f0041d64a1e..ae214c734c9 100644 --- a/Auth/tests/Credentials/GCECredentialsTest.php +++ b/Auth/tests/Credentials/GCECredentialsTest.php @@ -321,6 +321,34 @@ public function testFetchAuthTokenShouldBeIdTokenWhenTargetAudienceIsSet() $this->assertEquals(2, $timesCalled); } + public function testFetchAuthTokenAppendsFullFormatToIdTokenRequest() + { + $expectedToken = ['id_token' => 'idtoken12345']; + $timesCalled = 0; + $httpHandler = function ($request) use (&$timesCalled, $expectedToken) { + $timesCalled++; + if ($timesCalled == 1) { + return new Psr7\Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']); + } + $this->assertEquals( + 'audience=a+target+audience&format=full', + $request->getUri()->getQuery() + ); + return new Psr7\Response(200, [], Utils::streamFor($expectedToken['id_token'])); + }; + $g = new GCECredentials(null, null, 'a+target+audience', null, null, null, false, 'full'); + $this->assertEquals($expectedToken, $g->fetchAuthToken($httpHandler)); + $this->assertEquals(2, $timesCalled); + } + + public function testInvalidIdTokenFormatThrowsException() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid idTokenFormat, must be one of "standard" or "full"'); + + new GCECredentials(null, null, 'a+target+audience', null, null, null, false, 'invalid'); + } + public function testSettingBothScopeAndTargetAudienceThrowsException() { $this->expectException(InvalidArgumentException::class);