Skip to content

Commit

Permalink
revert ID tokens for ImpersonatedServiceAccountCredentials
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer committed Nov 5, 2024
1 parent 4d34f14 commit ebd83b3
Show file tree
Hide file tree
Showing 5 changed files with 13 additions and 200 deletions.
2 changes: 0 additions & 2 deletions src/ApplicationDefaultCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use DomainException;
use Google\Auth\Credentials\AppIdentityCredentials;
use Google\Auth\Credentials\GCECredentials;
use Google\Auth\Credentials\ImpersonatedServiceAccountCredentials;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\Credentials\UserRefreshCredentials;
use Google\Auth\HttpHandler\HttpClientCache;
Expand Down Expand Up @@ -302,7 +301,6 @@ public static function getIdTokenCredentials(

$creds = match ($jsonKey['type']) {
'authorized_user' => new UserRefreshCredentials(null, $jsonKey, $targetAudience),
'impersonated_service_account' => new ImpersonatedServiceAccountCredentials(null, $jsonKey, $targetAudience),
'service_account' => new ServiceAccountCredentials(null, $jsonKey, null, $targetAudience),
default => throw new InvalidArgumentException('invalid value in the type field')
};
Expand Down
54 changes: 12 additions & 42 deletions src/Credentials/ImpersonatedServiceAccountCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements
use IamSignerTrait;

private const CRED_TYPE = 'imp';
private const IAM_SCOPE = 'https://www.googleapis.com/auth/iam';

/**
* @var string
Expand Down Expand Up @@ -72,12 +71,10 @@ class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements
* @type int $lifetime The lifetime of the impersonated credentials
* @type string[] $delegates The delegates to impersonate
* }
* @param string|null $targetAudience The audience to request an ID token.
*/
public function __construct(
$scope,
$jsonKey,
private ?string $targetAudience = null
$jsonKey
) {
if (is_string($jsonKey)) {
if (!file_exists($jsonKey)) {
Expand All @@ -96,23 +93,10 @@ public function __construct(
if (!array_key_exists('source_credentials', $jsonKey)) {
throw new LogicException('json key is missing the source_credentials field');
}
if ($scope && $targetAudience) {
throw new InvalidArgumentException(
'Scope and targetAudience cannot both be supplied'
);
}
if (is_array($jsonKey['source_credentials'])) {
if (!array_key_exists('type', $jsonKey['source_credentials'])) {
throw new InvalidArgumentException('json key source credentials are missing the type field');
}
if (
$targetAudience !== null
&& $jsonKey['source_credentials']['type'] === 'service_account'
) {
// Service account tokens MUST request a scope, and as this token is only used to impersonate
// an ID token, the narrowest scope we can request is `iam`.
$scope = self::IAM_SCOPE;
}
$jsonKey['source_credentials'] = CredentialsLoader::makeCredentials($scope, $jsonKey['source_credentials']);
}

Expand Down Expand Up @@ -187,19 +171,13 @@ public function fetchAuthToken(?callable $httpHandler = null)
'Content-Type' => 'application/json',
'Cache-Control' => 'no-store',
'Authorization' => sprintf('Bearer %s', $authToken['access_token'] ?? $authToken['id_token']),
], $this->isIdTokenRequest() ? 'it' : 'at');

$body = match ($this->isIdTokenRequest()) {
true => [
'audience' => $this->targetAudience,
'includeEmail' => true,
],
false => [
'scope' => $this->targetScope,
'delegates' => $this->delegates,
'lifetime' => sprintf('%ss', $this->lifetime),
]
};
], 'at');

$body = [
'scope' => $this->targetScope,
'delegates' => $this->delegates,
'lifetime' => sprintf('%ss', $this->lifetime),
];

$request = new Request(
'POST',
Expand All @@ -211,13 +189,10 @@ public function fetchAuthToken(?callable $httpHandler = null)
$response = $httpHandler($request);
$body = json_decode((string) $response->getBody(), true);

return match ($this->isIdTokenRequest()) {
true => ['id_token' => $body['token']],
false => [
'access_token' => $body['accessToken'],
'expires_at' => strtotime($body['expireTime']),
]
};
return [
'access_token' => $body['accessToken'],
'expires_at' => strtotime($body['expireTime']),
];
}

/**
Expand Down Expand Up @@ -245,9 +220,4 @@ protected function getCredType(): string
{
return self::CRED_TYPE;
}

private function isIdTokenRequest(): bool
{
return !is_null($this->targetAudience);
}
}
7 changes: 0 additions & 7 deletions tests/ApplicationDefaultCredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -496,13 +496,6 @@ public function testGetIdTokenCredentialsFailsIfNotOnGceAndNoDefaultFileFound()
);
}

public function testGetIdTokenCredentialsWithImpersonatedServiceAccountCredentials()
{
putenv('HOME=' . __DIR__ . '/fixtures5');
$creds = ApplicationDefaultCredentials::getIdTokenCredentials('[email protected]');
$this->assertInstanceOf(ImpersonatedServiceAccountCredentials::class, $creds);
}

public function testGetIdTokenCredentialsWithCacheOptions()
{
$keyFile = __DIR__ . '/fixtures' . '/private.json';
Expand Down
136 changes: 1 addition & 135 deletions tests/Credentials/ImpersonatedServiceAccountCredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\Credentials\UserRefreshCredentials;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\Middleware\AuthTokenMiddleware;
use Google\Auth\OAuth2;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use LogicException;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PHPUnit\ProphecyTrait;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Http\Message\RequestInterface;
use ReflectionClass;

Expand Down Expand Up @@ -135,44 +133,6 @@ public function testGetAccessTokenWithServiceAccountAndUserRefreshCredentials($j
$this->assertEquals(2, $requestCount);
}

/**
* Test ID token impersonation for Service Account and User Refresh Credentials.
*
* @dataProvider provideAuthTokenJson
*/
public function testGetIdTokenWithServiceAccountAndUserRefreshCredentials($json, $grantType)
{
$requestCount = 0;
// getting an id token will take two requests
$httpHandler = function (RequestInterface $request) use (&$requestCount, $json, $grantType) {
if (++$requestCount == 1) {
// the call to swap the refresh token for an access token
$this->assertEquals(UserRefreshCredentials::TOKEN_CREDENTIAL_URI, (string) $request->getUri());
parse_str((string) $request->getBody(), $result);
$this->assertEquals($grantType, $result['grant_type']);
} elseif ($requestCount == 2) {
// the call to swap the access token for an id token
$this->assertEquals($json['service_account_impersonation_url'], (string) $request->getUri());
$this->assertEquals(self::TARGET_AUDIENCE, json_decode($request->getBody(), true)['audience'] ?? '');
$this->assertEquals('Bearer test-access-token', $request->getHeader('authorization')[0] ?? null);
}

return new Response(
200,
['Content-Type' => 'application/json'],
json_encode(match ($requestCount) {
1 => ['access_token' => 'test-access-token'],
2 => ['token' => 'test-impersonated-id-token']
})
);
};

$creds = new ImpersonatedServiceAccountCredentials(null, $json, self::TARGET_AUDIENCE);
$token = $creds->fetchAuthToken($httpHandler);
$this->assertEquals('test-impersonated-id-token', $token['id_token']);
$this->assertEquals(2, $requestCount);
}

public function provideAuthTokenJson()
{
return [
Expand Down Expand Up @@ -220,72 +180,6 @@ public function testGetAccessTokenWithExternalAccountCredentials()
$this->assertEquals(3, $requestCount);
}

/**
* Test ID token impersonation for Exernal Account Credentials.
*/
public function testGetIdTokenWithExternalAccountCredentials()
{
$json = self::EXTERNAL_ACCOUNT_TO_SERVICE_ACCOUNT_JSON;
$httpHandler = function (RequestInterface $request) use (&$requestCount, $json) {
if (++$requestCount == 1) {
// the call to swap the refresh token for an access token
$this->assertEquals(
$json['source_credentials']['credential_source']['url'],
(string) $request->getUri()
);
} elseif ($requestCount == 2) {
$this->assertEquals($json['source_credentials']['token_url'], (string) $request->getUri());
} elseif ($requestCount == 3) {
// the call to swap the access token for an id token
$this->assertEquals($json['service_account_impersonation_url'], (string) $request->getUri());
$this->assertEquals(self::TARGET_AUDIENCE, json_decode($request->getBody(), true)['audience'] ?? '');
$this->assertEquals('Bearer test-access-token', $request->getHeader('authorization')[0] ?? null);
}

return new Response(
200,
['Content-Type' => 'application/json'],
json_encode(match ($requestCount) {
1 => ['access_token' => 'test-access-token'],
2 => ['access_token' => 'test-access-token'],
3 => ['token' => 'test-impersonated-id-token']
})
);
};

$creds = new ImpersonatedServiceAccountCredentials(null, $json, self::TARGET_AUDIENCE);
$token = $creds->fetchAuthToken($httpHandler);
$this->assertEquals('test-impersonated-id-token', $token['id_token']);
$this->assertEquals(3, $requestCount);
}

/**
* Test ID token impersonation for an arbitrary credential fetcher.
*/
public function testGetIdTokenWithArbitraryCredentials()
{
$httpHandler = function (RequestInterface $request) {
$this->assertEquals('https://some/url', (string) $request->getUri());
$this->assertEquals('Bearer test-access-token', $request->getHeader('authorization')[0] ?? null);
return new Response(200, [], json_encode(['token' => 'test-impersonated-id-token']));
};

$credentials = $this->prophesize(FetchAuthTokenInterface::class);
$credentials->fetchAuthToken($httpHandler, Argument::type('array'))
->shouldBeCalledOnce()
->willReturn(['access_token' => 'test-access-token']);

$json = [
'type' => 'impersonated_service_account',
'service_account_impersonation_url' => 'https://some/url',
'source_credentials' => $credentials->reveal(),
];
$creds = new ImpersonatedServiceAccountCredentials(null, $json, self::TARGET_AUDIENCE);

$token = $creds->fetchAuthToken($httpHandler);
$this->assertEquals('test-impersonated-id-token', $token['id_token']);
}

/**
* Test access token impersonation for an arbitrary credential fetcher.
*/
Expand Down Expand Up @@ -317,34 +211,6 @@ public function testGetAccessTokenWithArbitraryCredentials()
$this->assertEquals('test-impersonated-access-token', $token['access_token']);
}

public function testIdTokenWithAuthTokenMiddleware()
{
$targetAudience = 'test-target-audience';
$credentials = new ImpersonatedServiceAccountCredentials(null, self::USER_TO_SERVICE_ACCOUNT_JSON, $targetAudience);

// this handler is for the middleware constructor, which will pass it to the ISAC to fetch tokens
$httpHandler = getHandler([
new Response(200, ['Content-Type' => 'application/json'], '{"access_token":"this.is.an.access.token"}'),
new Response(200, ['Content-Type' => 'application/json'], '{"token":"this.is.an.id.token"}'),
]);
$middleware = new AuthTokenMiddleware($credentials, $httpHandler);

// this handler is the actual handler that makes the authenticated request
$requestCount = 0;
$httpHandler = function (RequestInterface $request) use (&$requestCount) {
$requestCount++;
$this->assertTrue($request->hasHeader('authorization'));
$this->assertEquals('Bearer this.is.an.id.token', $request->getHeader('authorization')[0] ?? null);
};

$middleware($httpHandler)(
new Request('GET', 'https://www.google.com'),
['auth' => 'google_auth']
);

$this->assertEquals(1, $requestCount);
}

// User Refresh to Service Account Impersonation JSON Credentials
private const USER_TO_SERVICE_ACCOUNT_JSON = [
'type' => 'impersonated_service_account',
Expand Down
14 changes: 0 additions & 14 deletions tests/ObservabilityMetricsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,20 +131,6 @@ public function testImpersonatedServiceAccountCredentials()
$this->assertUpdateMetadata($impersonatedCred, $handler, 'imp', $handlerCalled);
}

public function testImpersonatedServiceAccountCredentialsWithIdTokens()
{
$keyFile = __DIR__ . '/fixtures5/.config/gcloud/application_default_credentials.json';
$handlerCalled = false;
$responseFromIam = json_encode(['token' => '1/abdef1234567890']);
$handler = getHandler([
$this->getExpectedRequest('imp', 'auth-request-type/at', $handlerCalled, $this->jsonTokens),
$this->getExpectedRequest('imp', 'auth-request-type/it', $handlerCalled, $responseFromIam),
]);

$impersonatedCred = new ImpersonatedServiceAccountCredentials(null, $keyFile, 'test-target-audience');
$this->assertUpdateMetadata($impersonatedCred, $handler, 'imp', $handlerCalled);
}

/**
* UserRefreshCredentials haven't enabled identity token support hence
* they don't have 'auth-request-type/it' observability metric header check.
Expand Down

0 comments on commit ebd83b3

Please sign in to comment.