Skip to content

Commit

Permalink
add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer committed Oct 4, 2024
1 parent fa156fa commit 20f2c56
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 45 deletions.
27 changes: 23 additions & 4 deletions src/Credentials/ImpersonatedServiceAccountCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
use Google\Auth\IamSignerTrait;
use Google\Auth\SignBlobInterface;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
use LogicException;

class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements SignBlobInterface
{
Expand Down Expand Up @@ -78,20 +80,28 @@ public function __construct(
) {
if (is_string($jsonKey)) {
if (!file_exists($jsonKey)) {
throw new \InvalidArgumentException('file does not exist');
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');
throw new LogicException('invalid json for auth config');
}
}
if (!array_key_exists('service_account_impersonation_url', $jsonKey)) {
throw new \LogicException(
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');
throw new LogicException('json key is missing the source_credentials field');
}
if (!array_key_exists('type', $jsonKey['source_credentials'])) {
throw new InvalidArgumentException('json key source credentials are missing the type field');
}
if ($scope && $targetAudience) {
throw new InvalidArgumentException(
'Scope and targetAudience cannot both be supplied'
);
}

$this->targetScope = $scope ?? [];
Expand All @@ -103,6 +113,15 @@ public function __construct(
$this->serviceAccountImpersonationUrl
);

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 `cloud-platform`.
$scope = 'https://www.googleapis.com/auth/cloud-platform';
}

$this->sourceCredentials = $jsonKey['source_credentials'] instanceof FetchAuthTokenInterface
? $jsonKey['source_credentials']
: CredentialsLoader::makeCredentials($scope, $jsonKey['source_credentials']);
Expand Down
8 changes: 8 additions & 0 deletions tests/ApplicationDefaultCredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Google\Auth\Credentials\ExternalAccountCredentials;
use Google\Auth\Credentials\GCECredentials;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\Credentials\ImpersonatedServiceAccountCredentials;
use Google\Auth\CredentialsLoader;
use Google\Auth\CredentialSource;
use Google\Auth\GCECache;
Expand Down Expand Up @@ -509,6 +510,13 @@ public function testGetIdTokenCredentialsFailsIfNotOnGceAndNoDefaultFileFound()
$this->assertNotNull($creds);
}

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
187 changes: 146 additions & 41 deletions tests/Credentials/ImpersonatedServiceAccountCredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,50 +21,60 @@
use Google\Auth\Credentials\ImpersonatedServiceAccountCredentials;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\Credentials\UserRefreshCredentials;
use Google\Auth\Middleware\AuthTokenMiddleware;
use Google\Auth\OAuth2;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use LogicException;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use ReflectionClass;

class ImpersonatedServiceAccountCredentialsTest extends TestCase
{
private const SCOPE = ['scope/1', 'scope/2'];
private const TARGET_AUDIENCE = 'test-target-audience';

/**
* @dataProvider provideServiceAccountImpersonationJson
*/
public function testGetServiceAccountNameEmail(array $testJson)
public function testGetServiceAccountNameEmail()
{
$creds = new ImpersonatedServiceAccountCredentials(self::SCOPE, $testJson);
$json = $this->userToServiceAccountImpersonationJson;
$creds = new ImpersonatedServiceAccountCredentials(self::SCOPE, $json);
$this->assertEquals('[email protected]', $creds->getClientName());
}

/**
* @dataProvider provideServiceAccountImpersonationJson
*/
public function testGetServiceAccountNameID(array $testJson)
public function testGetServiceAccountNameID()
{
$testJson['service_account_impersonation_url'] = 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/1234567890987654321:generateAccessToken';
$creds = new ImpersonatedServiceAccountCredentials(self::SCOPE, $testJson);
$json = $this->userToServiceAccountImpersonationJson;
$json['service_account_impersonation_url'] = 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/1234567890987654321:generateAccessToken';
$creds = new ImpersonatedServiceAccountCredentials(self::SCOPE, $json);
$this->assertEquals('1234567890987654321', $creds->getClientName());
}

/**
* @dataProvider provideServiceAccountImpersonationJson
*/
public function testErrorCredentials(array $testJson)
public function testMissingImpersonationUriThrowsException()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('json key is missing the service_account_impersonation_url field');

new ImpersonatedServiceAccountCredentials(self::SCOPE, $testJson['source_credentials']);
new ImpersonatedServiceAccountCredentials(self::SCOPE, []);
}

public function testMissingSourceCredentialTypeThrowsException()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('json key source credentials are missing the type field');

new ImpersonatedServiceAccountCredentials(self::SCOPE, [
'service_account_impersonation_url' => 'https//google.com',
'source_credentials' => []
]);
}

/**
* @dataProvider provideServiceAccountImpersonationJson
*/
public function testSourceCredentialsFromJsonFiles(array $testJson, string $credClass)
public function testSourceCredentialsFromJsonFiles(array $json, string $credClass)
{
$creds = new ImpersonatedServiceAccountCredentials(['scope/1', 'scope/2'], $testJson);
$creds = new ImpersonatedServiceAccountCredentials(['scope/1', 'scope/2'], $json);

$sourceCredentialsProperty = (new ReflectionClass($creds))->getProperty('sourceCredentials');
$sourceCredentialsProperty->setAccessible(true);
Expand All @@ -74,38 +84,133 @@ public function testSourceCredentialsFromJsonFiles(array $testJson, string $cred
public function provideServiceAccountImpersonationJson()
{
return [
[$this->createUserISACTestJson(), UserRefreshCredentials::class],
[$this->createSAISACTestJson(), ServiceAccountCredentials::class],
[$this->userToServiceAccountImpersonationJson, UserRefreshCredentials::class],
[$this->serviceAccountToServiceAccountImpersonationJson, ServiceAccountCredentials::class],
];
}

// Creates a standard JSON auth object for testing.
private function createUserISACTestJson()
/**
* @dataProvider provideServiceAccountImpersonationIdTokenJson
*/
public function testGetIdTokenWithServiceAccountImpersonationCredentials($json, $grantType)
{
return [
'type' => 'impersonated_service_account',
'service_account_impersonation_url' => 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken',
'source_credentials' => [
'client_id' => 'client123',
'client_secret' => 'clientSecret123',
'refresh_token' => 'refreshToken123',
'type' => 'authorized_user',
]
];
$creds = new ImpersonatedServiceAccountCredentials(null, $json, self::TARGET_AUDIENCE);

$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());
$body = (string) $request->getBody();
parse_str($body, $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-id-token']
})
);
};

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

// Creates a standard JSON auth object for testing.
private function createSAISACTestJson()
public function provideServiceAccountImpersonationIdTokenJson()
{
return [
'type' => 'impersonated_service_account',
'service_account_impersonation_url' => 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken',
'source_credentials' => [
'client_email' => '[email protected]',
'private_key' => 'privatekey123',
'type' => 'service_account',
]
[$this->userToServiceAccountImpersonationIdTokenJson, 'refresh_token'],
[$this->serviceAccountToServiceAccountImpersonationIdTokenJson, OAuth2::JWT_URN],
];
}

public function testIdTokenWithAuthTokenMiddleware()
{
$targetAudience = 'test-target-audience';
$json = $this->userToServiceAccountImpersonationIdTokenJson;
$credentials = new ImpersonatedServiceAccountCredentials(null, $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 array $userToServiceAccountImpersonationJson = [
'type' => 'impersonated_service_account',
'service_account_impersonation_url' => 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken',
'source_credentials' => [
'client_id' => 'client123',
'client_secret' => 'clientSecret123',
'refresh_token' => 'refreshToken123',
'type' => 'authorized_user',
]
];

// Service Account to Service Account Impersonation JSON Credentials
private array $serviceAccountToServiceAccountImpersonationJson = [
'type' => 'impersonated_service_account',
'service_account_impersonation_url' => 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken',
'source_credentials' => [
'client_email' => '[email protected]',
'private_key' => 'privatekey123',
'type' => 'service_account',
]
];

// User Refresh to Service Account Impersonation ID Token JSON Credentials
// NOTE: The only difference is the use of "generateIdToken" instead of
// "generateAccessToken" in the service_account_impersonation_url
private array $userToServiceAccountImpersonationIdTokenJson = [
'type' => 'impersonated_service_account',
'service_account_impersonation_url' => 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateIdToken',
'source_credentials' => [
'client_id' => 'client123',
'client_secret' => 'clientSecret123',
'refresh_token' => 'refreshToken123',
'type' => 'authorized_user',
]
];

// Service Account to Service Account Impersonation ID Token JSON Credentials
// NOTE: The only difference is the use of "generateIdToken" instead of
// "generateAccessToken" in the service_account_impersonation_url
private array $serviceAccountToServiceAccountImpersonationIdTokenJson = [
'type' => 'impersonated_service_account',
'service_account_impersonation_url' => 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateIdToken',
'source_credentials' => [
'client_email' => '[email protected]',
'private_key' => "-----BEGIN RSA PRIVATE KEY-----\nMIICWgIBAAKBgGhw1WMos5gp2YjV7+fNwXN1tI4/DFXKzwY6TDWsPxkbyfjHgunX\n/sijlnJt3Qs1gBxiwEEjzFFlp39O3/gEbIoYWHR/4sZdqNRFzbhJcTpnUvRlZDBL\nE5h8f5uu4aL4D32WyiELF/vpr533lZCBwWsnN3zIYJxThgRF9i/R7F8tAgMBAAEC\ngYAgUyv4cNSFOA64J18FY82IKtojXKg4tXi1+L01r4YoA03TzgxazBtzhg4+hHpx\nybFJF9dhUe8fElNxN7xiSxw8i5MnfPl+piwbfoENhgrzU0/N14AV/4Pq+WAJQe2M\nxPcI1DPYMEwGjX2PmxqnkC47MyR9agX21YZVc9rpRCgPgQJBALodH492I0ydvEUs\ngT+3DkNqoWx3O3vut7a0+6k+RkM1Yu+hGI8RQDCGwcGhQlOpqJkYGsVegZbxT+AF\nvvIFrIUCQQCPqJbRalHK/QnVj4uovj6JvjTkqFSugfztB4Zm/BPT2eEpjLt+851d\nIJ4brK/HVkQT2zk9eb0YzIBfeQi9WpyJAkB9+BRSf72or+KsV1EsFPScgOG9jn4+\nhfbmvVzQ0ouwFcRfOQRsYVq2/Z7LNiC0i9LHvF7yU+MWjUJo+LqjCWAZAkBHearo\nMIzXgQRGlC/5WgZFhDRO3A2d8aDE0eymCp9W1V24zYNwC4dtEVB5Fncyp5Ihiv40\nvwA9eWoZll+pzo55AkBMMdk95skWeaRv8T0G1duv5VQ7q4us2S2TKbEbC8j83BTP\nNefc3KEugylyAjx24ydxARZXznPi1SFeYVx1KCMZ\n-----END RSA PRIVATE KEY-----\n",
'type' => 'service_account',
]
];
}
2 changes: 2 additions & 0 deletions tests/FetchAuthTokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ public function provideMakeHttpClient()
{
return [
['Google\Auth\Credentials\AppIdentityCredentials'],
['Google\Auth\Credentials\ExternalAccountCredentials'],
['Google\Auth\Credentials\GCECredentials'],
['Google\Auth\Credentials\ImpersonatedServiceAccountCredentials'],
['Google\Auth\Credentials\ServiceAccountCredentials'],
['Google\Auth\Credentials\ServiceAccountJwtAccessCredentials'],
['Google\Auth\Credentials\UserRefreshCredentials'],
Expand Down

0 comments on commit 20f2c56

Please sign in to comment.