From 1bbcabb6eee13909eecb4c7ebdce874843ad2b6a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 16 Jun 2020 12:57:40 -0700 Subject: [PATCH] feat!: v2.0 --- .github/actions/unittest/entrypoint.sh | 19 - .github/actions/unittest/retry.php | 24 - .github/workflows/tests.yml | 72 +- .gitignore | 1 + CHANGELOG.md | 4 +- README.md | 92 +- UPGRADING.md | 774 ++++++++++++++ composer.json | 15 +- src/AccessToken.php | 479 --------- src/ApplicationDefaultCredentials.php | 308 ------ .../Credentials/AnonymousCredentials.php} | 32 +- src/Auth/Credentials/ComputeCredentials.php | 430 ++++++++ .../Credentials/CredentialsInterface.php} | 40 +- src/Auth/Credentials/CredentialsTrait.php | 272 +++++ src/Auth/Credentials/OAuth2Credentials.php | 96 ++ .../Credentials/ServiceAccountCredentials.php | 238 +++-- .../ServiceAccountJwtAccessCredentials.php | 171 +-- .../Credentials/UserRefreshCredentials.php | 89 +- src/Auth/GoogleAuth.php | 477 +++++++++ src/Auth/Http/ApiKeyClient.php | 68 ++ src/Auth/Http/ClientFactory.php | 44 + src/Auth/Http/CredentialsClient.php | 60 ++ src/Auth/Jwt/FirebaseJwtClient.php | 54 + .../Jwt/JwtClientInterface.php} | 26 +- src/{ => Auth}/OAuth2.php | 479 ++++----- .../SignBlob/PrivateKeySignBlobTrait.php} | 40 +- .../ServiceAccountApiSignBlobTrait.php} | 53 +- src/{ => Auth/SignBlob}/SignBlobInterface.php | 17 +- src/Cache/InvalidArgumentException.php | 4 +- src/Cache/Item.php | 6 +- src/Cache/MemoryCacheItemPool.php | 6 +- src/Cache/SysVCacheItemPool.php | 15 +- src/CacheTrait.php | 83 -- src/Credentials/AppIdentityCredentials.php | 230 ---- src/Credentials/GCECredentials.php | 542 ---------- src/Credentials/IAMCredentials.php | 91 -- src/CredentialsLoader.php | 250 ----- src/FetchAuthTokenCache.php | 263 ----- src/GCECache.php | 92 -- src/GetQuotaProjectInterface.php | 33 - src/Http/Client/GuzzleClient.php | 87 ++ src/Http/Client/Psr18Client.php | 73 ++ src/Http/ClientInterface.php | 52 + src/Http/Promise/GuzzlePromise.php | 133 +++ src/Http/PromiseInterface.php | 105 ++ src/HttpHandler/Guzzle5HttpHandler.php | 126 --- src/HttpHandler/Guzzle6HttpHandler.php | 62 -- src/HttpHandler/Guzzle7HttpHandler.php | 21 - src/HttpHandler/HttpClientCache.php | 54 - src/HttpHandler/HttpHandlerFactory.php | 53 - src/Middleware/AuthTokenMiddleware.php | 148 --- .../ScopedAccessTokenMiddleware.php | 175 ---- src/Middleware/SimpleMiddleware.php | 92 -- src/Subscriber/AuthTokenSubscriber.php | 136 --- .../ScopedAccessTokenSubscriber.php | 180 ---- src/Subscriber/SimpleSubscriber.php | 93 -- src/UpdateMetadataInterface.php | 41 - tests/AccessTokenTest.php | 537 ---------- tests/ApplicationDefaultCredentialsTest.php | 858 --------------- .../Credentials/AnonymousCredentialsTest.php | 57 + .../Credentials/ComputeCredentialsTest.php | 419 ++++++++ .../Auth/Credentials/CredentialsTraitTest.php | 315 ++++++ .../Credentials/OAuth2CredentialsTest.php | 87 ++ .../ServiceAccountCredentialsTest.php | 333 ++++++ ...ServiceAccountJwtAccessCredentialsTest.php | 175 ++++ .../UserRefreshCredentialsTest.php | 193 ++++ tests/Auth/CredentialsTest.php | 321 ++++++ tests/Auth/GoogleAuthTest.php | 985 ++++++++++++++++++ tests/Auth/Http/ApiKeyClientTest.php | 67 ++ tests/Auth/Http/ClientFactoryTest.php | 44 + tests/Auth/Http/CredentialsClientTest.php | 84 ++ tests/Auth/Jwt/FirebaseJwtClientTest.php | 147 +++ tests/{ => Auth}/OAuth2Test.php | 634 +++++------ .../SignBlob/PrivateKeySignBlobTraitTest.php} | 42 +- .../ServiceAccountApiSignBlobTraitTest.php} | 56 +- .../fixtures/client_credentials.json} | 2 +- .../{ => Auth}/fixtures/federated-certs.json | 0 .../application_default_credentials.json | 0 .../application_default_credentials.json | 0 tests/{ => Auth}/fixtures/private.json | 0 tests/{ => Auth}/fixtures/private.pem | 0 tests/{ => Auth}/fixtures/public.pem | 0 tests/BaseTest.php | 58 -- tests/Cache/ItemTest.php | 4 +- tests/Cache/MemoryCacheItemPoolTest.php | 22 +- tests/Cache/SysVCacheItemPoolTest.php | 12 +- tests/Cache/sysv_cache_creator.php | 6 +- tests/CacheTraitTest.php | 194 ---- .../AppIdentityCredentialsTest.php | 246 ----- tests/Credentials/GCECredentialsTest.php | 469 --------- tests/Credentials/IAMCredentialsTest.php | 90 -- tests/Credentials/InsecureCredentialsTest.php | 46 - .../ServiceAccountCredentialsTest.php | 770 -------------- .../UserRefreshCredentialsTest.php | 280 ----- tests/CredentialsLoaderTest.php | 50 - tests/FetchAuthTokenCacheTest.php | 505 --------- tests/FetchAuthTokenTest.php | 237 ----- tests/GCECacheTest.php | 161 --- tests/HttpHandler/Guzzle5HttpHandlerTest.php | 240 ----- tests/HttpHandler/Guzzle6HttpHandlerTest.php | 68 -- tests/HttpHandler/Guzzle7HttpHandlerTest.php | 34 - tests/HttpHandler/HttpHandlerFactoryTest.php | 52 - tests/Middleware/AuthTokenMiddlewareTest.php | 350 ------- .../ScopedAccessTokenMiddlewareTest.php | 221 ---- tests/Middleware/SimpleMiddlewareTest.php | 39 - tests/Subscriber/AuthTokenSubscriberTest.php | 335 ------ .../ScopedAccessTokenSubscriberTest.php | 256 ----- tests/Subscriber/SimpleSubscriberTest.php | 76 -- tests/bootstrap.php | 58 +- tests/fixtures2/gcloud.json | 6 - tests/fixtures2/valid_oauth_creds.json | 6 - tests/fixtures3/key.pub | 6 - .../service_account_credentials.json | 5 - tests/mocks/AppIdentityService.php | 38 - 114 files changed, 6955 insertions(+), 11061 deletions(-) delete mode 100755 .github/actions/unittest/entrypoint.sh delete mode 100644 .github/actions/unittest/retry.php create mode 100644 UPGRADING.md delete mode 100644 src/AccessToken.php delete mode 100644 src/ApplicationDefaultCredentials.php rename src/{Credentials/InsecureCredentials.php => Auth/Credentials/AnonymousCredentials.php} (62%) create mode 100644 src/Auth/Credentials/ComputeCredentials.php rename src/{FetchAuthTokenInterface.php => Auth/Credentials/CredentialsInterface.php} (50%) create mode 100644 src/Auth/Credentials/CredentialsTrait.php create mode 100644 src/Auth/Credentials/OAuth2Credentials.php rename src/{ => Auth}/Credentials/ServiceAccountCredentials.php (50%) rename src/{ => Auth}/Credentials/ServiceAccountJwtAccessCredentials.php (52%) rename src/{ => Auth}/Credentials/UserRefreshCredentials.php (61%) create mode 100644 src/Auth/GoogleAuth.php create mode 100644 src/Auth/Http/ApiKeyClient.php create mode 100644 src/Auth/Http/ClientFactory.php create mode 100644 src/Auth/Http/CredentialsClient.php create mode 100644 src/Auth/Jwt/FirebaseJwtClient.php rename src/{ProjectIdProviderInterface.php => Auth/Jwt/JwtClientInterface.php} (62%) rename src/{ => Auth}/OAuth2.php (78%) rename src/{ServiceAccountSignerTrait.php => Auth/SignBlob/PrivateKeySignBlobTrait.php} (51%) rename src/{Iam.php => Auth/SignBlob/ServiceAccountApiSignBlobTrait.php} (65%) rename src/{ => Auth/SignBlob}/SignBlobInterface.php (60%) delete mode 100644 src/CacheTrait.php delete mode 100644 src/Credentials/AppIdentityCredentials.php delete mode 100644 src/Credentials/GCECredentials.php delete mode 100644 src/Credentials/IAMCredentials.php delete mode 100644 src/CredentialsLoader.php delete mode 100644 src/FetchAuthTokenCache.php delete mode 100644 src/GCECache.php delete mode 100644 src/GetQuotaProjectInterface.php create mode 100644 src/Http/Client/GuzzleClient.php create mode 100644 src/Http/Client/Psr18Client.php create mode 100644 src/Http/ClientInterface.php create mode 100644 src/Http/Promise/GuzzlePromise.php create mode 100644 src/Http/PromiseInterface.php delete mode 100644 src/HttpHandler/Guzzle5HttpHandler.php delete mode 100644 src/HttpHandler/Guzzle6HttpHandler.php delete mode 100644 src/HttpHandler/Guzzle7HttpHandler.php delete mode 100644 src/HttpHandler/HttpClientCache.php delete mode 100644 src/HttpHandler/HttpHandlerFactory.php delete mode 100644 src/Middleware/AuthTokenMiddleware.php delete mode 100644 src/Middleware/ScopedAccessTokenMiddleware.php delete mode 100644 src/Middleware/SimpleMiddleware.php delete mode 100644 src/Subscriber/AuthTokenSubscriber.php delete mode 100644 src/Subscriber/ScopedAccessTokenSubscriber.php delete mode 100644 src/Subscriber/SimpleSubscriber.php delete mode 100644 src/UpdateMetadataInterface.php delete mode 100644 tests/AccessTokenTest.php delete mode 100644 tests/ApplicationDefaultCredentialsTest.php create mode 100644 tests/Auth/Credentials/AnonymousCredentialsTest.php create mode 100644 tests/Auth/Credentials/ComputeCredentialsTest.php create mode 100644 tests/Auth/Credentials/CredentialsTraitTest.php create mode 100644 tests/Auth/Credentials/OAuth2CredentialsTest.php create mode 100644 tests/Auth/Credentials/ServiceAccountCredentialsTest.php create mode 100644 tests/Auth/Credentials/ServiceAccountJwtAccessCredentialsTest.php create mode 100644 tests/Auth/Credentials/UserRefreshCredentialsTest.php create mode 100644 tests/Auth/CredentialsTest.php create mode 100644 tests/Auth/GoogleAuthTest.php create mode 100644 tests/Auth/Http/ApiKeyClientTest.php create mode 100644 tests/Auth/Http/ClientFactoryTest.php create mode 100644 tests/Auth/Http/CredentialsClientTest.php create mode 100644 tests/Auth/Jwt/FirebaseJwtClientTest.php rename tests/{ => Auth}/OAuth2Test.php (64%) rename tests/{ServiceAccountSignerTraitTest.php => Auth/SignBlob/PrivateKeySignBlobTraitTest.php} (56%) rename tests/{IamTest.php => Auth/SignBlob/ServiceAccountApiSignBlobTraitTest.php} (66%) rename tests/{fixtures2/private.json => Auth/fixtures/client_credentials.json} (98%) rename tests/{ => Auth}/fixtures/federated-certs.json (100%) rename tests/{fixtures => Auth/fixtures/gcloud1}/.config/gcloud/application_default_credentials.json (100%) rename tests/{fixtures2 => Auth/fixtures/gcloud2}/.config/gcloud/application_default_credentials.json (100%) rename tests/{ => Auth}/fixtures/private.json (100%) rename tests/{ => Auth}/fixtures/private.pem (100%) rename tests/{ => Auth}/fixtures/public.pem (100%) delete mode 100644 tests/BaseTest.php delete mode 100644 tests/CacheTraitTest.php delete mode 100644 tests/Credentials/AppIdentityCredentialsTest.php delete mode 100644 tests/Credentials/GCECredentialsTest.php delete mode 100644 tests/Credentials/IAMCredentialsTest.php delete mode 100644 tests/Credentials/InsecureCredentialsTest.php delete mode 100644 tests/Credentials/ServiceAccountCredentialsTest.php delete mode 100644 tests/Credentials/UserRefreshCredentialsTest.php delete mode 100644 tests/CredentialsLoaderTest.php delete mode 100644 tests/FetchAuthTokenCacheTest.php delete mode 100644 tests/FetchAuthTokenTest.php delete mode 100644 tests/GCECacheTest.php delete mode 100644 tests/HttpHandler/Guzzle5HttpHandlerTest.php delete mode 100644 tests/HttpHandler/Guzzle6HttpHandlerTest.php delete mode 100644 tests/HttpHandler/Guzzle7HttpHandlerTest.php delete mode 100644 tests/HttpHandler/HttpHandlerFactoryTest.php delete mode 100644 tests/Middleware/AuthTokenMiddlewareTest.php delete mode 100644 tests/Middleware/ScopedAccessTokenMiddlewareTest.php delete mode 100644 tests/Middleware/SimpleMiddlewareTest.php delete mode 100644 tests/Subscriber/AuthTokenSubscriberTest.php delete mode 100644 tests/Subscriber/ScopedAccessTokenSubscriberTest.php delete mode 100644 tests/Subscriber/SimpleSubscriberTest.php delete mode 100644 tests/fixtures2/gcloud.json delete mode 100644 tests/fixtures2/valid_oauth_creds.json delete mode 100644 tests/fixtures3/key.pub delete mode 100644 tests/fixtures3/service_account_credentials.json delete mode 100644 tests/mocks/AppIdentityService.php diff --git a/.github/actions/unittest/entrypoint.sh b/.github/actions/unittest/entrypoint.sh deleted file mode 100755 index a300c131c..000000000 --- a/.github/actions/unittest/entrypoint.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -l - -apt-get update && \ -apt-get install -y --no-install-recommends \ - git \ - zip \ - curl \ - unzip \ - wget - -curl --silent --show-error https://getcomposer.org/installer | php -php composer.phar self-update - -echo "---Installing dependencies ---" -echo "${composerargs}" -php $(dirname $0)/retry.php "php composer.phar update $composerargs" - -echo "---Running unit tests ---" -vendor/bin/phpunit diff --git a/.github/actions/unittest/retry.php b/.github/actions/unittest/retry.php deleted file mode 100644 index c6525abe8..000000000 --- a/.github/actions/unittest/retry.php +++ /dev/null @@ -1,24 +0,0 @@ - 0) { - sleep($delay); - return retry($f, $delay, $retries - 1); - } else { - throw $e; - } - } -} - -retry(function () { - global $argv; - passthru($argv[1], $ret); - - if ($ret != 0) { - throw new \Exception('err'); - } -}, 1); diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 432bfde1f..87cdaf3c6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: operating-system: [ ubuntu-latest ] - php: [ "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0" ] + php: [ 7.1, 7.2, 7.3, 7.4, "8.0" ] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 @@ -42,7 +42,7 @@ jobs: strategy: matrix: operating-system: [ ubuntu-latest ] - php: [ "5.6", "7.0", "7.1", "7.2" ] + php: [ 7.1 ] name: PHP ${{matrix.php }} Unit Test Prefer Lowest steps: - uses: actions/checkout@v2 @@ -58,72 +58,6 @@ jobs: command: composer update --prefer-lowest - name: Run Script run: vendor/bin/phpunit - guzzle6: - runs-on: ubuntu-latest - strategy: - matrix: - operating-system: [ ubuntu-latest ] - php: [ "5.6", "7.2" ] - name: PHP ${{ matrix.php }} Unit Test Guzzle 6 - steps: - - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - - name: Install Dependencies - uses: nick-invision/retry@v1 - with: - timeout_minutes: 10 - max_attempts: 3 - command: composer require guzzlehttp/guzzle:^6 && composer update - - name: Run Script - run: vendor/bin/phpunit - # use dockerfiles for oooooolllllldddd versions of php, setup-php times out for those. - test_php55: - name: "PHP 5.5 Unit Test" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Run Unit Tests - uses: docker://php:5.5-cli - with: - entrypoint: ./.github/actions/unittest/entrypoint.sh - test_php55_lowest: - name: "PHP 5.5 Unit Test Prefer Lowest" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Run Unit Tests - uses: docker://php:5.5-cli - env: - composerargs: "--prefer-lowest" - with: - entrypoint: ./.github/actions/unittest/entrypoint.sh - test_php54: - name: "PHP 5.4 Unit Test" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Run Unit Tests - uses: docker://php:5.4-cli - with: - entrypoint: ./.github/actions/unittest/entrypoint.sh - test_php54_lowest: - name: "PHP 5.4 Unit Test Prefer Lowest" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Run Unit Tests - uses: docker://php:5.4-cli - env: - composerargs: "--prefer-lowest" - with: - entrypoint: ./.github/actions/unittest/entrypoint.sh style: runs-on: ubuntu-latest name: PHP Style Check @@ -132,7 +66,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: "7.4" + php-version: 7.4 - name: Install Dependencies uses: nick-invision/retry@v1 with: diff --git a/.gitignore b/.gitignore index 91b769cf9..ea909eb1a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ composer.lock .cache .docs .gitmodules +.phpunit.result.cache # IntelliJ .idea diff --git a/CHANGELOG.md b/CHANGELOG.md index e9d258ecd..a97d4a22b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,8 +91,8 @@ ## 1.5.1 (04/16/2019) -* [fix] Moved `getClientName()` from `Google\Auth\FetchAuthTokenInterface` - to `Google\Auth\SignBlobInterface`, and removed `getClientName()` from +* [fix] Moved `getClientEmail()` from `Google\Auth\FetchAuthTokenInterface` + to `Google\Auth\SignBlobInterface`, and removed `getClientEmail()` from `InsecureCredentials` and `UserRefreshCredentials`. (#223) ## 1.5.0 (04/15/2019) diff --git a/README.md b/README.md index 37f335a14..ea0cf8f60 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,9 @@
Homepage
http://www.github.com/google/google-auth-library-php
Reference Docs
https://googleapis.github.io/google-auth-library-php/master/
Authors
-
Tim Emiola
-
Stanley Cheung
Brent Shaffer
-
Copyright
Copyright © 2015 Google, Inc.
+
David Supplee
+
Copyright
Copyright © 2020 Google LLC
License
Apache 2.0
@@ -19,17 +18,10 @@ authorization and authentication with Google APIs. ### Installing via Composer The recommended way to install the google auth library is through -[Composer](http://getcomposer.org). +[Composer](http://getcomposer.org). Run the Composer command to install the latest stable version: ```bash -# Install Composer -curl -sS https://getcomposer.org/installer | php -``` - -Next, run the Composer command to install the latest stable version: - -```bash -composer.phar require google/auth +composer require google/auth ``` ## Application Default Credentials @@ -78,9 +70,8 @@ As long as you update the environment variable below to point to *your* JSON credentials file, the following code should output a list of your Drive files. ```php -use Google\Auth\ApplicationDefaultCredentials; -use GuzzleHttp\Client; -use GuzzleHttp\HandlerStack; +use Google\Auth\GoogleAuth; +use GuzzleHttp\Psr7\Request; // specify the path to your application credentials putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json'); @@ -88,52 +79,29 @@ putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json'); // define the scopes for your API call $scopes = ['https://www.googleapis.com/auth/drive.readonly']; -// create middleware -$middleware = ApplicationDefaultCredentials::getMiddleware($scopes); -$stack = HandlerStack::create(); -$stack->push($middleware); +// create the auth client +$auth = new GoogleAuth(); -// create the HTTP client -$client = new Client([ - 'handler' => $stack, - 'base_uri' => 'https://www.googleapis.com', - 'auth' => 'google_auth' // authorize all requests -]); +// authorize an http client +$client = $auth->makeHttpClient(['scope' => $scope]); // make the request -$response = $client->get('drive/v2/files'); +$request = new Request('GET', 'https://www.googleapis.com/drive/v2/files'); +$response = $client->send($request); // show the result! print_r((string) $response->getBody()); ``` -##### Guzzle 5 Compatibility - -If you are using [Guzzle 5][Guzzle 5], replace the `create middleware` and -`create the HTTP Client` steps with the following: - -```php -// create the HTTP client -$client = new Client([ - 'base_url' => 'https://www.googleapis.com', - 'auth' => 'google_auth' // authorize all requests -]); - -// create subscriber -$subscriber = ApplicationDefaultCredentials::getSubscriber($scopes); -$client->getEmitter()->attach($subscriber); -``` - #### Call using an ID Token If your application is running behind Cloud Run, or using Cloud Identity-Aware Proxy (IAP), you will need to fetch an ID token to access your application. For this, use the static method `getIdTokenMiddleware` on -`ApplicationDefaultCredentials`. +`GoogleAuth`. ```php -use Google\Auth\ApplicationDefaultCredentials; -use GuzzleHttp\Client; -use GuzzleHttp\HandlerStack; +use Google\Auth\GoogleAuth; +use GuzzleHttp\Psr7\Request; // specify the path to your application credentials putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json'); @@ -144,21 +112,15 @@ putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json'); // $targetAudience = 'https://service-1234-uc.a.run.app'; $targetAudience = 'YOUR_ID_TOKEN_AUDIENCE'; -// create middleware -$middleware = ApplicationDefaultCredentials::getIdTokenMiddleware($targetAudience); -$stack = HandlerStack::create(); -$stack->push($middleware); - -// create the HTTP client -$client = new Client([ - 'handler' => $stack, - 'auth' => 'google_auth', - // Cloud Run, IAP, or custom resource URL - 'base_uri' => 'https://YOUR_PROTECTED_RESOURCE', -]); +// create the auth client +$auth = new GoogleAuth(); + +// authorize an http client +$client = $auth->makeHttpClient(['targetAudience' => $targetAudience]); // make the request -$response = $client->get('/'); +$request = new Request('GET', 'https://YOUR_PROTECTED_RESOURCE'); +$response = $client->send($request); // show the result! print_r((string) $response->getBody()); @@ -178,9 +140,9 @@ If you are [using Google ID tokens to authenticate users][google-id-tokens], use the `Google\Auth\AccessToken` class to verify the ID token: ```php -use Google\Auth\AccessToken; +use Google\Auth\GoogleAuth; -$auth = new AccessToken(); +$auth = new GoogleAuth(); $auth->verify($idToken); ``` @@ -190,11 +152,11 @@ appropriate certificate URL for IAP. This is because IAP signs the ID tokens with a different key than the Google Identity service: ```php -use Google\Auth\AccessToken; +use Google\Auth\GoogleAuth; -$auth = new AccessToken(); +$auth = new GoogleAuth(); $auth->verify($idToken, [ - 'certsLocation' => AccessToken::IAP_CERT_URL + 'certsLocation' => GoogleAuth::IAP_CERT_URL ]); ``` diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 000000000..9d93506f7 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,774 @@ +Google Auth Upgrade Guide +========================= + +1.0 to 2.0 +---------- + +In order to take advantage of the new features of PHP, Google Auth dropped +support for PHP 7.0 and below. The minimum supported PHP version is now PHP 7.1. +Type hints and return types for functions and methods have been added wherever +possible. + +### Improvements! + +#### PHP Language Features (7.1) + +* [Return types](https://wiki.php.net/rfc/return_types) for all functions +* [Scalar types](https://www.tutorialspoint.com/php7/php7_scalartype_declarations.htm) for scalar function arguments +* [Strict typing](https://www.php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration.strict) via `declare(strict_types=1)` +* private constants + +#### Improved Caching + +* Implements caching in credentials in `CacheTrait` instead of using the + `CredentialsCache` wrapper: + +```php +$auth = new GoogleClient(); +$credentials = $auth->makeCredentials([ + 'cache' => new MemoryCachePool, + 'cacheLifetime' => 1500, // Could potentially be "cacheOptions.lifetime" +]); +``` +* Implement [in-memory cache](https://github.com/googleapis/google-auth-library-php/tree/master/src/Cache) by default +* **TODO**: Fix [SysVCachePool race condition](https://github.com/googleapis/google-auth-library-php/issues/226) +* Better Cache Keys + * Different Auth Token types don't overwrite each other (ID tokens vs Access Token) + * Ensures unique cache keys for different credentials / scopes / etc +* Better Token Expiration + * Sets cache expiry based on token expiry when possible + * **TODO**: + * Verify token is not expired before using it + * Fix bug where token expiration is never checked ([b/149049606](http://b/149049606)) + * Set expiration for ID token / JWT / self-signed + * Automatic retry for token expiration API exception + + +#### Improved HTTP handling + +* Provides an abstraction from Guzzle HTTP Client + * Using the composer "[replace](https://stackoverflow.com/questions/18882201/how-does-the-replace-property-work-with-composer)" keyword, users can ignore sub-dependencies such as Guzzle in favor of a separate HTTP library +* Replaces Middleware classes with `CredentialsClient` and `ApiKeyClient` classes +* Adds `Google\Http\ClientInterface` and `Google\Http\PromiseInterface` for + vendor abstraction. +* Adds `Google\Http\Client\GuzzleClient`, `Google\Http\Promise\GuzzlePromise` + and `Google\Http\Client\Psr18Client` implementations. +* Uses `Guzzle` implementations by default. +* **TODO**: Consider wrapping Exceptions + +**Example** + +```php +// By default, Guzzle is used. A custom guzzle client can be passed in: +$guzzleConfig = [ /* some custom config */ ]; +$guzzle = new GuzzleHttp\Client($guzzleConfig); +$httpClient = new Google\Http\Client\GuzzleClient($guzzle); +$auth = new GoogleAuth(['httpClient' => $httpClient]); +``` + +```php +// To use a different HTTP library or create your own: +$httpClient = new class implements Google\Http\ClientInterface { + public function send( + RequestInterface $request, + array $options = [] + ) : ResponseInterface { + // Send method + } + + public function sendAsync( + RequestInterface $request, + array $options = [] + ) : PromiseInterface { + // Send Async method + } +}; +$googleAuth = new GoogleAuth(['httpClient' => $httpClient]); +$googleAuth->verify($someJwt); +``` + +#### Improved JWT handling + +* Provides an abstraction from `firebase/jwt` via `JwtClientInterface` +* Removed dependencies on `phpseclib/phpseclib`, and `kelvinmo/simplejwt` + * Using the composer "[replace](https://stackoverflow.com/questions/18882201/how-does-the-replace-property-work-with-composer)" keyword, users can ignore sub-dependencies such as Firebase JWT in favor of a separate JWT library +* **TODO**: Consider wrapping Exceptions + +**Example** + + +```php +// by default, "firebase/php-jwt` is ued via `FirebaseJwtClient` +$googleAuth = new GoogleAuth(); +$googleAuth->verify($someJwt); +``` + +```php +// To use a different library or create your own: +$jwt = new class implements Google\Auth\Jwt\JwtClientInterface { + public function encode( + array $payload, + string $signingKey, + string $signingAlg, + ?string $keyId + ): string { + // encode method + } + + public function decode(string $jwt, array $keys, array $allowedAlgs): array + { + // decode method + } + + public function parseKeySet(array $keySey): array + { + // parseKeySet method + } +}; +$googleAuth = new GoogleAuth(['jwtClient' => $jwt]); +$googleAuth->verify($someJwt); +``` + +#### New `GoogleAuth` class + +`GoogleAuth` replaces `ApplicationDefaultCredentials`, and provides a +centralized, single entrypoint to the auth library. It has the following +methods: + +```php +namespace Google\Auth; + +use Google\Auth\Credentials\CredentialsInterface; + +class GoogleAuth +{ + public function makeCredentials(array $options = []): CredentialsInterface; + public function onCompute(array $options = []): bool; + public function verify(string $token, array $options = []): bool; +} +``` + +The new `GoogleAuth` class does the following: + +* Returns Application Default Credentials for the environment. +* Uses options array instead of list of arguments in method signature. +* Consolidates HTTP Handler and Caching options in method signatures in favor + of class constructor config. +* Removes static methods and public constants. + +**Examples** + +```php +// create auth client +$googleAuth = new Google\Auth\GoogleAuth([ + 'scope' => 'https://www.googleapis.com/auth/drive.readonly', +]); + +// create an authorized client using Application Default Credentials +$credentials = $googleAuth->makeCredentials(); +$authHttp = new Google\Auth\Http\CredentialsClient($credentials); +$response = $authHttp->send(new Psr\Http\Message\Request('GET', '/')); // or sendAsync +``` + +```php +// create auth client +$googleAuth = new Google\Auth\GoogleAuth([ + 'scope' => 'https://www.googleapis.com/auth/drive.readonly', +]); + +// create an authorized client from an existing Guzzle client +$guzzle = new GuzzleHttp\Client(); +$authHttp = new CredentialsClient( + $credentials, + new Google\Http\Guzzle6Client($guzzle) +); + +// make the request +$response = $authHttp->send(new Psr\Http\Message\Request('GET', '/')); // or sendAsync +``` + +**Example: Metadata** + +```php +// 2.0 implementation +$auth = new GoogleAuth(); +if ($auth->onCompute()) { + // ... +} + +// 1.0 implementation: +// GCECredentials::onGce($httpHandler = null); +``` + +#### New `CredentialsInterface` and `CredentialsTrait` to replace `CredentialsLoader` + +* Uses options array instead of list of arguments in method signature +* Renames `updateMetadata` to `getRequestMetadata` + * An array of headers is returned instead of updating an existing array +* Removes **tokenCallback** + * Anything done here should be doable with the HttpHandler + * Proper caching makes this unnecessary +* Removes **getUpdateMetadataFunc** +* Removes **makeInsecureCredentials** +* `CredentialsTrait` is marked `@internal` + + +```php +namespace Google\Auth\Credentials; + +use Google\Http\ClientInterface; + +/** + * An interface implemented by objects that can fetch auth tokens. + */ +interface CredentialsInterface +{ + public function fetchAuthToken(): array; + public function getRequestMetadata(): array; + public function getProjectId(): ?string; + public function getQuotaProject(): ?string; +} +``` + +#### Improved ID Token auth + +* The `AccessToken` class has been combined with `OAuth2` and `GoogleAuth` + * `verify` and cert fetching functions are in `GoogleAuth` + * `revoke` function is in the `OAuth2` class +* Removed `SimpleJWT ` and `phpseclib` dependencies in favor of `openssl` extension +* Validates options and throws error if `targetAudience` is supplied to credentials which do not support ID token auth +* **TODO:** Should we make fetching IAP certs or OIDC certs implicit? Right now, the user has to specify the IAP cert URL. Other languages do this. The only downside is we must inspect the JWT header before verifying to determine the algorithm. + +**ID token verify** + +```php +$googleAuth = new GoogleAuth(); +$googleAuth->verify($idToken); +``` + +**ID token auth** + +```php +use Google\Auth\GoogleAuth; +use Psr\Http\Message\Request; + +// create auth client +$cloudRunUrl = 'https://cloud-run-url'; +$googleAuth = new GoogleAuth([ + 'targetAudience' => $cloudRunUrl, +]); + +// create an authorized HTTP client and send a request +// @throws InvalidArgumentException if credentials do not support ID Token auth +$authHttp = new Google\Auth\Http\CredentialsClient( + $googleAuth->makeCredentials() +); +$response = $authHttp->send(new Psr\Http\Message\Request('GET', $cloudRunUrl)); +``` + +#### SignBlob Implementation + +* New `SignBlobInterface` +* Falls back to calling [Service Account Credentials](https://cloud.google.com/iam/docs/reference/credentials/rest) API if `openssl` isn't installed. +* `ServiceAccountSignerTrait` has been renamed `PrivateKeySignBlobTrait` +* `IAM` class has been moved into a trait and renamed `ServiceAccountApiSignBlobTrait` +* `PrivateKeySignBlobTrait` and `ServiceAccountApiSignBlobTrait` are marked `@internal` + +#### Improved 3LO Support + +* Ensures refresh token is used when access token is expired +* Adds `OAuth2Credentials` class for wrapping the OAuth2 service +* Adds support for `credentialsFile` option on `OAuth2` +* `OAuth2::isExpired` now returns `true` when token expiration is null +* **TODO**: Consider adding caching to `OAuth2` +* **TODO**: Consider adding method `hasValidToken` + +```php +$oauth = new OAuth2( + 'credentialsFile' => '/path/to/client-credentials.json', + 'scope' => 'https://www.googleapis.com/auth/drive', +]); +``` + +**3LO example:** + +```php +use Google\Auth\OAuth2; +use Google\Auth\Credentials\OAuthCredentials; +use Google\Auth\Http\CredentialsClient; + +// Create the auth client +$oauth = new OAuth2([ + 'credentialsFile' => '/path/to/client-credentials.json', + 'scope' => 'https://www.googleapis.com/auth/drive', +]); + +$accessTokenFile = '/path/to/access-token.json'; +if (isset($_GET['code'])) + // If we have a code back from the OAuth 2.0 flow, exchange that for an access token. + file_put_contents( + $accessTokenFile, + json_encode($oauth->fetchAuthTokenWithCode($_GET['code'])) + ); +} + +if (file_exists($accessTokenFile)) { + // $accessToken has a refresh token because "access_type" is set to "offline" + $accessToken = json_decode(file_get_contents($accessTokenFile)); + $oauth->setToken($accessToken); +} else { + // Redirect the user back to this page after authorization + $redirectUri = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']; + $authUri = $oauth->buildFullAuthorizationUri(['redirect_uri' => $redirectUri]); + // Redirect the user to the authorization URI + header('Location: ' . $authUri); + return; +} + +// Make the call with the access token +$http = new CredentialsClient(new OAuthCredentials($oauth)); +$http->send(new Request('GET', 'https://www.googleapis.com/drive/v3/files')); +``` + +### Breaking Changes + +#### Class/Interface Renames + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ApplicationDefaultCredentials +

+ => GoogleAuth +

The classname "GoogleAuth" more clearly provides an entrypoint to the library. +

+Consistent with NodeJS. +

IAM +

+ => SignBlob\IamCredentialsSignBlobTrait +

More explicit name, "IAM" is too generic. +

+A trait is a better fit as it's a utility class. +

Credentials\InsecureCredentials +

+ => Credentials\AnonymousCredentials +

"Anonymous" is more consistent with our documentation. +
Credentials\GCECredentials +

+ => Credentials\ComputeCredentials +

"Compute" represents a suite of products (App Engine, Compute Engine, Cloud Functions, Cloud Run) which all have a metadata server, and so use these credentials. +

+Consistent with NodeJS. +

HttpHandler\HttpHanderFactory +

+ => Http\ClientFactory +

"HTTP Client" is more Idiomatic. +
ServiceAccountSignerTrait +

+ => SignBlob\PrivateKeySignBlobTrait +

More explicit. +

+Organized into subdirectory. +

FetchAuthTokenInterface +

+ => Credentials\CredentialsInterface +

More intuitive +
SignBlobInterface +

+ => SignBlob\SignBlobInterface +

Organized into subdirectory. +

+Consistency with SignBlob traits. +

+ +**TODO**: Consider marking some classes as `final` + +#### Class/Interface Removals + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AccessToken + refactored +

+into GoogleAuth (constants and verify method) and OAuth2 (revoke method) +

CacheTrait + refactored +

+into Credentials\CredentialsTrait +

CredentialsLoader + refactored +

+into GoogleAuth (makeCredentials method) and Credentials\CredentialsTrait (getRequestMetadata method). fromEnv and fromWellKnownFile have been removed. +

+See Method Removals +

FetchAuthTokenCache + not needed +

+Caching happens in CredentialsTrait +

+See Improved Caching +

GetQuotaProjectInterface + refactored +

+into Credentials\CredentialsInterface +

ProjectIdProviderInterface + refactored +

+into Credentials\CredentialsInterface +

Credentials\AppIdentityCredentials + obsolete +

+The php55 runtime is no longer supported +

Credentials\IAMCredentials + not needed +

+This class does not seem useful +

+

HttpHandler\Guzzle5HttpHandler + obsolete +

+Guzzle 5 is no longer supported +

HttpHandler\Guzzle6HttpHandler + refactored +

+into Google\Http\Client\GuzzleClient +

+See PHP HTTP +

HttpHandler\HttpClientCache + not needed +

+This class does not seem useful +

+

Middleware\AuthTokenMiddleware + replaced +

+See Http\CredentialsClient +

Middleware\ScopedAccessTokenMiddleware + not needed +

+This class does not seem useful +

+

Middleware\SimpleMiddleware + replaced +

+See Http\ApiKeyClient +

Subscriber\AuthTokenSubscriber + obsolete +

+Guzzle 5 is no longer supported +

Subscriber\ScopedAccessTokenSubscriber + obsolete +

+Guzzle 5 is no longer supported +

Subscriber\SimpleSubscriber + obsolete +

+Guzzle 5 is no longer supported +

+ + + +#### Method Removals + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ApplicationDefaultCredentials +

+:: getCredentials +

renamed +

+to GoogleAuth::makeCredentials +

ApplicationDefaultCredentials +

+:: getSubscriber +

obsolete +

+Guzzle 5 is no longer supported +

ApplicationDefaultCredentials +

+:: getMiddleware +

refactored +

+into GoogleAuth::makeCredentials and CredentialsClient +

ApplicationDefaultCredentials +

+:: getIdTokenMiddleware +

refactored +

+into GoogleAuth::makeCredentials and CredentialsClient +

ApplicationDefaultCredentials +

+:: getIdTokenCredentials +

refactored +

+into GoogleAuth::makeCredentials +

CredentialsLoader +

+:: makeInsecureCredentials +

not needed +

+A wrapper method to just create the class is not very useful. +

CredentialsLoader +

+:: fromEnv +

renamed/refactored +

+This happens implicitly when calling GoogleAuth::makeCredentials +

CredentialsLoader +

+:: fromWellKnownFile +

renamed/refactored +

+This happens implicitly when calling GoogleAuth::makeCredentials +

CredentialsLoader +

+:: getUpdateMetadataFunc +

not needed +

+A function to return the callable of another function is not very useful. +

+See Improved Credentials Interface +

CredentialsLoader +

+:: updateMetadata +

renamed/refactored +

+into CredentialsTrait::getRequestMetadata +

+See Improved Credentials Interface +

FetchAuthTokenInterface +

+:: getLastReceivedToken +

not needed +

+Proper caching should make this unnecessary +

FetchAuthTokenInterface +

+:: getCacheKey +

not needed +

+Proper caching should make this unnecessary +

GCECredentials +

+:: getTokenUri +

+:: getClientNameUri +

not needed +

+The URIs used to call the metadata server is implementation detail, and does not need to be public +

ServiceAccountCredentials +

+:: setSub +

not needed +

+subject can be passed in to the GoogleAuth class +

SignBlobInterface +

+:: getClientName +

+ => +

+:: getClientEmail +

renamed +

+More accurate description of the returned value (the JSON field is client_email and the metadata URL is service-accounts/default/email +

OAuth2 +

+:: updateToken +

+ => +

+:: setAuthToken +

renamed +

+Consistent with fetchAuthToken +

+More clearly identifies that no network calls are made +

OAuth2 +

+:: verifyIdToken +

refactored +

+This functionality is now in GoogleAuth::verify and +JwtClientInterface::decode. +

+ +#### Dropped Library Support + +* Dropped support for Guzzle 5 +* Dropped support for `firebase\php-jwt` 2.0, 3.0, and 4.0 +* Dropped support for App Engine `php55` + +#### Other breaking changes + +* The `$tokenCallback` arguments have been removed. See **Improved Credentials Interface** +* `CredentialsInterface `implementations no longer extend `CredentialsLoader` (as it's been removed) +* `OAuth2` no longer implements `CredentialsInterface` (previously `FetchAuthTokenInterface`), and instead is passed to an `OAuth2Credentials` object which does. +* Removed class constant `ApplicationDefaultCredentials::AUTH_METADATA_KEY` +* Most class constants have been made private. Interface constants are still public. +* The argument `$forceOpenssl` has been removed from `signBlob` methods diff --git a/composer.json b/composer.json index f682d7242..026753537 100644 --- a/composer.json +++ b/composer.json @@ -9,9 +9,9 @@ "docs": "https://googleapis.github.io/google-auth-library-php/master/" }, "require": { - "php": ">=5.4", - "firebase/php-jwt": "~2.0|~3.0|~4.0|~5.0", - "guzzlehttp/guzzle": "^5.3.1|^6.2.1|^7.0", + "php": ">=7.1", + "firebase/php-jwt": "^5.2", + "guzzlehttp/guzzle": "^6.2|^7.0", "guzzlehttp/psr7": "^1.2", "psr/http-message": "^1.0", "psr/cache": "^1.0" @@ -19,17 +19,16 @@ "require-dev": { "guzzlehttp/promises": "0.1.1|^1.3", "squizlabs/php_codesniffer": "^3.5", - "phpunit/phpunit": "^4.8.36|^5.7", + "phpunit/phpunit": "^7.5|^8.0", "sebastian/comparator": ">=1.2.3", "phpseclib/phpseclib": "^2", "kelvinmo/simplejwt": "^0.2.5|^0.5.1" }, - "suggest": { - "phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2." - }, "autoload": { "psr-4": { - "Google\\Auth\\": "src" + "Google\\Auth\\": "src/Auth", + "Google\\Cache\\": "src/Cache", + "Google\\Http\\": "src/Http" } }, "autoload-dev": { diff --git a/src/AccessToken.php b/src/AccessToken.php deleted file mode 100644 index 1352e9351..000000000 --- a/src/AccessToken.php +++ /dev/null @@ -1,479 +0,0 @@ -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 $options [optional] Configuration options. - * @param string $options.audience The indended recipient of the token. - * @param string $options.issuer The intended issuer of the token. - * @param string $options.cacheKey The cache key of the cached certs. Defaults to - * the sha1 of $certsLocation if provided, otherwise is set to - * "federated_signon_certs_v3". - * @param string $options.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. - * @param bool $options.throwException Whether the function should throw an - * exception if the verification fails. This is useful for - * determining the reason verification failed. - * @return array|bool 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 = isset($options['audience']) - ? $options['audience'] - : null; - $issuer = isset($options['issuer']) - ? $options['issuer'] - : null; - $certsLocation = isset($options['certsLocation']) - ? $options['certsLocation'] - : self::FEDERATED_SIGNON_CERT_URL; - $cacheKey = isset($options['cacheKey']) - ? $options['cacheKey'] - : $this->getCacheKeyFromCertLocation($certsLocation); - $throwException = isset($options['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 3+ - } catch (\ExpiredException $e) { // firebase/php-jwt 2 - } catch (SignatureInvalidException $e) { // firebase/php-jwt 3+ - } catch (\SignatureInvalidException $e) { // firebase/php-jwt 2 - } catch (InvalidTokenException $e) { // simplejwt - } catch (DomainException $e) { - } 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 $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 $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|bool 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 (isset($payload['aud'])) { - if ($audience && $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 $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|bool 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' - ); - } - $rsa = new RSA(); - $rsa->loadKey([ - 'n' => new BigInteger($this->callJwtStatic('urlsafeB64Decode', [ - $cert['n'], - ]), 256), - 'e' => new BigInteger($this->callJwtStatic('urlsafeB64Decode', [ - $cert['e'] - ]), 256), - ]); - - // create an array of key IDs to certs for the JWT library - $keys[$cert['kid']] = $rsa->getPublicKey(); - } - - $payload = $this->callJwtStatic('decode', [ - $token, - $keys, - ['RS256'] - ]); - - if (property_exists($payload, 'aud')) { - if ($audience && $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 $token The token (access token or a refresh token) that should be revoked. - * @param array $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 = Psr7\stream_for(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 $options [optional] Configuration options. - * @return array - * @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; - - $gotNewCerts = false; - if (!$certs) { - $certs = $this->retrieveCertsFromLocation($location, $options); - - $gotNewCerts = true; - } - - 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 ($gotNewCerts) { - $cacheItem->expiresAt(new DateTime('+1 hour')); - $cacheItem->set($certs); - $this->cache->save($cacheItem); - } - - return $certs['keys']; - } - - /** - * Retrieve and cache a certificates file. - * - * @param $url string location - * @param array $options [optional] Configuration options. - * @return array certificates - * @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. - 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(file_get_contents($url), true); - } - - $httpHandler = $this->httpHandler; - $response = $httpHandler(new Request('GET', $url), $options); - - if ($response->getStatusCode() == 200) { - return json_decode((string) $response->getBody(), true); - } - - throw new RuntimeException(sprintf( - 'Failed to retrieve verification certificates: "%s".', - $response->getBody()->getContents() - ), $response->getStatusCode()); - } - - private function checkAndInitializePhpsec() - { - // @codeCoverageIgnoreStart - if (!class_exists('phpseclib\Crypt\RSA')) { - throw new RuntimeException('Please require phpseclib/phpseclib v2 to use this utility.'); - } - // @codeCoverageIgnoreEnd - - $this->setPhpsecConstants(); - } - - private function checkSimpleJwt() - { - // @codeCoverageIgnoreStart - if (!class_exists('SimpleJWT\JWT')) { - throw new RuntimeException('Please require kelvinmo/simplejwt ^0.2 to use this utility.'); - } - // @codeCoverageIgnoreEnd - } - - /** - * phpseclib calls "phpinfo" by default, which requires special - * whitelisting in the AppEngine VM environment. This function - * sets constants to bypass the need for phpseclib to check phpinfo - * - * @see phpseclib/Math/BigInteger - * @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85 - * @codeCoverageIgnore - */ - private function setPhpsecConstants() - { - if (filter_var(getenv('GAE_VM'), FILTER_VALIDATE_BOOLEAN)) { - if (!defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) { - define('MATH_BIGINTEGER_OPENSSL_ENABLED', true); - } - if (!defined('CRYPT_RSA_MODE')) { - define('CRYPT_RSA_MODE', RSA::MODE_OPENSSL); - } - } - } - - /** - * Provide a hook to mock calls to the JWT static methods. - * - * @param string $method - * @param array $args - * @return mixed - */ - protected function callJwtStatic($method, array $args = []) - { - $class = class_exists('Firebase\JWT\JWT') - ? 'Firebase\JWT\JWT' - : 'JWT'; - return call_user_func_array([$class, $method], $args); - } - - /** - * Provide a hook to mock calls to the JWT static methods. - * - * @param array $args - * @return mixed - */ - protected function callSimpleJwtDecode(array $args = []) - { - return call_user_func_array(['SimpleJWT\JWT', '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; - } -} diff --git a/src/ApplicationDefaultCredentials.php b/src/ApplicationDefaultCredentials.php deleted file mode 100644 index 6e04c3c36..000000000 --- a/src/ApplicationDefaultCredentials.php +++ /dev/null @@ -1,308 +0,0 @@ -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 -{ - /** - * 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|array 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 $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( - $scope = null, - callable $httpHandler = null, - array $cacheConfig = null, - CacheItemPoolInterface $cache = null - ) { - $creds = self::getCredentials($scope, $httpHandler, $cacheConfig, $cache); - - 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|array 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 $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 an AuthTokenMiddleware which will fetch an access token to use in - * the Authorization header. The middleware is configured with 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|array $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 $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|array $defaultScope The default scope to use if no - * user-defined scopes exist, expressed either as an Array or as a - * space-delimited string. - * - * @return CredentialsLoader - * @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 - ) { - $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($jsonKey)) { - if ($quotaProject) { - $jsonKey['quota_project_id'] = $quotaProject; - } - $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); - } - - 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 $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 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 $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 CredentialsLoader - * @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); - } - - if (is_null($creds)) { - throw new DomainException(self::notFound()); - } - if (!is_null($cache)) { - $creds = new FetchAuthTokenCache($creds, $cacheConfig, $cache); - } - return $creds; - } - - private static function notFound() - { - $msg = 'Could not load the default credentials. Browse to '; - $msg .= 'https://developers.google.com'; - $msg .= '/accounts/docs/application-default-credentials'; - $msg .= ' for more information'; - - return $msg; - } - - 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); - } -} diff --git a/src/Credentials/InsecureCredentials.php b/src/Auth/Credentials/AnonymousCredentials.php similarity index 62% rename from src/Credentials/InsecureCredentials.php rename to src/Auth/Credentials/AnonymousCredentials.php index dae894fab..7a76ba985 100644 --- a/src/Credentials/InsecureCredentials.php +++ b/src/Auth/Credentials/AnonymousCredentials.php @@ -1,6 +1,6 @@ token; } - /** - * Returns the cache key. In this case it returns a null value, disabling - * caching. + * Get the project ID. * * @return string|null */ - public function getCacheKey() + public function getProjectId(): ?string { return null; } /** - * Fetches the last received token. In this case, it returns the same empty string - * auth token. + * Get the quota project used for this API request * - * @return array + * @return string|null */ - public function getLastReceivedToken() + public function getQuotaProject(): ?string { - return $this->token; + return null; } } diff --git a/src/Auth/Credentials/ComputeCredentials.php b/src/Auth/Credentials/ComputeCredentials.php new file mode 100644 index 000000000..d7000708b --- /dev/null +++ b/src/Auth/Credentials/ComputeCredentials.php @@ -0,0 +1,430 @@ +send(new Request('GET', $url)); + */ +class ComputeCredentials implements + CredentialsInterface, + SignBlobInterface +{ + use CredentialsTrait, ServiceAccountApiSignBlobTrait; + + /** + * 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. + */ + private const METADATA_IP = '169.254.169.254'; + + /** + * The metadata path of the default token. + */ + private const ACCESS_TOKEN_URI_PATH = 'v1/instance/service-accounts/default/token'; + + /** + * The metadata path of the default id token. + */ + private const ID_TOKEN_URI_PATH = 'v1/instance/service-accounts/default/identity'; + + /** + * The metadata path of the client ID. + */ + private const CLIENT_EMAIL_URI_PATH = 'v1/instance/service-accounts/default/email'; + + /** + * The metadata path of the project ID. + */ + private const PROJECT_ID_URI_PATH = 'v1/project/project-id'; + + /** + * The header whose presence indicates GCE presence. + */ + private const FLAVOR_HEADER = 'Metadata-Flavor'; + + /** + * @var string|null + */ + private $clientEmail; + + /** + * @var string|null + */ + private $projectId; + + /** + * @var string|null + */ + private $targetAudience; + + /** + * @var array|null + */ + private $scope; + + /** + * @var string|null + */ + private $quotaProject; + + /** + * @var string|null + */ + private $serviceAccountIdentity; + + /** + * @var ClientInterface + */ + private $httpClient; + + /** + * @var string + */ + private $tokenUri; + + /** + * @param array $options { + * @type string|array $scope the scope of the access request, + * expressed either as an array or as a space-delimited string. + * @type string $targetAudience The audience for the ID token. + * @type string $quotaProject Specifies a project to bill for access + * charges associated with the request. + * @type string $serviceAccountIdentity [optional] Specify a service + * account identity name to use instead of "default". + * } + */ + public function __construct(array $options = []) + { + $options += [ + 'httpClient' => null, + 'quotaProject' => null, + 'serviceAccountIdentity' => null, + 'scope' => null, + 'targetAudience' => null, + ]; + + if (isset($options['scope']) && isset($options['targetAudience'])) { + throw new InvalidArgumentException( + 'Scope and targetAudience cannot both be supplied' + ); + } + + $this->setCacheFromOptions($options); + $this->setHttpClientFromOptions($options); + + $this->quotaProject = $options['quotaProject']; + $this->serviceAccountIdentity = $options['serviceAccountIdentity']; + $this->scope = is_string($options['scope']) + ? explode(' ', $options['scope']) + : $options['scope']; + $this->targetAudience = $options['targetAudience']; + $this->tokenUri = $this->getTokenUri(); + } + + /** + * The full uri for accessing the auth token. + * + * @return string + */ + + private function getTokenUri(): string + { + $tokenUri = 'http://' . self::METADATA_IP . '/computeMetadata/'; + + if ($this->targetAudience) { + $tokenUri .= self::ID_TOKEN_URI_PATH; + $tokenUri .= '?audience=' . $this->targetAudience; + } else { + $tokenUri .= self::ACCESS_TOKEN_URI_PATH; + if ($this->scope) { + $tokenUri .= '?scopes=' . implode(',', $this->scope); + } + } + + if ($this->serviceAccountIdentity) { + return str_replace( + '/default/', + '/' . $this->serviceAccountIdentity . '/', + $tokenUri + ); + } + + return $tokenUri; + } + + /** + * Determines if this an App Engine Flexible instance, by accessing the + * GAE_INSTANCE environment variable. + * + * @return bool + */ + public static function onAppEngineFlexible(): bool + { + if ($gaeInstance = getenv('GAE_INSTANCE')) { + return substr($gaeInstance, 0, 4) === 'aef-'; + } + return false; + } + + /** + * Determines if this a GCE instance, by accessing the expected metadata + * host. + * + * @param ClientInterface $httpClient + * @return bool + */ + public static function onCompute(ClientInterface $httpClient): bool + { + /** + * 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. + */ + $maxComputePingTries = 3; + $computePingConnectionTimeoutSeconds = 0.5; + $checkUri = 'http://' . self::METADATA_IP; + for ($i = 1; $i <= $maxComputePingTries; $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 = $httpClient->send( + new Request( + 'GET', + $checkUri, + [self::FLAVOR_HEADER => 'Google'] + ), + ['timeout' => $computePingConnectionTimeoutSeconds] + ); + + return $resp->getHeaderLine(self::FLAVOR_HEADER) == 'Google'; + } catch (ClientException $e) { + } catch (ServerException $e) { + } catch (RequestException $e) { + } catch (ConnectException $e) { + } + } + return false; + } + + /** + * Implements CredentialsInterface#fetchAuthToken. + * + * Fetches the auth tokens from the GCE metadata host if it is available. + * If $httpClient is not specified a the default HttpHandler is used. + * + * @param ClientInterface $httpClient callback which delivers psr7 request + * + * @return array A set of auth related metadata, based on the token type. + * + * Access tokens have the following keys: + * - access_token (string) + * - expires_in (int) + * - token_type (string) + * ID tokens have the following keys: + * - id_token (string) + * + * @throws \Exception + */ + private function fetchAuthTokenNoCache(): array + { + $response = $this->getFromMetadata($this->tokenUri); + + if ($this->targetAudience) { + return ['id_token' => $response]; + } + + if (null === $json = json_decode($response, true)) { + throw new \Exception('Invalid JSON response'); + } + + $json['expires_at'] = time() + $json['expires_in']; + + return $json; + } + + /** + * Get the client name from GCE metadata. + * + * Subsequent calls will return a cached value. + * + * @return string + */ + public function getClientEmail(): string + { + if ($this->clientEmail) { + return $this->clientEmail; + } + + return $this->clientEmail = $this->getFromMetadata( + self::getClientEmailUri($this->serviceAccountIdentity) + ); + } + + /** + * 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. + * @return string + */ + public function signBlob(string $stringToSign): string + { + $accessToken = $this->fetchAuthToken()['access_token']; + + return $this->signBlobWithServiceAccountApi( + $this->getClientEmail(), + $accessToken, + $stringToSign, + $this->httpClient + ); + } + + /** + * Fetch the default Project ID from compute engine. + * + * Returns null if called outside GCE. + * + * @return string|null + */ + public function getProjectId(): ?string + { + if ($this->projectId) { + return $this->projectId; + } + + return $this->projectId = $this->getFromMetadata( + self::getProjectIdUri() + ); + } + + /** + * Get the quota project used for this API request + * + * @return string|null + */ + public function getQuotaProject(): ?string + { + return $this->quotaProject; + } + + private function getCacheKey(): string + { + return $this->tokenUri; + } + + /** + * Fetch the value of a GCE metadata server URI. + * + * @param string $uri The metadata URI. + * @return string + */ + private function getFromMetadata($uri) + { + $resp = $this->httpClient->send( + new Request( + 'GET', + $uri, + [self::FLAVOR_HEADER => 'Google'] + ) + ); + + return (string) $resp->getBody(); + } + + /** + * The full uri for accessing the default project ID. + * + * @return string + */ + private static function getProjectIdUri(): string + { + $base = 'http://' . self::METADATA_IP . '/computeMetadata/'; + + return $base . self::PROJECT_ID_URI_PATH; + } + + /** + * 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 + */ + + private static function getClientEmailUri( + string $serviceAccountIdentity = null + ): string { + $base = 'http://' . self::METADATA_IP . '/computeMetadata/'; + $base .= self::CLIENT_EMAIL_URI_PATH; + + if ($serviceAccountIdentity) { + return str_replace( + '/default/', + '/' . $serviceAccountIdentity . '/', + $base + ); + } + + return $base; + } +} diff --git a/src/FetchAuthTokenInterface.php b/src/Auth/Credentials/CredentialsInterface.php similarity index 50% rename from src/FetchAuthTokenInterface.php rename to src/Auth/Credentials/CredentialsInterface.php index 4bf4d27ff..e36d33bb8 100644 --- a/src/FetchAuthTokenInterface.php +++ b/src/Auth/Credentials/CredentialsInterface.php @@ -1,6 +1,6 @@ fetchAuthToken(); + if (isset($result['access_token'])) { + return ['Authorization' => 'Bearer ' . $result['access_token']]; + } + + return []; + } + + /** + * Implements CredentialsInterface#fetchAuthToken. + * + * Fetches the auth tokens and caches it based on the default cache + * configuration. + * + * @return array The auth token + * + * Access tokens have the following keys: + * - access_token (string) + * - expires_in (int) + * - token_type (string) + * ID tokens have the following keys: + * - id_token (string) + * + * @throws \Exception + */ + public function fetchAuthToken(): array + { + $cacheKey = $this->getCacheKey(); + if ($cachedToken = $this->getCachedToken($cacheKey)) { + return $cachedToken; + } + + $token = $this->fetchAuthTokenNoCache(); + + $this->setCachedToken($cacheKey, $token); + + return $token; + } + + /** + * @param array $options + * @param ClientInterface $options.httpClient + */ + private function setHttpClientFromOptions(array $options): void + { + if (empty($options['httpClient'])) { + $options['httpClient'] = ClientFactory::build(); + } + if (!$options['httpClient'] instanceof ClientInterface) { + throw new \RuntimeException(sprintf( + 'Invalid option "httpClient": must be an instance of %s', + ClientInterface::class + )); + } + $this->httpClient = $options['httpClient']; + } + + /** + * @param array $options + * @param CacheItemPoolInterface $options.cache + * @param int $options.cacheLifetime + * @param strring $options.cachePrefix + */ + private function setCacheFromOptions(array $options): void + { + if (!empty($options['cache'])) { + if (!$options['cache'] instanceof CacheItemPoolInterface) { + throw new \RuntimeException(sprintf( + 'Invalid option "cache": must be an instance of %s', + CacheItemPoolInterface::class + )); + } + $this->cache = $options['cache']; + } else { + $this->cache = new MemoryCacheItemPool(); + } + if (array_key_exists('cacheLifetime', $options)) { + $this->cacheLifetime = (int) $options['cacheLifetime']; + } + if (array_key_exists('cachePrefix', $options)) { + $this->cachePrefix = (string) $options['cachePrefix']; + } + } + + private function getCacheKey(): string + { + throw new \LogicException( + 'getCacheKey must be implemented in the Credentials class' + ); + } + + private function fetchAuthTokenNoCache(): array + { + throw new \LogicException( + 'fetchAuthTokenNoCache must be implemented in the Credentials class' + ); + } + + /** + * Gets the cached value if it is present in the cache when that is + * available. + */ + private function getCachedToken(string $cacheKey): ?array + { + if (is_null($this->cache)) { + throw new \LogicException('Cache has not been initialized'); + } + + $key = $this->getFullCacheKey($cacheKey); + + $cacheItem = $this->cache->getItem($key); + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + + return null; + } + + /** + * Saves the value in the cache when that is available. + */ + private function setCachedToken(string $cacheKey, array $token): bool + { + if (is_null($this->cache)) { + throw new \LogicException('Cache has not been initialized'); + } + + $key = $this->getFullCacheKey($cacheKey); + + $cacheItem = $this->cache->getItem($key); + $cacheItem->set($token); + + // Set token cache expiry to access token expiry when possible + if (isset($token['expires_at'])) { + $expiresTimestamp = (string) $token['expires_at']; + $expiresAt = \DateTime::createFromFormat('U', $expiresTimestamp); + $cacheItem->expiresAt($expiresAt); + } elseif (isset($token['expires_in'])) { + $cacheItem->expiresAfter($token['expires_in']); + } else { + $cacheItem->expiresAfter($this->cacheLifetime); + } + + return $this->cache->save($cacheItem); + } + + private function getFullCacheKey(string $key): string + { + if (empty($key)) { + throw new \LogicException('Cache key cannot be empty'); + } + + $key = $this->cachePrefix . $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 (strlen($this->cachePrefix . $key) > $this->maxCacheKeyLength) { + $maxKeyLength = $this->maxCacheKeyLength - strlen($this->cachePrefix); + $key = substr(hash('sha256', $key), 0, $maxKeyLength); + } + + return $this->cachePrefix . $key; + } + + /** + * Throws an exception when targetAudience is supplied to credentials which + * do not support it. + */ + private function throwExceptionForTargetAudience(array $options) + { + if (isset($options['targetAudience'])) { + throw new \InvalidArgumentException(sprintf( + '"targetAudience" is not a valid option for %s', + __CLASS__ + )); + } + } + + /** + * Parses the JSON key file and sets the quota project if applicable. + */ + private function parseJsonKey($jsonKey): array + { + if (is_string($jsonKey)) { + if (!file_exists($jsonKey)) { + throw new \InvalidArgumentException('file does not exist'); + } + $jsonKeyStream = file_get_contents($jsonKey); + if (!$jsonKey = json_decode($jsonKeyStream, true)) { + throw new \LogicException('invalid json for auth config'); + } + } + + if (!is_array($jsonKey)) { + throw new \InvalidArgumentException( + 'JSON key must be a string or an array' + ); + } + + if (array_key_exists('quota_project_id', $jsonKey)) { + $this->quotaProject = (string) $jsonKey['quota_project_id']; + } + + return $jsonKey; + } +} diff --git a/src/Auth/Credentials/OAuth2Credentials.php b/src/Auth/Credentials/OAuth2Credentials.php new file mode 100644 index 000000000..594a49641 --- /dev/null +++ b/src/Auth/Credentials/OAuth2Credentials.php @@ -0,0 +1,96 @@ +setCacheFromOptions($options); + + $this->oauth2 = $oauth2; + } + + /** + * Fetches the auth tokens based on the current state. + * + * @return array a hash of auth tokens + */ + private function fetchAuthTokenNoCache(): array + { + return $this->oauth2->fetchAuthToken(); + } + + /** + * Get the project ID. + * + * @return string|null + */ + public function getProjectId(): ?string + { + throw new LogicException( + 'getProjectId is not implemented for OAuth2 credentials' + ); + } + + /** + * Get the quota project used for this API request + * + * @return string|null + */ + public function getQuotaProject(): ?string + { + throw new LogicException( + 'getQuotaProject is not implemented for OAuth2 credentials' + ); + } + + /** + * Obtains a key that can used to cache the results of #fetchAuthToken. + * + * The key is derived from the scopes. + * + * @return string a key that may be used to cache the auth token. + */ + private function getCacheKey(): string + { + if ($cacheKey = $this->oauth2->getCacheKey()) { + return $cacheKey; + } + + // If no scope and no audience, return default string. + return 'oauth2_credentials_cache'; + } +} diff --git a/src/Credentials/ServiceAccountCredentials.php b/src/Auth/Credentials/ServiceAccountCredentials.php similarity index 50% rename from src/Credentials/ServiceAccountCredentials.php rename to src/Auth/Credentials/ServiceAccountCredentials.php index e12f6c672..818086fb7 100644 --- a/src/Credentials/ServiceAccountCredentials.php +++ b/src/Auth/Credentials/ServiceAccountCredentials.php @@ -1,6 +1,6 @@ get('myproject/taskqueues/myqueue'); */ -class ServiceAccountCredentials extends CredentialsLoader implements - GetQuotaProjectInterface, - SignBlobInterface, - ProjectIdProviderInterface +class ServiceAccountCredentials implements + CredentialsInterface, + SignBlobInterface { - use ServiceAccountSignerTrait; + use CredentialsTrait { + CredentialsTrait::getRequestMetadata as traitGetRequestMetadata; + } + use PrivateKeySignBlobTrait; + use ServiceAccountApiSignBlobTrait; /** * The OAuth2 instance used to conduct authorization. * * @var OAuth2 */ - protected $auth; + private $oauth2; /** * The quota project associated with the JSON credentials * * @var string */ - protected $quotaProject; + private $quotaProject; /* * @var string|null */ - protected $projectId; - - /* - * @var array|null - */ - private $lastReceivedJwtAccessToken; + private $projectId; /** * Create a new ServiceAccountCredentials. * - * @param string|array $scope the scope of the access request, expressed - * either as an Array or as a space-delimited String. - * @param string|array $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. + * @param string|array $jsonKey JSON credential file path or JSON + * credentials in associative array + * @param array $options { + * @type string|array $scope the scope of the access request, expressed + * as an array or as a space-delimited string. + * @type string $subject an email address account to impersonate, in + * situations when the service account has been delegated domain + * wide access. + * @type 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($jsonKeyStream, true)) { - throw new \LogicException('invalid json for auth config'); - } - } + public function __construct($jsonKey, array $options = []) + { + $options += [ + 'scope' => null, + 'targetAudience' => null, + 'subject' => null, + ]; + + $jsonKey = $this->parseJsonKey($jsonKey); + if (!array_key_exists('client_email', $jsonKey)) { throw new \InvalidArgumentException( 'json key is missing the client_email field' @@ -125,27 +121,30 @@ public function __construct( '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) { + if ($options['scope'] && $options['targetAudience']) { throw new InvalidArgumentException( 'Scope and targetAudience cannot both be supplied' ); } $additionalClaims = []; - if ($targetAudience) { - $additionalClaims = ['target_audience' => $targetAudience]; + if ($options['targetAudience']) { + $additionalClaims = [ + 'target_audience' => $options['targetAudience'] + ]; } - $this->auth = new OAuth2([ + $this->setHttpClientFromOptions($options); + $this->setCacheFromOptions($options); + + $this->oauth2 = new OAuth2([ 'audience' => self::TOKEN_CREDENTIAL_URI, - 'issuer' => $jsonKey['client_email'], - 'scope' => $scope, + 'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI, 'signingAlgorithm' => 'RS256', 'signingKey' => $jsonKey['private_key'], - 'sub' => $sub, - 'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI, + 'issuer' => $jsonKey['client_email'], + 'scope' => $options['scope'], + 'sub' => $options['subject'], 'additionalClaims' => $additionalClaims, + 'httpClient' => $this->httpClient, ]); $this->projectId = isset($jsonKey['project_id']) @@ -154,42 +153,14 @@ public function __construct( } /** - * @param callable $httpHandler - * - * @return array A set of auth related metadata, containing the following - * keys: - * - access_token (string) - * - expires_in (int) - * - token_type (string) - */ - public function fetchAuthToken(callable $httpHandler = null) - { - return $this->auth->fetchAuthToken($httpHandler); - } - - /** - * @return string + * @return array Auth related metadata, with the following keys: + * - access_token (string) + * - expires_in (int) + * - token_type (string) */ - public function getCacheKey() + private function fetchAuthTokenNoCache(): array { - $key = $this->auth->getIssuer() . ':' . $this->auth->getCacheKey(); - if ($sub = $this->auth->getSub()) { - $key .= ':' . $sub; - } - - return $key; - } - - /** - * @return array - */ - 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(); + return $this->oauth2->fetchAuthToken(); } /** @@ -197,56 +168,67 @@ public function getLastReceivedToken() * * 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) + public function getProjectId(): ?string { return $this->projectId; } /** - * Updates metadata with the authorization token. + * @param string $authUri The optional uri being authorized * - * @param array $metadata metadata hashmap - * @param string $authUri optional auth uri - * @param callable $httpHandler callback which delivers psr7 request - * @return array updated metadata hashmap + * @return array metadata hashmap for request headers */ - public function updateMetadata( - $metadata, - $authUri = null, - callable $httpHandler = null - ) { + public function getRequestMetadata(string $authUri = null): array + { // scope exists. use oauth implementation if (!$this->useSelfSignedJwt()) { - return parent::updateMetadata($metadata, $authUri, $httpHandler); + return $this->traitGetRequestMetadata(); } // no scope found. create jwt with the auth uri $credJson = array( - 'private_key' => $this->auth->getSigningKey(), - 'client_email' => $this->auth->getIssuer(), + 'private_key' => $this->oauth2->getSigningKey(), + 'client_email' => $this->oauth2->getIssuer(), ); - $jwtCreds = new ServiceAccountJwtAccessCredentials($credJson); - $updatedMetadata = $jwtCreds->updateMetadata($metadata, $authUri, $httpHandler); + $options = [ + 'httpClient' => $this->httpClient, + 'cache' => $this->cache, + 'cacheLifetime' => $this->cacheLifetime, + 'cachePrefix' => $this->cachePrefix, + ]; - if ($lastReceivedToken = $jwtCreds->getLastReceivedToken()) { - // Keep self-signed JWTs in memory as the last received token - $this->lastReceivedJwtAccessToken = $lastReceivedToken; - } + $jwtCreds = new ServiceAccountJwtAccessCredentials($credJson, $options); - return $updatedMetadata; + return $jwtCreds->getRequestMetadata($authUri); } /** - * @param string $sub an email address account to impersonate, in situations when - * the service account has been delegated domain wide access. + * Sign a string using the method which is best for a given credentials type. + * If OpenSSL is not installed, uses the Service Account Credentials API. + * + * @param string $stringToSign The string to sign. + * @return string The resulting signature. Value should be base64-encoded. */ - public function setSub($sub) + public function signBlob(string $stringToSign): string { - $this->auth->setSub($sub); + try { + return $this->signBlobWithPrivateKey( + $stringToSign, + $this->oauth2->getSigningKey() + ); + } catch (\RuntimeException $e) { + } + + $accessToken = $this->fetchAuthToken()['access_token']; + return $this->signBlobWithServiceAccountApi( + $this->httpClient, + $this->getClientEmail(), + $accessToken, + $stringToSign + ); } /** @@ -254,12 +236,31 @@ public function setSub($sub) * * 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) + public function getClientEmail(): string + { + return $this->oauth2->getIssuer(); + } + + private function getCacheKey(): string { - return $this->auth->getIssuer(); + $key = $this->oauth2->getIssuer() . ':' . $this->oauth2->getCacheKey(); + if ($sub = $this->oauth2->getSub()) { + $key .= ':' . $sub; + } + if ($claims = $this->oauth2->getAdditionalClaims()) { + if (isset($claims['target_audience'])) { + $key .= ':' . $claims['target_audience']; + } + } + + return $key; + } + + private function useSelfSignedJwt() + { + return is_null($this->oauth2->getScope()); } /** @@ -267,13 +268,8 @@ public function getClientName(callable $httpHandler = null) * * @return string|null */ - public function getQuotaProject() + public function getQuotaProject(): ?string { return $this->quotaProject; } - - private function useSelfSignedJwt() - { - return is_null($this->auth->getScope()); - } } diff --git a/src/Credentials/ServiceAccountJwtAccessCredentials.php b/src/Auth/Credentials/ServiceAccountJwtAccessCredentials.php similarity index 52% rename from src/Credentials/ServiceAccountJwtAccessCredentials.php rename to src/Auth/Credentials/ServiceAccountJwtAccessCredentials.php index ac1147fce..ffcf081dd 100644 --- a/src/Credentials/ServiceAccountJwtAccessCredentials.php +++ b/src/Auth/Credentials/ServiceAccountJwtAccessCredentials.php @@ -1,6 +1,6 @@ null, + ]; + + $jsonKey = $this->parseJsonKey($jsonKey); + if (!array_key_exists('client_email', $jsonKey)) { throw new \InvalidArgumentException( 'json key is missing the client_email field' @@ -79,14 +79,18 @@ public function __construct($jsonKey) '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([ + + $this->setHttpClientFromOptions($options); + $this->setCacheFromOptions($options); + $this->throwExceptionForTargetAudience($options); + + $this->oauth2 = new OAuth2([ + 'audience' => $options['audience'], 'issuer' => $jsonKey['client_email'], 'sub' => $jsonKey['client_email'], 'signingAlgorithm' => 'RS256', 'signingKey' => $jsonKey['private_key'], + 'httpClient' => $this->httpClient, ]); $this->projectId = isset($jsonKey['project_id']) @@ -95,78 +99,82 @@ public function __construct($jsonKey) } /** - * Updates metadata with the authorization token. + * Implements FetchAuthTokenInterface#fetchAuthToken. * - * @param array $metadata metadata hashmap - * @param string $authUri optional auth uri - * @param callable $httpHandler callback which delivers psr7 request - * @return array updated metadata hashmap + * @return array A set of auth related metadata, containing the + * following keys: + * - access_token (string) */ - public function updateMetadata( - $metadata, - $authUri = null, - callable $httpHandler = null - ) { - if (empty($authUri)) { - return $metadata; - } - - $this->auth->setAudience($authUri); - - return parent::updateMetadata($metadata, $authUri, $httpHandler); + private function fetchAuthTokenNoCache(): array + { + return ['access_token' => $this->oauth2->toJwt()]; } /** - * Implements FetchAuthTokenInterface#fetchAuthToken. + * Get the project ID from the service account keyfile. * - * @param callable $httpHandler + * Returns null if the project ID does not exist in the keyfile. * - * @return array|void A set of auth related metadata, containing the - * following keys: - * - access_token (string) + * @return string|null */ - public function fetchAuthToken(callable $httpHandler = null) + public function getProjectId(): ?string { - $audience = $this->auth->getAudience(); - if (empty($audience)) { - return null; - } - - $access_token = $this->auth->toJwt(); - - // Set the self-signed access token in OAuth2 for getLastReceivedToken - $this->auth->setAccessToken($access_token); - - return array('access_token' => $access_token); + return $this->projectId; } /** - * @return string + * Get the quota project used for this API request + * + * @return string|null */ - public function getCacheKey() + public function getQuotaProject(): ?string { - return $this->auth->getCacheKey(); + return $this->quotaProject; } /** - * @return array + * Sign a string using the method which is best for a given credentials type. + * If OpenSSL is not installed, uses the Service Account Credentials API. + * + * @param string $stringToSign The string to sign. + * @return string The resulting signature. Value should be base64-encoded. */ - public function getLastReceivedToken() + public function signBlob(string $stringToSign): string { - return $this->auth->getLastReceivedToken(); + try { + return $this->signBlobWithPrivateKey( + $stringToSign, + $this->oauth2->getSigningKey() + ); + } catch (\RuntimeException $e) { + } + + $accessToken = $this->fetchAuthToken()['access_token']; + return $this->signBlobWithServiceAccountApi( + $this->httpClient, + $this->getClientEmail(), + $accessToken, + $stringToSign + ); } /** - * Get the project ID from the service account keyfile. + * Returns metadata with the authorization token. * - * Returns null if the project ID does not exist in the keyfile. + * @param string $authUri The optional uri being authorized * - * @param callable $httpHandler Not used by this credentials type. - * @return string|null + * @return array */ - public function getProjectId(callable $httpHandler = null) + public function getRequestMetadata(string $authUri = null): array { - return $this->projectId; + // no-op when audience is null + if (empty($authUri)) { + return []; + } + + $this->oauth2->setAudience($authUri); + + return $this->traitGetRequestMetadata($authUri); } /** @@ -174,21 +182,22 @@ public function getProjectId(callable $httpHandler = null) * * 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) + public function getClientEmail(): string { - return $this->auth->getIssuer(); + return $this->oauth2->getIssuer(); } /** - * Get the quota project used for this API request - * - * @return string|null + * @return string */ - public function getQuotaProject() + private function getCacheKey(): string { - return $this->quotaProject; + if ($cacheKey = $this->oauth2->getCacheKey()) { + return $cacheKey; + } + + throw new \LogicException('Unable to cache token without an audience'); } } diff --git a/src/Credentials/UserRefreshCredentials.php b/src/Auth/Credentials/UserRefreshCredentials.php similarity index 61% rename from src/Credentials/UserRefreshCredentials.php rename to src/Auth/Credentials/UserRefreshCredentials.php index b17ce5fcd..722d57f81 100644 --- a/src/Credentials/UserRefreshCredentials.php +++ b/src/Auth/Credentials/UserRefreshCredentials.php @@ -1,6 +1,6 @@ null, + ]; + + $jsonKey = $this->parseJsonKey($jsonKey); + if (!array_key_exists('client_id', $jsonKey)) { throw new \InvalidArgumentException( 'json key is missing the client_id field' @@ -82,21 +78,22 @@ public function __construct( 'json key is missing the refresh_token field' ); } - $this->auth = new OAuth2([ + + $this->setHttpClientFromOptions($options); + $this->setCacheFromOptions($options); + $this->throwExceptionForTargetAudience($options); + + $this->oauth2 = new OAuth2([ 'clientId' => $jsonKey['client_id'], 'clientSecret' => $jsonKey['client_secret'], - 'refresh_token' => $jsonKey['refresh_token'], - 'scope' => $scope, + 'refreshToken' => $jsonKey['refresh_token'], + 'scope' => $options['scope'] ?? null, + 'httpClient' => $this->httpClient, 'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI, ]); - if (array_key_exists('quota_project_id', $jsonKey)) { - $this->quotaProject = (string) $jsonKey['quota_project_id']; - } } /** - * @param callable $httpHandler - * * @return array A set of auth related metadata, containing the following * keys: * - access_token (string) @@ -105,34 +102,38 @@ public function __construct( * - token_type (string) * - id_token (string) */ - public function fetchAuthToken(callable $httpHandler = null) + private function fetchAuthTokenNoCache(): array { - return $this->auth->fetchAuthToken($httpHandler); + return $this->oauth2->fetchAuthToken(); } /** - * @return string + * Get the quota project used for this API request + * + * @return string|null */ - public function getCacheKey() + public function getQuotaProject(): ?string { - return $this->auth->getClientId() . ':' . $this->auth->getCacheKey(); + return $this->quotaProject; } /** - * @return array + * Get the project ID. + * + * @return string|null */ - public function getLastReceivedToken() + public function getProjectId(): ?string { - return $this->auth->getLastReceivedToken(); + throw new \RuntimeException( + 'getProjectId is not implemented for user refresh credentials' + ); } /** - * Get the quota project used for this API request - * - * @return string|null + * @return string */ - public function getQuotaProject() + private function getCacheKey(): string { - return $this->quotaProject; + return $this->oauth2->getClientId() . ':' . $this->oauth2->getCacheKey(); } } diff --git a/src/Auth/GoogleAuth.php b/src/Auth/GoogleAuth.php new file mode 100644 index 000000000..8bd1d2322 --- /dev/null +++ b/src/Auth/GoogleAuth.php @@ -0,0 +1,477 @@ +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 GoogleAuth +{ + const OIDC_CERT_URI = 'https://www.googleapis.com/oauth2/v3/certs'; + const OIDC_ISSUERS = ['http://accounts.google.com', 'https://accounts.google.com']; + const IAP_JWK_URI = 'https://www.gstatic.com/iap/verify/public_key-jwk'; + const IAP_ISSUERS = ['https://cloud.google.com/iap']; + + private const ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS'; + private const WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json'; + private const NON_WINDOWS_WELL_KNOWN_PATH_BASE = '.config'; + + private $cache; + private $cacheLifetime; + private $cachePrefix; + private $httpClient; + private $jwtClient; + + /** + * Obtains an AuthTokenMiddleware which will fetch an access token to use in + * the Authorization header. The middleware is configured with 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 array $options { + * @type ClientInterface $httpClient client which delivers psr7 request + * @type JwtClientInterface $jwtClient + * @type CacheItemPoolInterface $cache A cache implementation, may be + * provided if you have one already available for use. + * @type int $cacheLifetime + * @type string $cachePrefix + * } + */ + public function __construct(array $options = []) + { + $options += [ + 'cache' => null, + 'cacheLifetime' => 1500, + 'cachePrefix' => '', + 'httpClient' => null, + 'jwtClient' => null, + ]; + $this->cache = $options['cache'] ?: new MemoryCacheItemPool(); + $this->cacheLifetime = $options['cacheLifetime']; + $this->cachePrefix = $options['cachePrefix']; + $this->httpClient = $options['httpClient'] ?: ClientFactory::build(); + $this->jwtClient = $options['jwtClient'] + ?: new FirebaseJwtClient(new JWT(), new JWK()); + } + + /** + * Obtains an AuthTokenMiddleware which will fetch an access token to use in + * the Authorization header. The middleware is configured with 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 array $options { + * @type string|array scope the scope of the access request, expressed + * either as an Array or as a space-delimited String. + * @type string $targetAudience The audience for the ID token. + * @type string $audience + * @type string $quotaProject specifies a project to bill for access + * charges associated with the request. + * @type string $subject + * @type string|array $defaultScope The default scope to use if no + * user-defined scopes exist, expressed either as an Array or as + * a space-delimited string. + * } + * @return CredentialsInterface + * @throws DomainException if no implementation can be obtained. + */ + public function makeCredentials(array $options = []): CredentialsInterface + { + $options += [ + 'scope' => null, + 'targetAudience' => null, + 'audience' => null, + 'quotaProject' => null, + 'subject' => null, + 'credentialsFile' => null, + 'defaultScope' => null, + ]; + + if (is_null($options['credentialsFile'])) { + $jsonKey = $this->fromEnv() ?: $this->fromWellKnownFile(); + } else { + if (!file_exists($options['credentialsFile'])) { + throw new InvalidArgumentException('Unable to read credentialsFile'); + } + $jsonContents = file_get_contents($options['credentialsFile']); + $jsonKey = json_decode($jsonContents, true); + } + + $credentials = null; + $anyScope = $options['scope'] ?: $options['defaultScope']; + if (!is_null($jsonKey)) { + if (!array_key_exists('type', $jsonKey)) { + throw new \InvalidArgumentException( + 'json key is missing the type field' + ); + } + + // Set quota project on jsonKey if passed in + if (isset($options['quotaProject'])) { + $jsonKey['quota_project_id'] = $options['quotaProject']; + } + + switch ($jsonKey['type']) { + case 'service_account': + $credentials = new ServiceAccountCredentials($jsonKey, [ + 'scope' => $options['scope'], + 'targetAudience' => $options['targetAudience'], + 'httpClient' => $this->httpClient, + 'subject' => $options['subject'], + ]); + break; + case 'authorized_user': + if (isset($options['targetAudience'])) { + throw new InvalidArgumentException( + 'ID tokens are not supported for end user credentials' + ); + } + $credentials = new UserRefreshCredentials($jsonKey, [ + 'scope' => $anyScope, + 'httpClient' => $this->httpClient, + ]); + break; + default: + throw new \InvalidArgumentException( + 'invalid value in the type field' + ); + } + } elseif ($this->onCompute()) { + $credentials = new ComputeCredentials([ + 'scope' => $anyScope, + 'quotaProject' => $options['quotaProject'], + 'httpClient' => $this->httpClient, + 'targetAudience' => $options['targetAudience'], + ]); + } + + if (is_null($credentials)) { + throw new DomainException( + 'Could not load the default credentials. Browse to ' + . 'https://developers.google.com/accounts/docs/application-default-credentials' + . ' for more information' + ); + } + + return $credentials; + } + + /** + * Determines if this a GCE instance, by accessing the expected metadata + * host. + * + * @return bool + */ + public function onCompute(array $options = []): bool + { + $cacheKey = 'google_auth_on_gce_cache'; + $cacheItem = $this->cache->getItem($this->cachePrefix . $cacheKey); + + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + + $onCompute = ComputeCredentials::onCompute($this->httpClient); + $cacheItem->set($onCompute); + $cacheItem->expiresAfter($this->cacheLifetime); + $this->cache->save($cacheItem); + + return $onCompute; + } + + /** + * @param string $token The JSON Web Token to be verified. + * @param array $options [optional] Configuration options. + * @param string $options.audience The indended recipient of the token. + * @param string $options.cacheKey cache key used for caching certs + * @param string $options.certsLocation URI for JSON certificate array conforming to + * the JWK spec (see https://tools.ietf.org/html/rfc7517). + * @param string $options.issuer The intended issuer of the token. + * + * @return array the verified ID token payload + */ + public function verify(string $token, array $options = []): array + { + $options += [ + 'audience' => null, + 'certsLocation' => null, + 'cacheKey' => null, + 'issuer' => null, + ]; + $location = $options['certsLocation'] ?: self::OIDC_CERT_URI; + $cacheKey = $options['cacheKey'] ?: + sprintf('google_auth_certs_cache|%s', sha1($location)); + + $certs = $this->getCerts($location, $cacheKey); + $alg = $this->determineAlg($certs); + + $keys = $this->jwtClient->parseKeySet($certs); + $payload = $this->jwtClient->decode($token, $keys, [$alg]); + + $issuers = (array) $options['issuer'] ?: + ['RS256' => self::OIDC_ISSUERS, 'ES256' => self::IAP_ISSUERS][$alg]; + + if (empty($payload['iss']) || !in_array($payload['iss'], $issuers)) { + throw new UnexpectedValueException('Issuer does not match'); + } + + $aud = $options['audience'] ?: null; + if ($aud && isset($payload['aud']) && $payload['aud'] != $aud) { + throw new UnexpectedValueException('Audience does not match'); + } + + return $payload; + } + + /** + * 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. + * @return array + * @throws InvalidArgumentException If received certs are in an invalid format. + */ + private function getCerts(string $location, string $cacheKey): array + { + $cacheItem = $this->cache->getItem($this->cachePrefix . $cacheKey); + $certs = $cacheItem ? $cacheItem->get() : null; + + $gotNewCerts = false; + if (!$certs) { + $certs = $this->retrieveCertsFromLocation($location); + + $gotNewCerts = true; + } + + if (!isset($certs['keys'])) { + 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 ($gotNewCerts) { + $cacheItem->expiresAfter($this->cacheLifetime); + $cacheItem->set($certs); + $this->cache->save($cacheItem); + } + + return $certs; + } + + /** + * Identifies the expected algorithm to verify by looking at the "alg" key + * of the provided certs. + * + * @param array $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): string + { + $alg = null; + foreach ($certs['keys'] 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' + ); + } + } + if (!in_array($alg, ['RS256', 'ES256'])) { + throw new InvalidArgumentException( + 'unrecognized "alg" in certs, expected ES256 or RS256' + ); + } + return $alg; + } + + /** + * Retrieve and cache a certificates file. + * + * @param $url string location + * @return array certificates + * @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(string $url): array + { + // If we're retrieving a local file, just grab it. + 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(file_get_contents($url), true); + } + + $response = $this->httpClient->send(new Request('GET', $url)); + + if ($response->getStatusCode() == 200) { + return json_decode((string) $response->getBody(), true); + } + + throw new RuntimeException(sprintf( + 'Failed to retrieve verification certificates: "%s".', + $response->getBody()->getContents() + ), $response->getStatusCode()); + } + + /** + * 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|null + */ + private function fromEnv(): ?array + { + $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($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|null + */ + private function fromWellKnownFile(): ?array + { + $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($jsonKey, true); + } + + /** + * @param string $cause + * + * @return string + */ + private static function unableToReadEnv(string $cause): string + { + $msg = 'Unable to read the credential file specified by '; + $msg .= ' GOOGLE_APPLICATION_CREDENTIALS: '; + $msg .= $cause; + + return $msg; + } + + /** + * @return bool + */ + private static function isOnWindows(): bool + { + return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; + } +} diff --git a/src/Auth/Http/ApiKeyClient.php b/src/Auth/Http/ApiKeyClient.php new file mode 100644 index 000000000..a5272f1f5 --- /dev/null +++ b/src/Auth/Http/ApiKeyClient.php @@ -0,0 +1,68 @@ +apiKey = $apiKey; + $this->httpClient = $httpClient ?: ClientFactory::build(); + } + + public function send( + RequestInterface $request, + array $options = [] + ): ResponseInterface { + return $this->httpClient->send( + $this->applyApiKey($request), + $options + ); + } + + public function sendAsync( + RequestInterface $request, + array $options = [] + ): PromiseInterface { + return $this->httpClient->sendAsync( + $this->applyApiKey($request), + $options + ); + } + + private function applyApiKey(RequestInterface $request): RequestInterface + { + $query = Psr7\parse_query($request->getUri()->getQuery()); + $query['key'] = $this->apiKey; + $uri = $request->getUri()->withQuery(Psr7\build_query($query)); + return $request->withUri($uri); + } +} diff --git a/src/Auth/Http/ClientFactory.php b/src/Auth/Http/ClientFactory.php new file mode 100644 index 000000000..933e8ccd2 --- /dev/null +++ b/src/Auth/Http/ClientFactory.php @@ -0,0 +1,44 @@ +credentials = $credentials; + $this->httpClient = $httpClient ?: ClientFactory::build(); + } + + public function send( + RequestInterface $request, + array $options = [] + ): ResponseInterface { + foreach ($this->credentials->getRequestMetadata() as $name => $value) { + $request = $request->withHeader($name, $value); + } + return $this->httpClient->send($request, $options); + } + + public function sendAsync( + RequestInterface $request, + array $options = [] + ): PromiseInterface { + foreach ($this->credentials->getRequestMetadata() as $name => $value) { + $request = $request->withHeader($name, $value); + } + return $this->httpClient->sendAsync($request, $options); + } +} diff --git a/src/Auth/Jwt/FirebaseJwtClient.php b/src/Auth/Jwt/FirebaseJwtClient.php new file mode 100644 index 000000000..32c7be03b --- /dev/null +++ b/src/Auth/Jwt/FirebaseJwtClient.php @@ -0,0 +1,54 @@ +jwt = $jwt; + $this->jwk = $jwk; + } + + public function encode( + array $payload, + string $signingKey, + string $signingAlg, + ?string $keyId + ): string { + return $this->jwt->encode($payload, $signingKey, $signingAlg, $keyId); + } + + public function decode(string $jwt, array $keys, array $allowedAlgs): array + { + return (array) $this->jwt->decode($jwt, $keys, $allowedAlgs); + } + + public function parseKeySet(array $keySet): array + { + return $this->jwk->parseKeySet($keySet); + } +} diff --git a/src/ProjectIdProviderInterface.php b/src/Auth/Jwt/JwtClientInterface.php similarity index 62% rename from src/ProjectIdProviderInterface.php rename to src/Auth/Jwt/JwtClientInterface.php index 0a41f7832..dc6dea929 100644 --- a/src/ProjectIdProviderInterface.php +++ b/src/Auth/Jwt/JwtClientInterface.php @@ -15,18 +15,20 @@ * limitations under the License. */ -namespace Google\Auth; +declare(strict_types=1); -/** - * Describes a Credentials object which supports fetching the project ID. - */ -interface ProjectIdProviderInterface +namespace Google\Auth\Jwt; + +interface JwtClientInterface { - /** - * Get the project ID. - * - * @param callable $httpHandler Callback which delivers psr7 request - * @return string|null - */ - public function getProjectId(callable $httpHandler = null); + public function encode( + array $payload, + string $signingKey, + string $signingAlg, + ?string $keyId + ): string; + + public function decode(string $jwt, array $keys, array $allowedAlgs): array; + + public function parseKeySet(array $keySey): array; } diff --git a/src/OAuth2.php b/src/Auth/OAuth2.php similarity index 78% rename from src/OAuth2.php rename to src/Auth/OAuth2.php index a757b5a37..a919e26ea 100644 --- a/src/OAuth2.php +++ b/src/Auth/OAuth2.php @@ -1,6 +1,6 @@ null, + 'httpClient' => null, + 'jwtClient' => null, 'expiry' => self::DEFAULT_EXPIRY_SECONDS, 'extensionParams' => [], 'authorizationUri' => null, 'redirectUri' => null, 'tokenCredentialUri' => null, + 'tokenRevokeUri' => null, 'state' => null, 'username' => null, 'password' => null, 'clientId' => null, 'clientSecret' => null, + 'refreshToken' => null, 'issuer' => null, 'sub' => null, 'audience' => null, @@ -343,14 +358,39 @@ public function __construct(array $config) 'additionalClaims' => [], ], $config); + if (isset($opts['credentialsFile'])) { + if (!file_exists($opts['credentialsFile'])) { + throw new InvalidArgumentException('Unable to read credentialsFile'); + } + $creds = file_get_contents($opts['credentialsFile']); + $jsonKey = json_decode($creds, true); + if (!array_key_exists('type', $jsonKey)) { + throw new \InvalidArgumentException('json key is missing the type field'); + } + if (isset($jsonKey['client_id'])) { + $opts['clientId'] = $jsonKey['client_id']; + } + if (isset($jsonKey['client_secret'])) { + $opts['clientSecret'] = $jsonKey['client_secret']; + } + if (isset($jsonKey['refresh_token'])) { + $opts['refreshToken'] = $jsonKey['refresh_token']; + } + } + + $this->httpClient = $opts['httpClient'] ?: ClientFactory::build(); + $this->jwtClient = $opts['jwtClient'] + ?: new FirebaseJwtClient(new JWT(), new JWK()); $this->setAuthorizationUri($opts['authorizationUri']); $this->setRedirectUri($opts['redirectUri']); $this->setTokenCredentialUri($opts['tokenCredentialUri']); + $this->setTokenRevokeUri($opts['tokenRevokeUri']); $this->setState($opts['state']); $this->setUsername($opts['username']); $this->setPassword($opts['password']); $this->setClientId($opts['clientId']); $this->setClientSecret($opts['clientSecret']); + $this->setRefreshToken($opts['refreshToken']); $this->setIssuer($opts['issuer']); $this->setSub($opts['sub']); $this->setExpiry($opts['expiry']); @@ -361,49 +401,6 @@ public function __construct(array $config) $this->setScope($opts['scope']); $this->setExtensionParams($opts['extensionParams']); $this->setAdditionalClaims($opts['additionalClaims']); - $this->updateToken($opts); - } - - /** - * Verifies the idToken if present. - * - * - if none is present, return null - * - if present, but invalid, raises DomainException. - * - otherwise returns the payload in the idtoken as a PHP object. - * - * The behavior of this method varies depending on the version of - * `firebase/php-jwt` you are using. In versions lower than 3.0.0, if - * `$publicKey` is null, the key is decoded without being verified. In - * newer versions, if a public key is not given, this method will throw an - * `\InvalidArgumentException`. - * - * @param string $publicKey The public key to use to authenticate the token - * @param array $allowed_algs List of supported verification algorithms - * @throws \DomainException if the token is missing an audience. - * @throws \DomainException if the audience does not match the one set in - * the OAuth2 class instance. - * @throws \UnexpectedValueException If the token is invalid - * @throws SignatureInvalidException If the signature is invalid. - * @throws BeforeValidException If the token is not yet valid. - * @throws ExpiredException If the token has expired. - * @return null|object - */ - public function verifyIdToken($publicKey = null, $allowed_algs = array()) - { - $idToken = $this->getIdToken(); - if (is_null($idToken)) { - return null; - } - - $resp = $this->jwtDecode($idToken, $publicKey, $allowed_algs); - if (!property_exists($resp, 'aud')) { - throw new \DomainException('No audience found the id token'); - } - if ($resp->aud != $this->getAudience()) { - throw new \DomainException('Wrong audience present in the id token'); - } - - return $resp; } /** @@ -445,7 +442,7 @@ public function toJwt(array $config = []) } $assertion += $this->getAdditionalClaims(); - return $this->jwtEncode( + return $this->jwtClient->encode( $assertion, $this->getSigningKey(), $this->getSigningAlgorithm(), @@ -453,12 +450,28 @@ public function toJwt(array $config = []) ); } + /** + * Fetches the auth tokens based on the current state. + * + * @return array the response + */ + public function fetchAuthToken(): array + { + $response = $this->httpClient->send( + $this->generateCredentialsRequest() + ); + $credentials = $this->parseTokenResponse($response); + $this->setAuthToken($credentials); + + return $credentials; + } + /** * Generates a request for token credentials. * * @return RequestInterface the authorization Url. */ - public function generateCredentialsRequest() + public function generateCredentialsRequest(): RequestInterface { $uri = $this->getTokenCredentialUri(); if (is_null($uri)) { @@ -466,7 +479,7 @@ public function generateCredentialsRequest() } $grantType = $this->getGrantType(); - $params = array('grant_type' => $grantType); + $params = ['grant_type' => $grantType]; switch ($grantType) { case 'authorization_code': $params['code'] = $this->getCode(); @@ -511,25 +524,6 @@ public function generateCredentialsRequest() ); } - /** - * Fetches the auth tokens based on the current state. - * - * @param callable $httpHandler callback which delivers psr7 request - * @return array the response - */ - public function fetchAuthToken(callable $httpHandler = null) - { - if (is_null($httpHandler)) { - $httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient()); - } - - $response = $httpHandler($this->generateCredentialsRequest()); - $credentials = $this->parseTokenResponse($response); - $this->updateToken($credentials); - - return $credentials; - } - /** * Obtains a key that can used to cache the results of #fetchAuthToken. * @@ -537,7 +531,7 @@ public function fetchAuthToken(callable $httpHandler = null) * * @return string a key that may be used to cache the auth token. */ - public function getCacheKey() + public function getCacheKey(): ?string { if (is_array($this->scope)) { return implode(':', $this->scope); @@ -558,13 +552,13 @@ public function getCacheKey() * @return array the tokens parsed from the response body. * @throws \Exception */ - public function parseTokenResponse(ResponseInterface $resp) + private function parseTokenResponse(ResponseInterface $resp) { $body = (string)$resp->getBody(); if ($resp->hasHeader('Content-Type') && $resp->getHeaderLine('Content-Type') == 'application/x-www-form-urlencoded' ) { - $res = array(); + $res = []; parse_str($body, $res); return $res; @@ -579,18 +573,18 @@ public function parseTokenResponse(ResponseInterface $resp) } /** - * Updates an OAuth 2.0 client. + * Sets properties of the OAuth2 token, usually after loading from cache. * * Example: * ``` - * $oauth->updateToken([ + * $oauth->setAuthToken([ * 'refresh_token' => 'n4E9O119d', * 'access_token' => 'FJQbwq9', * 'expires_in' => 3600 * ]); * ``` * - * @param array $config + * @param array $authToken * The configuration parameters related to the token. * * - refresh_token @@ -612,7 +606,7 @@ public function parseTokenResponse(ResponseInterface $resp) * - issued_at * The timestamp that the token was issued at. */ - public function updateToken(array $config) + public function setAuthToken(array $authToken) { $opts = array_merge([ 'extensionParams' => [], @@ -621,10 +615,11 @@ public function updateToken(array $config) 'expires_in' => null, 'expires_at' => null, 'issued_at' => null, - ], $config); + ], $authToken); $this->setExpiresAt($opts['expires_at']); $this->setExpiresIn($opts['expires_in']); + // By default, the token is issued at `Time.now` when `expiresIn` is set, // but this can be used to supply a more precise time. if (!is_null($opts['issued_at'])) { @@ -633,6 +628,7 @@ public function updateToken(array $config) $this->setAccessToken($opts['access_token']); $this->setIdToken($opts['id_token']); + // The refresh token should only be updated if a value is explicitly // passed in, as some access token responses do not include a refresh // token. @@ -641,6 +637,32 @@ public function updateToken(array $config) } } + /** + * Revoke an OAuth2 access token or refresh token. This method will revoke the current access + * token, if a token isn't provided. + * + * @param string $token The token (access token or a refresh token) that should be revoked. + * @return bool Returns True if the revocation was successful, otherwise False. + */ + public function revoke(string $token): bool + { + if (is_null($this->getTokenRevokeUri())) { + throw new InvalidArgumentException( + 'requires an tokenRevokeUri to have been set' + ); + } + + $body = Psr7\stream_for(http_build_query(['token' => $token])); + $request = new Request('POST', $this->tokenRevokeUri, [ + 'Cache-Control' => 'no-store', + 'Content-Type' => 'application/x-www-form-urlencoded', + ], $body); + + $response = $this->httpClient->send($request); + + return $response->getStatusCode() == 200; + } + /** * Builds the authorization Uri that the user should be redirected to. * @@ -650,7 +672,7 @@ public function updateToken(array $config) */ public function buildFullAuthorizationUri(array $config = []) { - if (is_null($this->getAuthorizationUri())) { + if (empty($this->getAuthorizationUri())) { throw new InvalidArgumentException( 'requires an authorizationUri to have been set' ); @@ -698,25 +720,27 @@ public function buildFullAuthorizationUri(array $config = []) } /** - * Sets the authorization server's HTTP endpoint capable of authenticating + * Gets the authorization server's HTTP endpoint capable of authenticating * the end-user and obtaining authorization. * - * @param string $uri + * @return string */ - public function setAuthorizationUri($uri) + public function getAuthorizationUri(): ?string { - $this->authorizationUri = $this->coerceUri($uri); + return $this->authorizationUri + ? (string) $this->authorizationUri + : null; } /** - * Gets the authorization server's HTTP endpoint capable of authenticating + * Sets the authorization server's HTTP endpoint capable of authenticating * the end-user and obtaining authorization. * - * @return UriInterface + * @param string $uri */ - public function getAuthorizationUri() + public function setAuthorizationUri(?string $uri): void { - return $this->authorizationUri; + $this->authorizationUri = $this->coerceUri($uri); } /** @@ -725,9 +749,11 @@ public function getAuthorizationUri() * * @return string */ - public function getTokenCredentialUri() + public function getTokenCredentialUri(): ?string { - return $this->tokenCredentialUri; + return $this->tokenCredentialUri + ? (string) $this->tokenCredentialUri + : null; } /** @@ -736,19 +762,45 @@ public function getTokenCredentialUri() * * @param string $uri */ - public function setTokenCredentialUri($uri) + public function setTokenCredentialUri(?string $uri): void { $this->tokenCredentialUri = $this->coerceUri($uri); } + /** + * Gets the authorization server's HTTP endpoint capable of revoking access + * tokens. + * + * @return string + */ + public function getTokenRevokeUri(): ?string + { + return $this->tokenRevokeUri ? + (string) $this->tokenRevokeUri + : null; + } + + /** + * Sets the authorization server's HTTP endpoint capable of revoking access + * tokens. + * + * @param string $uri + */ + public function setTokenRevokeUri(?string $uri): void + { + $this->tokenRevokeUri = $this->coerceUri($uri); + } + /** * Gets the redirection URI used in the initial request. * * @return string */ - public function getRedirectUri() + public function getRedirectUri(): ?string { - return $this->redirectUri; + return $this->redirectUri + ? (string) $this->redirectUri + : null; } /** @@ -756,7 +808,7 @@ public function getRedirectUri() * * @param string $uri */ - public function setRedirectUri($uri) + public function setRedirectUri(?string $uri): void { if (is_null($uri)) { $this->redirectUri = null; @@ -781,7 +833,7 @@ public function setRedirectUri($uri) * * @return string */ - public function getScope() + public function getScope(): ?string { if (is_null($this->scope)) { return $this->scope; @@ -797,7 +849,7 @@ public function getScope() * @param string|array $scope * @throws InvalidArgumentException */ - public function setScope($scope) + public function setScope($scope): void { if (is_null($scope)) { $this->scope = null; @@ -825,7 +877,7 @@ public function setScope($scope) * * @return string */ - public function getGrantType() + public function getGrantType(): ?string { if (!is_null($this->grantType)) { return $this->grantType; @@ -858,7 +910,7 @@ public function getGrantType() * @param $grantType * @throws InvalidArgumentException */ - public function setGrantType($grantType) + public function setGrantType($grantType): void { if (in_array($grantType, self::$knownGrantTypes)) { $this->grantType = $grantType; @@ -878,7 +930,7 @@ public function setGrantType($grantType) * * @return string */ - public function getState() + public function getState(): ?string { return $this->state; } @@ -888,7 +940,7 @@ public function getState() * * @param string $state */ - public function setState($state) + public function setState(?string $state): void { $this->state = $state; } @@ -896,7 +948,7 @@ public function setState($state) /** * Gets the authorization code issued to this client. */ - public function getCode() + public function getCode(): ?string { return $this->code; } @@ -906,7 +958,7 @@ public function getCode() * * @param string $code */ - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } @@ -914,7 +966,7 @@ public function setCode($code) /** * Gets the resource owner's username. */ - public function getUsername() + public function getUsername(): ?string { return $this->username; } @@ -924,7 +976,7 @@ public function getUsername() * * @param string $username */ - public function setUsername($username) + public function setUsername(?string $username): void { $this->username = $username; } @@ -932,7 +984,7 @@ public function setUsername($username) /** * Gets the resource owner's password. */ - public function getPassword() + public function getPassword(): ?string { return $this->password; } @@ -942,7 +994,7 @@ public function getPassword() * * @param $password */ - public function setPassword($password) + public function setPassword(?string $password): void { $this->password = $password; } @@ -951,7 +1003,7 @@ public function setPassword($password) * Sets a unique identifier issued to the client to identify itself to the * authorization server. */ - public function getClientId() + public function getClientId(): ?string { return $this->clientId; } @@ -962,7 +1014,7 @@ public function getClientId() * * @param $clientId */ - public function setClientId($clientId) + public function setClientId(?string $clientId): void { $this->clientId = $clientId; } @@ -971,7 +1023,7 @@ public function setClientId($clientId) * Gets a shared symmetric secret issued by the authorization server, which * is used to authenticate the client. */ - public function getClientSecret() + public function getClientSecret(): ?string { return $this->clientSecret; } @@ -982,7 +1034,7 @@ public function getClientSecret() * * @param $clientSecret */ - public function setClientSecret($clientSecret) + public function setClientSecret(?string $clientSecret): void { $this->clientSecret = $clientSecret; } @@ -990,7 +1042,7 @@ public function setClientSecret($clientSecret) /** * Gets the Issuer ID when using assertion profile. */ - public function getIssuer() + public function getIssuer(): ?string { return $this->issuer; } @@ -1000,7 +1052,7 @@ public function getIssuer() * * @param string $issuer */ - public function setIssuer($issuer) + public function setIssuer(?string $issuer): void { $this->issuer = $issuer; } @@ -1008,7 +1060,7 @@ public function setIssuer($issuer) /** * Gets the target sub when issuing assertions. */ - public function getSub() + public function getSub(): ?string { return $this->sub; } @@ -1018,7 +1070,7 @@ public function getSub() * * @param string $sub */ - public function setSub($sub) + public function setSub(?string $sub): void { $this->sub = $sub; } @@ -1026,7 +1078,7 @@ public function setSub($sub) /** * Gets the target audience when issuing assertions. */ - public function getAudience() + public function getAudience(): ?string { return $this->audience; } @@ -1036,7 +1088,7 @@ public function getAudience() * * @param string $audience */ - public function setAudience($audience) + public function setAudience(?string $audience): void { $this->audience = $audience; } @@ -1044,7 +1096,7 @@ public function setAudience($audience) /** * Gets the signing key when using an assertion profile. */ - public function getSigningKey() + public function getSigningKey(): ?string { return $this->signingKey; } @@ -1054,7 +1106,7 @@ public function getSigningKey() * * @param string $signingKey */ - public function setSigningKey($signingKey) + public function setSigningKey(?string $signingKey): void { $this->signingKey = $signingKey; } @@ -1064,7 +1116,7 @@ public function setSigningKey($signingKey) * * @return string */ - public function getSigningKeyId() + public function getSigningKeyId(): ?string { return $this->signingKeyId; } @@ -1074,7 +1126,7 @@ public function getSigningKeyId() * * @param string $signingKeyId */ - public function setSigningKeyId($signingKeyId) + public function setSigningKeyId(?string $signingKeyId): void { $this->signingKeyId = $signingKeyId; } @@ -1084,7 +1136,7 @@ public function setSigningKeyId($signingKeyId) * * @return string */ - public function getSigningAlgorithm() + public function getSigningAlgorithm(): ?string { return $this->signingAlgorithm; } @@ -1094,7 +1146,7 @@ public function getSigningAlgorithm() * * @param string $signingAlgorithm */ - public function setSigningAlgorithm($signingAlgorithm) + public function setSigningAlgorithm(?string $signingAlgorithm): void { if (is_null($signingAlgorithm)) { $this->signingAlgorithm = null; @@ -1109,7 +1161,7 @@ public function setSigningAlgorithm($signingAlgorithm) * Gets the set of parameters used by extension when using an extension * grant type. */ - public function getExtensionParams() + public function getExtensionParams(): array { return $this->extensionParams; } @@ -1120,7 +1172,7 @@ public function getExtensionParams() * * @param $extensionParams */ - public function setExtensionParams($extensionParams) + public function setExtensionParams(array $extensionParams) { $this->extensionParams = $extensionParams; } @@ -1128,7 +1180,7 @@ public function setExtensionParams($extensionParams) /** * Gets the number of seconds assertions are valid for. */ - public function getExpiry() + public function getExpiry(): ?int { return $this->expiry; } @@ -1138,7 +1190,7 @@ public function getExpiry() * * @param int $expiry */ - public function setExpiry($expiry) + public function setExpiry(int $expiry): void { $this->expiry = $expiry; } @@ -1146,7 +1198,7 @@ public function setExpiry($expiry) /** * Gets the lifetime of the access token in seconds. */ - public function getExpiresIn() + public function getExpiresIn(): ?int { return $this->expiresIn; } @@ -1156,7 +1208,7 @@ public function getExpiresIn() * * @param int $expiresIn */ - public function setExpiresIn($expiresIn) + public function setExpiresIn(?int $expiresIn): void { if (is_null($expiresIn)) { $this->expiresIn = null; @@ -1170,9 +1222,9 @@ public function setExpiresIn($expiresIn) /** * Gets the time the current access token expires at. * - * @return int + * @return int|null */ - public function getExpiresAt() + public function getExpiresAt(): ?int { if (!is_null($this->expiresAt)) { return $this->expiresAt; @@ -1190,12 +1242,12 @@ public function getExpiresAt() * * @return bool */ - public function isExpired() + public function isExpired(): bool { $expiration = $this->getExpiresAt(); $now = time(); - return !is_null($expiration) && $now >= $expiration; + return is_null($expiration) || $now >= $expiration; } /** @@ -1203,15 +1255,17 @@ public function isExpired() * * @param int $expiresAt */ - public function setExpiresAt($expiresAt) + public function setExpiresAt(?int $expiresAt): void { $this->expiresAt = $expiresAt; } /** * Gets the time the current access token was issued at. + * + * @return int|null */ - public function getIssuedAt() + public function getIssuedAt(): ?int { return $this->issuedAt; } @@ -1221,15 +1275,17 @@ public function getIssuedAt() * * @param int $issuedAt */ - public function setIssuedAt($issuedAt) + public function setIssuedAt(int $issuedAt): void { $this->issuedAt = $issuedAt; } /** * Gets the current access token. + * + * @return string|null */ - public function getAccessToken() + public function getAccessToken(): ?string { return $this->accessToken; } @@ -1237,17 +1293,19 @@ public function getAccessToken() /** * Sets the current access token. * - * @param string $accessToken + * @param string|null $accessToken */ - public function setAccessToken($accessToken) + public function setAccessToken(string $accessToken = null): void { $this->accessToken = $accessToken; } /** * Gets the current ID token. + * + * @return string|null */ - public function getIdToken() + public function getIdToken(): ?string { return $this->idToken; } @@ -1255,17 +1313,19 @@ public function getIdToken() /** * Sets the current ID token. * - * @param $idToken + * @param string|null $idToken */ - public function setIdToken($idToken) + public function setIdToken(string $idToken = null): void { $this->idToken = $idToken; } /** * Gets the refresh token associated with the current access token. + * + * @return string|null */ - public function getRefreshToken() + public function getRefreshToken(): ?string { return $this->refreshToken; } @@ -1273,126 +1333,46 @@ public function getRefreshToken() /** * Sets the refresh token associated with the current access token. * - * @param $refreshToken + * @param string|null $refreshToken */ - public function setRefreshToken($refreshToken) + public function setRefreshToken(?string $refreshToken): void { $this->refreshToken = $refreshToken; } - /** - * Sets additional claims to be included in the JWT token - * - * @param array $additionalClaims - */ - public function setAdditionalClaims(array $additionalClaims) - { - $this->additionalClaims = $additionalClaims; - } - /** * Gets the additional claims to be included in the JWT token. * * @return array */ - public function getAdditionalClaims() + public function getAdditionalClaims(): array { return $this->additionalClaims; } /** - * The expiration of the last received token. - * - * @return array|null - */ - public function getLastReceivedToken() - { - if ($token = $this->getAccessToken()) { - // the bare necessity of an auth token - $authToken = [ - 'access_token' => $token, - 'expires_at' => $this->getExpiresAt(), - ]; - } elseif ($idToken = $this->getIdToken()) { - $authToken = [ - 'id_token' => $idToken, - 'expires_at' => $this->getExpiresAt(), - ]; - } else { - return null; - } - - if ($expiresIn = $this->getExpiresIn()) { - $authToken['expires_in'] = $expiresIn; - } - if ($issuedAt = $this->getIssuedAt()) { - $authToken['issued_at'] = $issuedAt; - } - if ($refreshToken = $this->getRefreshToken()) { - $authToken['refresh_token'] = $refreshToken; - } - - return $authToken; - } - - /** - * Get the client ID. - * - * Alias of {@see Google\Auth\OAuth2::getClientId()}. + * Sets additional claims to be included in the JWT token * - * @param callable $httpHandler - * @return string - * @access private + * @param array $additionalClaims */ - public function getClientName(callable $httpHandler = null) + public function setAdditionalClaims(array $additionalClaims): void { - return $this->getClientId(); + $this->additionalClaims = $additionalClaims; } /** - * @todo handle uri as array - * * @param string $uri * @return null|UriInterface */ - private function coerceUri($uri) + private function coerceUri(?string $uri): ?UriInterface { if (is_null($uri)) { - return; + return null; } return Psr7\uri_for($uri); } - /** - * @param string $idToken - * @param string|array|null $publicKey - * @param array $allowedAlgs - * @return object - */ - private function jwtDecode($idToken, $publicKey, $allowedAlgs) - { - if (class_exists('Firebase\JWT\JWT')) { - return \Firebase\JWT\JWT::decode($idToken, $publicKey, $allowedAlgs); - } - - return \JWT::decode($idToken, $publicKey, $allowedAlgs); - } - - private function jwtEncode($assertion, $signingKey, $signingAlgorithm, $signingKeyId = null) - { - if (class_exists('Firebase\JWT\JWT')) { - return \Firebase\JWT\JWT::encode( - $assertion, - $signingKey, - $signingAlgorithm, - $signingKeyId - ); - } - - return \JWT::encode($assertion, $signingKey, $signingAlgorithm, $signingKeyId); - } - /** * Determines if the URI is absolute based on its scheme and host or path * (RFC 3986). @@ -1400,7 +1380,7 @@ private function jwtEncode($assertion, $signingKey, $signingAlgorithm, $signingK * @param string $uri * @return bool */ - private function isAbsoluteUri($uri) + private function isAbsoluteUri(string $uri): bool { $uri = $this->coerceUri($uri); @@ -1409,9 +1389,8 @@ private function isAbsoluteUri($uri) /** * @param array $params - * @return array */ - private function addClientCredentials(&$params) + private function addClientCredentials(array &$params): void { $clientId = $this->getClientId(); $clientSecret = $this->getClientSecret(); @@ -1420,7 +1399,5 @@ private function addClientCredentials(&$params) $params['client_id'] = $clientId; $params['client_secret'] = $clientSecret; } - - return $params; } } diff --git a/src/ServiceAccountSignerTrait.php b/src/Auth/SignBlob/PrivateKeySignBlobTrait.php similarity index 51% rename from src/ServiceAccountSignerTrait.php rename to src/Auth/SignBlob/PrivateKeySignBlobTrait.php index 72fb14280..3d5fc777b 100644 --- a/src/ServiceAccountSignerTrait.php +++ b/src/Auth/SignBlob/PrivateKeySignBlobTrait.php @@ -1,6 +1,6 @@ auth->getSigningKey(); - - $signedString = ''; - if (class_exists('\\phpseclib\\Crypt\\RSA') && !$forceOpenssl) { - $rsa = new RSA; - $rsa->loadKey($privateKey); - $rsa->setSignatureMode(RSA::SIGNATURE_PKCS1); - $rsa->setHash('sha256'); - - $signedString = $rsa->sign($stringToSign); - } elseif (extension_loaded('openssl')) { - openssl_sign($stringToSign, $signedString, $privateKey, 'sha256WithRSAEncryption'); - } else { - // @codeCoverageIgnoreStart + private function signBlobWithPrivateKey( + string $stringToSign, + string $privateKey + ): string { + if (!extension_loaded('openssl')) { throw new \RuntimeException('OpenSSL is not installed.'); } - // @codeCoverageIgnoreEnd + $signedString = ''; + openssl_sign($stringToSign, $signedString, $privateKey, 'sha256WithRSAEncryption'); return base64_encode($signedString); } diff --git a/src/Iam.php b/src/Auth/SignBlob/ServiceAccountApiSignBlobTrait.php similarity index 65% rename from src/Iam.php rename to src/Auth/SignBlob/ServiceAccountApiSignBlobTrait.php index 300c0b97e..0046b872b 100644 --- a/src/Iam.php +++ b/src/Auth/SignBlob/ServiceAccountApiSignBlobTrait.php @@ -1,6 +1,6 @@ httpHandler = $httpHandler - ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient()); - } - /** * Sign a string using the IAM signBlob API. * @@ -53,23 +37,30 @@ public function __construct(callable $httpHandler = null) * `iam.serviceAccounts.signBlob` permission, part of the "Service Account * Token Creator" IAM role. * + * @param string $stringToSign The string to be signed. * @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 $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); - $uri = self::IAM_API_ROOT . '/' . sprintf(self::SIGN_BLOB_PATH, $name); + private function signBlobWithServiceAccountApi( + string $stringToSign, + string $email, + string $accessToken, + ClientInterface $httpClient, + array $delegates = [] + ): string { + $name = sprintf('projects/-/serviceAccounts/%s', $email); + $uri = sprintf( + 'https://iamcredentials.googleapis.com/v1/%s:signBlob?alt=json', + $name + ); if ($delegates) { foreach ($delegates as &$delegate) { - $delegate = sprintf(self::SERVICE_ACCOUNT_NAME, $delegate); + $delegate = sprintf('projects/-/serviceAccounts/%s', $delegate); } } else { $delegates = [$name]; @@ -91,7 +82,7 @@ public function signBlob($email, $accessToken, $stringToSign, array $delegates = Psr7\stream_for(json_encode($body)) ); - $res = $httpHandler($request); + $res = $httpClient->send($request); $body = json_decode((string) $res->getBody(), true); return $body['signedBlob']; diff --git a/src/SignBlobInterface.php b/src/Auth/SignBlob/SignBlobInterface.php similarity index 60% rename from src/SignBlobInterface.php rename to src/Auth/SignBlob/SignBlobInterface.php index 5f2c94414..4f7ecee1d 100644 --- a/src/SignBlobInterface.php +++ b/src/Auth/SignBlob/SignBlobInterface.php @@ -1,6 +1,6 @@ cache)) { - return; - } - - $key = $this->getFullCacheKey($k); - if (is_null($key)) { - return; - } - - $cacheItem = $this->cache->getItem($key); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - } - - /** - * Saves the value in the cache when that is available. - */ - private function setCachedValue($k, $v) - { - if (is_null($this->cache)) { - return; - } - - $key = $this->getFullCacheKey($k); - if (is_null($key)) { - return; - } - - $cacheItem = $this->cache->getItem($key); - $cacheItem->set($v); - $cacheItem->expiresAfter($this->cacheConfig['lifetime']); - return $this->cache->save($cacheItem); - } - - private function getFullCacheKey($key) - { - if (is_null($key)) { - return; - } - - $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; - } -} diff --git a/src/Credentials/AppIdentityCredentials.php b/src/Credentials/AppIdentityCredentials.php deleted file mode 100644 index 829344d03..000000000 --- a/src/Credentials/AppIdentityCredentials.php +++ /dev/null @@ -1,230 +0,0 @@ -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 - */ - protected $lastReceivedToken; - - /** - * Array of OAuth2 scopes to be requested. - * - * @var array - */ - private $scope; - - /** - * @var string - */ - private $clientName; - - /** - * @param array $scope One or more scopes. - */ - public function __construct($scope = array()) - { - $this->scope = $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 A set of auth related metadata, containing the following - * keys: - * - access_token (string) - * - expiration_time (string) - */ - public function fetchAuthToken(callable $httpHandler = null) - { - try { - $this->checkAppEngineContext(); - } catch (\Exception $e) { - return []; - } - - // AppIdentityService expects an array when multiple scopes are supplied - $scope = is_array($this->scope) ? $this->scope : explode(' ', $this->scope); - - $token = AppIdentityService::getAccessToken($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(); - - 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 $httpHander = null) - { - try { - $this->checkAppEngineContext(); - } catch (\Exception $e) { - return null; - } - - 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) { - $this->clientName = AppIdentityService::getServiceAccountName(); - } - - return $this->clientName; - } - - /** - * @return array|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 ''; - } - - 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' - ); - } - } -} diff --git a/src/Credentials/GCECredentials.php b/src/Credentials/GCECredentials.php deleted file mode 100644 index e985e3ca2..000000000 --- a/src/Credentials/GCECredentials.php +++ /dev/null @@ -1,542 +0,0 @@ -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 -{ - // 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 header whose presence indicates GCE presence. - */ - const FLAVOR_HEADER = 'Metadata-Flavor'; - - /** - * 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. - */ - protected $lastReceivedToken; - - /** - * @var string|null - */ - private $clientName; - - /** - * @var string|null - */ - private $projectId; - - /** - * @var Iam|null - */ - private $iam; - - /** - * @var string - */ - private $tokenUri; - - /** - * @var string - */ - private $targetAudience; - - /** - * @var string|null - */ - private $quotaProject; - - /** - * @var string|null - */ - private $serviceAccountIdentity; - - /** - * @param Iam $iam [optional] An IAM instance. - * @param string|array $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". - */ - public function __construct( - Iam $iam = null, - $scope = null, - $targetAudience = null, - $quotaProject = null, - $serviceAccountIdentity = 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; - } - - /** - * 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; - } - - /** - * 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(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'] - ), - ['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) { - } - } - return false; - } - - /** - * 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 A set of auth related metadata, based on the token type. - * - * Access tokens have the following keys: - * - access_token (string) - * - expires_in (int) - * - token_type (string) - * ID tokens have the following keys: - * - id_token (string) - * - * @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 array(); // return an empty array with no access token - } - - $response = $this->getFromMetadata($httpHandler, $this->tokenUri); - - if ($this->targetAudience) { - return ['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; - } - - /** - * @return string - */ - public function getCacheKey() - { - return self::cacheKey; - } - - /** - * @return array|null - */ - public function getLastReceivedToken() - { - if ($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; - } - - /** - * 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. - * @return string - */ - public function signBlob($stringToSign, $forceOpenSsl = false) - { - $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 ?: new Iam($httpHandler); - - $email = $this->getClientName($httpHandler); - - $previousToken = $this->getLastReceivedToken(); - $accessToken = $previousToken - ? $previousToken['access_token'] - : $this->fetchAuthToken($httpHandler)['access_token']; - - return $signer->signBlob($email, $accessToken, $stringToSign); - } - - /** - * 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 value of a GCE metadata server URI. - * - * @param callable $httpHandler An HTTP Handler to deliver PSR7 requests. - * @param string $uri The metadata URI. - * @return string - */ - private function getFromMetadata(callable $httpHandler, $uri) - { - $resp = $httpHandler( - new Request( - 'GET', - $uri, - [self::FLAVOR_HEADER => 'Google'] - ) - ); - - return (string) $resp->getBody(); - } - - /** - * Get the quota project used for this API request - * - * @return string|null - */ - public function getQuotaProject() - { - return $this->quotaProject; - } -} diff --git a/src/Credentials/IAMCredentials.php b/src/Credentials/IAMCredentials.php deleted file mode 100644 index 5f055d842..000000000 --- a/src/Credentials/IAMCredentials.php +++ /dev/null @@ -1,91 +0,0 @@ -selector = $selector; - $this->token = $token; - } - - /** - * export a callback function which updates runtime metadata. - * - * @return array updateMetadata function - */ - public function getUpdateMetadataFunc() - { - return array($this, 'updateMetadata'); - } - - /** - * Updates metadata with the appropriate header metadata. - * - * @param array $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 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; - } -} diff --git a/src/CredentialsLoader.php b/src/CredentialsLoader.php deleted file mode 100644 index 9b7f4c416..000000000 --- a/src/CredentialsLoader.php +++ /dev/null @@ -1,250 +0,0 @@ -setDefaultOption('auth', 'google_auth'); - $subscriber = new Subscriber\AuthTokenSubscriber( - $fetcher, - $httpHandler, - $tokenCallback - ); - $client->getEmitter()->attach($subscriber); - return $client; - } - - $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(); - } - - /** - * export a callback function which updates runtime metadata. - * - * @return array updateMetadata function - * @deprecated - */ - public function getUpdateMetadataFunc() - { - return array($this, 'updateMetadata'); - } - - /** - * Updates metadata with the authorization token. - * - * @param array $metadata metadata hashmap - * @param string $authUri optional auth uri - * @param callable $httpHandler callback which delivers psr7 request - * @return array updated metadata hashmap - */ - public function updateMetadata( - $metadata, - $authUri = null, - callable $httpHandler = null - ) { - if (isset($metadata[self::AUTH_METADATA_KEY])) { - // Auth metadata has already been set - return $metadata; - } - $result = $this->fetchAuthToken($httpHandler); - if (!isset($result['access_token'])) { - return $metadata; - } - $metadata_copy = $metadata; - $metadata_copy[self::AUTH_METADATA_KEY] = array('Bearer ' . $result['access_token']); - - return $metadata_copy; - } -} diff --git a/src/FetchAuthTokenCache.php b/src/FetchAuthTokenCache.php deleted file mode 100644 index 5dfad60cd..000000000 --- a/src/FetchAuthTokenCache.php +++ /dev/null @@ -1,263 +0,0 @@ -fetcher = $fetcher; - $this->cache = $cache; - $this->cacheConfig = array_merge([ - 'lifetime' => 1500, - 'prefix' => '', - ], (array) $cacheConfig); - } - - /** - * 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 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|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) - { - 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' - ); - } - - 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(); - } - } - - /* - * 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' - ); - } - - return $this->fetcher->getProjectId($httpHandler); - } - - /** - * Updates metadata with the authorization token. - * - * @param array $metadata metadata hashmap - * @param string $authUri optional auth uri - * @param callable $httpHandler callback which delivers psr7 request - * @return array 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'] - ]; - } - } - - $newMetadata = $this->fetcher->updateMetadata( - $metadata, - $authUri, - $httpHandler - ); - - if (!$cached && $token = $this->fetcher->getLastReceivedToken()) { - $this->saveAuthTokenInCache($token, $authUri); - } - - return $newMetadata; - } - - 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() < $cached['expires_at']) { - // access token is not expired - return $cached; - } - } - - return null; - } - - 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); - } - } -} diff --git a/src/GCECache.php b/src/GCECache.php deleted file mode 100644 index 82123ecd5..000000000 --- a/src/GCECache.php +++ /dev/null @@ -1,92 +0,0 @@ -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; - } -} diff --git a/src/GetQuotaProjectInterface.php b/src/GetQuotaProjectInterface.php deleted file mode 100644 index 517f062e7..000000000 --- a/src/GetQuotaProjectInterface.php +++ /dev/null @@ -1,33 +0,0 @@ -client = $client; + } + + /** + * Accepts a PSR-7 request and an array of options and returns a PSR-7 response. + * + * @param RequestInterface $request + * @param array $options + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function send( + RequestInterface $request, + array $options = [] + ): ResponseInterface { + return $this->client->send($request, $options); + } + + /** + * Accepts a PSR-7 request and an array of options and returns a PromiseInterface + * + * @param \Psr\Http\Message\RequestInterface $request + * @param array $options + * + * @return \Google\Http\Promise\PromiseInterface + * @throws \LogicException + */ + public function sendAsync( + RequestInterface $request, + array $options = [] + ): PromiseInterface { + return new GuzzlePromise($this->client->sendAsync($request, $options)); + } + + private function getGuzzleMajorVersion(): int + { + if (defined('GuzzleHttp\ClientInterface::VERSION')) { + // Guzzle 4 (unsupported), 5 (unsupported), and 6 + return GuzzleClientInterface::VERSION[0]; + } + if (defined('GuzzleHttp\ClientInterface::MAJOR_VERSION')) { + // Guzzle 7 + return GuzzleClientInterface::MAJOR_VERSION; + } + throw new \LogicException('Unable to detect Guzzle version'); + } +} diff --git a/src/Http/Client/Psr18Client.php b/src/Http/Client/Psr18Client.php new file mode 100644 index 000000000..6ccaa9346 --- /dev/null +++ b/src/Http/Client/Psr18Client.php @@ -0,0 +1,73 @@ +client = $client; + } + + /** + * Accepts a PSR-7 request and an array of options and returns a PSR-7 response. + * + * @param \Psr\Http\Message\RequestInterface $request + * @param array $options + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function send( + RequestInterface $request, + array $options = [] + ): ResponseInterface { + if (!empty($options)) { + // Ignore per-request options + } + return $this->client->sendRequest($request); + } + + /** + * Not implemented for PSR-18 clients + * + * @param \Psr\Http\Message\RequestInterface $request + * + * @throws \RuntimeException + */ + public function sendAsync( + RequestInterface $request, + array $options = [] + ): PromiseInterface { + throw new \RuntimeException('async not supported for PSR-18 clients'); + } +} diff --git a/src/Http/ClientInterface.php b/src/Http/ClientInterface.php new file mode 100644 index 000000000..08fa26300 --- /dev/null +++ b/src/Http/ClientInterface.php @@ -0,0 +1,52 @@ +promise = $promise; + } + + /** + * Appends fulfillment and rejection handlers to the promise, and returns + * a new promise resolving to the return value of the called handler. + * + * @param callable $onFulfilled Invoked when the promise fulfills. + * @param callable $onRejected Invoked when the promise is rejected. + * + * @return PromiseInterface + */ + public function then( + callable $onFulfilled = null, + callable $onRejected = null + ): PromiseInterface { + return $this->promise->then($onFulfilled, $onRejected); + } + + /** + * Appends a rejection handler callback to the promise, and returns a new + * promise resolving to the return value of the callback if it is called, + * or to its original fulfillment value if the promise is instead + * fulfilled. + * + * @param callable $onRejected Invoked when the promise is rejected. + * + * @return PromiseInterface + */ + public function otherwise(callable $onRejected): PromiseInterface + { + return $this->promise->otherwise($onRejected); + } + + /** + * Get the state of the promise ("pending", "rejected", or "fulfilled"). + * + * The three states can be checked against the constants defined on + * PromiseInterface: PENDING, FULFILLED, and REJECTED. + * + * @return string + */ + public function getState(): string + { + return $this->promise->getState(); + } + + /** + * Resolve the promise with the given value. + * + * @param mixed $value + * @throws \RuntimeException if the promise is already resolved. + */ + public function resolve($value) + { + return $this->promise->resolve($value); + } + + /** + * Reject the promise with the given reason. + * + * @param mixed $reason + * @throws \RuntimeException if the promise is already resolved. + */ + public function reject($reason) + { + return $this->promise->reject($reason); + } + + /** + * Cancels the promise if possible. + * + * @link https://github.com/promises-aplus/cancellation-spec/issues/7 + */ + public function cancel() + { + return $this->promise->cancel(); + } + + /** + * Waits until the promise completes if possible. + * + * Pass $unwrap as true to unwrap the result of the promise, either + * returning the resolved value or throwing the rejected exception. + * + * If the promise cannot be waited on, then the promise will be rejected. + * + * @param bool $unwrap + * + * @return mixed + * @throws \LogicException if the promise has no wait function or if the + * promise does not settle after waiting. + */ + public function wait(bool $unwrap = true) + { + return $this->promise->wait($unwrap); + } +} diff --git a/src/Http/PromiseInterface.php b/src/Http/PromiseInterface.php new file mode 100644 index 000000000..e38cceac5 --- /dev/null +++ b/src/Http/PromiseInterface.php @@ -0,0 +1,105 @@ +client = $client; - } - - /** - * Accepts a PSR-7 Request and an array of options and returns a PSR-7 response. - * - * @param RequestInterface $request - * @param array $options - * @return ResponseInterface - */ - public function __invoke(RequestInterface $request, array $options = []) - { - $response = $this->client->send( - $this->createGuzzle5Request($request, $options) - ); - - return $this->createPsr7Response($response); - } - - /** - * Accepts a PSR-7 request and an array of options and returns a PromiseInterface - * - * @param RequestInterface $request - * @param array $options - * @return Promise - */ - public function async(RequestInterface $request, array $options = []) - { - if (!class_exists('GuzzleHttp\Promise\Promise')) { - throw new Exception('Install guzzlehttp/promises to use async with Guzzle 5'); - } - - $futureResponse = $this->client->send( - $this->createGuzzle5Request( - $request, - ['future' => true] + $options - ) - ); - - $promise = new Promise( - function () use ($futureResponse) { - try { - $futureResponse->wait(); - } catch (Exception $e) { - // The promise is already delivered when the exception is - // thrown, so don't rethrow it. - } - }, - [$futureResponse, 'cancel'] - ); - - $futureResponse->then([$promise, 'resolve'], [$promise, 'reject']); - - return $promise->then( - function (Guzzle5ResponseInterface $response) { - // Adapt the Guzzle 5 Response to a PSR-7 Response. - return $this->createPsr7Response($response); - }, - function (Exception $e) { - return new RejectedPromise($e); - } - ); - } - - private function createGuzzle5Request(RequestInterface $request, array $options) - { - return $this->client->createRequest( - $request->getMethod(), - $request->getUri(), - array_merge_recursive([ - 'headers' => $request->getHeaders(), - 'body' => $request->getBody(), - ], $options) - ); - } - - private function createPsr7Response(Guzzle5ResponseInterface $response) - { - return new Response( - $response->getStatusCode(), - $response->getHeaders() ?: [], - $response->getBody(), - $response->getProtocolVersion(), - $response->getReasonPhrase() - ); - } -} diff --git a/src/HttpHandler/Guzzle6HttpHandler.php b/src/HttpHandler/Guzzle6HttpHandler.php deleted file mode 100644 index aaa7b4385..000000000 --- a/src/HttpHandler/Guzzle6HttpHandler.php +++ /dev/null @@ -1,62 +0,0 @@ -client = $client; - } - - /** - * Accepts a PSR-7 request and an array of options and returns a PSR-7 response. - * - * @param RequestInterface $request - * @param array $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 $options - * - * @return \GuzzleHttp\Promise\PromiseInterface - */ - public function async(RequestInterface $request, array $options = []) - { - return $this->client->sendAsync($request, $options); - } -} diff --git a/src/HttpHandler/Guzzle7HttpHandler.php b/src/HttpHandler/Guzzle7HttpHandler.php deleted file mode 100644 index e84f6603b..000000000 --- a/src/HttpHandler/Guzzle7HttpHandler.php +++ /dev/null @@ -1,21 +0,0 @@ -' - */ -class AuthTokenMiddleware -{ - /** - * @var callback - */ - private $httpHandler; - - /** - * @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 = [...]; - * $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 = $request->withHeader('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 - */ - 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']; - } - } - - private function getQuotaProject() - { - if ($this->fetcher instanceof GetQuotaProjectInterface) { - return $this->fetcher->getQuotaProject(); - } - } -} diff --git a/src/Middleware/ScopedAccessTokenMiddleware.php b/src/Middleware/ScopedAccessTokenMiddleware.php deleted file mode 100644 index ecbb6596b..000000000 --- a/src/Middleware/ScopedAccessTokenMiddleware.php +++ /dev/null @@ -1,175 +0,0 @@ -' - */ -class ScopedAccessTokenMiddleware -{ - use CacheTrait; - - const DEFAULT_CACHE_LIFETIME = 1500; - - /** - * @var CacheItemPoolInterface - */ - private $cache; - - /** - * @var array configuration - */ - private $cacheConfig; - - /** - * @var callable - */ - private $tokenFunc; - - /** - * @var array|string - */ - private $scopes; - - /** - * Creates a new ScopedAccessTokenMiddleware. - * - * @param callable $tokenFunc a token generator function - * @param array|string $scopes the token authentication scopes - * @param array $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; - } -} diff --git a/src/Middleware/SimpleMiddleware.php b/src/Middleware/SimpleMiddleware.php deleted file mode 100644 index 5104542b9..000000000 --- a/src/Middleware/SimpleMiddleware.php +++ /dev/null @@ -1,92 +0,0 @@ -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 = Psr7\parse_query($request->getUri()->getQuery()); - $params = array_merge($query, $this->config); - $uri = $request->getUri()->withQuery(Psr7\build_query($params)); - $request = $request->withUri($uri); - - return $handler($request, $options); - }; - } -} diff --git a/src/Subscriber/AuthTokenSubscriber.php b/src/Subscriber/AuthTokenSubscriber.php deleted file mode 100644 index bc529d22b..000000000 --- a/src/Subscriber/AuthTokenSubscriber.php +++ /dev/null @@ -1,136 +0,0 @@ -' - */ -class AuthTokenSubscriber implements SubscriberInterface -{ - /** - * @var callable - */ - private $httpHandler; - - /** - * @var FetchAuthTokenInterface - */ - private $fetcher; - - /** - * @var callable - */ - private $tokenCallback; - - /** - * Creates a new AuthTokenSubscriber. - * - * @param FetchAuthTokenInterface $fetcher is used to fetch the auth token - * @param callable $httpHandler (optional) http client to fetch the token. - * @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; - } - - /** - * @return array - */ - public function getEvents() - { - return ['before' => ['onBefore', RequestEvents::SIGN_REQUEST]]; - } - - /** - * Updates the request with an Authorization header when auth is 'fetched_auth_token'. - * - * Example: - * ``` - * use GuzzleHttp\Client; - * use Google\Auth\OAuth2; - * use Google\Auth\Subscriber\AuthTokenSubscriber; - * - * $config = [...]; - * $oauth2 = new OAuth2($config) - * $subscriber = new AuthTokenSubscriber($oauth2); - * - * $client = new Client([ - * 'base_url' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/', - * 'defaults' => ['auth' => 'google_auth'] - * ]); - * $client->getEmitter()->attach($subscriber); - * - * $res = $client->get('myproject/taskqueues/myqueue'); - * ``` - * - * @param BeforeEvent $event - */ - public function onBefore(BeforeEvent $event) - { - // Requests using "auth"="google_auth" will be authorized. - $request = $event->getRequest(); - if ($request->getConfig()['auth'] != 'google_auth') { - return; - } - - // Fetch the auth token. - $auth_tokens = $this->fetcher->fetchAuthToken($this->httpHandler); - if (array_key_exists('access_token', $auth_tokens)) { - $request->setHeader('authorization', 'Bearer ' . $auth_tokens['access_token']); - - // notify the callback if applicable - if ($this->tokenCallback) { - call_user_func($this->tokenCallback, $this->fetcher->getCacheKey(), $auth_tokens['access_token']); - } - } - - if ($quotaProject = $this->getQuotaProject()) { - $request->setHeader( - GetQuotaProjectInterface::X_GOOG_USER_PROJECT_HEADER, - $quotaProject - ); - } - } - - private function getQuotaProject() - { - if ($this->fetcher instanceof GetQuotaProjectInterface) { - return $this->fetcher->getQuotaProject(); - } - } -} diff --git a/src/Subscriber/ScopedAccessTokenSubscriber.php b/src/Subscriber/ScopedAccessTokenSubscriber.php deleted file mode 100644 index a52dccefd..000000000 --- a/src/Subscriber/ScopedAccessTokenSubscriber.php +++ /dev/null @@ -1,180 +0,0 @@ -' - */ -class ScopedAccessTokenSubscriber implements SubscriberInterface -{ - use CacheTrait; - - const DEFAULT_CACHE_LIFETIME = 1500; - - /** - * @var CacheItemPoolInterface - */ - private $cache; - - /** - * @var callable The access token generator function - */ - private $tokenFunc; - - /** - * @var array|string The scopes used to generate the token - */ - private $scopes; - - /** - * @var array - */ - private $cacheConfig; - - /** - * Creates a new ScopedAccessTokenSubscriber. - * - * @param callable $tokenFunc a token generator function - * @param array|string $scopes the token authentication scopes - * @param array $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); - } - } - - /** - * @return array - */ - public function getEvents() - { - return ['before' => ['onBefore', RequestEvents::SIGN_REQUEST]]; - } - - /** - * Updates the request with an Authorization header when auth is 'scoped'. - * - * E.g this could be used to authenticate using the AppEngine AppIdentityService. - * - * Example: - * ``` - * use google\appengine\api\app_identity\AppIdentityService; - * use Google\Auth\Subscriber\ScopedAccessTokenSubscriber; - * use GuzzleHttp\Client; - * - * $scope = 'https://www.googleapis.com/auth/taskqueue' - * $subscriber = new ScopedAccessToken( - * 'AppIdentityService::getAccessToken', - * $scope, - * ['prefix' => 'Google\Auth\ScopedAccessToken::'], - * $cache = new Memcache() - * ); - * - * $client = new Client([ - * 'base_url' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/', - * 'defaults' => ['auth' => 'scoped'] - * ]); - * $client->getEmitter()->attach($subscriber); - * - * $res = $client->get('myproject/taskqueues/myqueue'); - * ``` - * - * @param BeforeEvent $event - */ - public function onBefore(BeforeEvent $event) - { - // Requests using "auth"="scoped" will be authorized. - $request = $event->getRequest(); - if ($request->getConfig()['auth'] != 'scoped') { - return; - } - $auth_header = 'Bearer ' . $this->fetchToken(); - $request->setHeader('authorization', $auth_header); - } - - /** - * @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; - } -} diff --git a/src/Subscriber/SimpleSubscriber.php b/src/Subscriber/SimpleSubscriber.php deleted file mode 100644 index a881eb19d..000000000 --- a/src/Subscriber/SimpleSubscriber.php +++ /dev/null @@ -1,93 +0,0 @@ -config = array_merge([], $config); - } - - /** - * @return array - */ - public function getEvents() - { - return ['before' => ['onBefore', RequestEvents::SIGN_REQUEST]]; - } - - /** - * Updates the request query with the developer key if auth is set to simple. - * - * Example: - * ``` - * use Google\Auth\Subscriber\SimpleSubscriber; - * use GuzzleHttp\Client; - * - * $my_key = 'is not the same as yours'; - * $subscriber = new SimpleSubscriber(['key' => $my_key]); - * - * $client = new Client([ - * 'base_url' => 'https://www.googleapis.com/discovery/v1/', - * 'defaults' => ['auth' => 'simple'] - * ]); - * $client->getEmitter()->attach($subscriber); - * - * $res = $client->get('drive/v2/rest'); - * ``` - * - * @param BeforeEvent $event - */ - public function onBefore(BeforeEvent $event) - { - // Requests using "auth"="simple" with the developer key. - $request = $event->getRequest(); - if ($request->getConfig()['auth'] != 'simple') { - return; - } - $request->getQuery()->overwriteWith($this->config); - } -} diff --git a/src/UpdateMetadataInterface.php b/src/UpdateMetadataInterface.php deleted file mode 100644 index d28b75c5f..000000000 --- a/src/UpdateMetadataInterface.php +++ /dev/null @@ -1,41 +0,0 @@ -cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - $this->jwt = $this->prophesize('Firebase\JWT\JWT'); - $this->token = 'foobar'; - $this->publicKey = 'barfoo'; - - $this->payload = [ - 'iat' => time(), - 'exp' => time() + 30, - 'name' => 'foo', - 'iss' => AccessToken::OAUTH2_ISSUER_HTTPS - ]; - } - - /** - * @dataProvider verifyCalls - */ - public function testVerify( - $payload, - $expected, - $audience = null, - $exception = null, - $certsLocation = null, - $issuer = null - ) { - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); - $item->get()->willReturn([ - 'keys' => [ - [ - 'kid' => 'ddddffdfd', - 'e' => 'AQAB', - 'kty' => 'RSA', - 'alg' => $certsLocation ? 'ES256' : 'RS256', - 'n' => $this->publicKey, - 'use' => 'sig' - ] - ] - ]); - - $cacheKey = 'google_auth_certs_cache|' . - ($certsLocation ? sha1($certsLocation) : 'federated_signon_certs_v3'); - $this->cache->getItem($cacheKey) - ->shouldBeCalledTimes(1) - ->willReturn($item->reveal()); - - $token = new AccessTokenStub( - null, - $this->cache->reveal() - ); - - $token->mocks['decode'] = function ($token, $publicKey, $allowedAlgs) use ($payload, $exception) { - $this->assertEquals($this->token, $token); - - if ($exception) { - throw $exception; - } - - return (object) $payload; - }; - - $e = null; - $res = false; - try { - $res = $token->verify($this->token, [ - 'audience' => $audience, - 'issuer' => $issuer, - 'certsLocation' => $certsLocation, - 'throwException' => (bool) $exception, - ]); - } catch (\Exception $e) { - } - - $this->assertEquals($expected, $res); - $this->assertEquals($exception, $e); - } - - public function verifyCalls() - { - $this->setUp(); - - if (class_exists('Firebase\JWT\JWT')) { - $expiredException = 'Firebase\JWT\ExpiredException'; - $sigInvalidException = 'Firebase\JWT\SignatureInvalidException'; - } else { - $expiredException = 'ExpiredException'; - $sigInvalidException = 'SignatureInvalidException'; - } - - return [ - [ - $this->payload, - $this->payload, - ], [ - $this->payload + [ - 'aud' => 'foo' - ], - $this->payload + [ - 'aud' => 'foo' - ], - 'foo' - ], [ - $this->payload + [ - 'aud' => 'foo' - ], - false, - 'bar' - ], [ - [ - 'iss' => 'invalid' - ] + $this->payload, - false - ], [ - [ - 'iss' => 'baz' - ] + $this->payload, - [ - 'iss' => 'baz' - ] + $this->payload, - null, - null, - null, - 'baz' - ], [ - $this->payload, - false, - null, - new $expiredException('expired!') - ], [ - $this->payload, - false, - null, - new $sigInvalidException('invalid!') - ], [ - $this->payload, - false, - null, - new \DomainException('expired!') - ], [ - [ - 'iss' => AccessToken::IAP_ISSUER - ] + $this->payload, [ - 'iss' => AccessToken::IAP_ISSUER - ] + $this->payload, - null, - null, - AccessToken::IAP_CERT_URL - ], [ - [ - 'iss' => 'invalid', - ] + $this->payload, - false, - null, - null, - AccessToken::IAP_CERT_URL - ], [ - [ - 'iss' => AccessToken::IAP_ISSUER, - ] + $this->payload + [ - 'aud' => 'foo' - ], - false, - 'bar', - null, - AccessToken::IAP_CERT_URL - ], [ - [ - 'iss' => 'baz' - ] + $this->payload, - false, - null, - null, - AccessToken::IAP_CERT_URL - ], [ - [ - 'iss' => 'baz' - ] + $this->payload, [ - 'iss' => 'baz' - ] + $this->payload, - null, - null, - AccessToken::IAP_CERT_URL, - 'baz' - ] - ]; - } - - public function testEsVerifyEndToEnd() - { - if (!$jwt = getenv('IAP_IDENTITY_TOKEN')) { - $this->markTestSkipped('Set the IAP_IDENTITY_TOKEN env var'); - } - - $token = new AccessTokenStub(); - $token->mocks['decode'] = function ($token, $publicKey, $allowedAlgs) { - // Skip expired validation - $jwt = SimpleJWT::decode( - $token, - $publicKey, - $allowedAlgs, - null, - ['exp'] - ); - return $jwt->getClaims(); - }; - - // Use Iap Cert URL - $payload = $token->verify($jwt, [ - 'certsLocation' => AccessToken::IAP_CERT_URL, - 'throwException' => true, - 'issuer' => 'https://cloud.google.com/iap', - ]); - - $this->assertNotFalse($payload); - $this->assertArrayHasKey('iss', $payload); - $this->assertEquals('https://cloud.google.com/iap', $payload['iss']); - } - - public function testGetCertsForIap() - { - $token = new AccessToken(); - $reflector = new \ReflectionObject($token); - $cacheKeyMethod = $reflector->getMethod('getCacheKeyFromCertLocation'); - $cacheKeyMethod->setAccessible(true); - $getCertsMethod = $reflector->getMethod('getCerts'); - $getCertsMethod->setAccessible(true); - $cacheKey = $cacheKeyMethod->invoke($token, AccessToken::IAP_CERT_URL); - $certs = $getCertsMethod->invoke( - $token, - AccessToken::IAP_CERT_URL, - $cacheKey - ); - $this->assertTrue(is_array($certs)); - $this->assertEquals(5, count($certs)); - } - - public function testRetrieveCertsFromLocationLocalFile() - { - $certsLocation = __DIR__ . '/fixtures/federated-certs.json'; - $certsData = json_decode(file_get_contents($certsLocation), true); - - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); - $item->get() - ->shouldBeCalledTimes(1) - ->willReturn(null); - $item->set($certsData) - ->shouldBeCalledTimes(1); - $item->expiresAt(Argument::type('\DateTime')) - ->shouldBeCalledTimes(1); - - $this->cache->getItem('google_auth_certs_cache|' . sha1($certsLocation)) - ->shouldBeCalledTimes(1) - ->willReturn($item->reveal()); - - $this->cache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->shouldBeCalledTimes(1); - - $token = new AccessTokenStub( - null, - $this->cache->reveal() - ); - - $token->mocks['decode'] = function ($token, $publicKey, $allowedAlgs) { - $this->assertEquals($this->token, $token); - $this->assertEquals(['RS256'], $allowedAlgs); - - return (object) $this->payload; - }; - - $token->verify($this->token, [ - 'certsLocation' => $certsLocation - ]); - } - - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Failed to retrieve verification certificates from path - */ - public function testRetrieveCertsFromLocationLocalFileInvalidFilePath() - { - $certsLocation = __DIR__ . '/fixtures/federated-certs-does-not-exist.json'; - - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); - $item->get() - ->shouldBeCalledTimes(1) - ->willReturn(null); - - $this->cache->getItem('google_auth_certs_cache|' . sha1($certsLocation)) - ->shouldBeCalledTimes(1) - ->willReturn($item->reveal()); - - $token = new AccessTokenStub( - null, - $this->cache->reveal() - ); - - $token->verify($this->token, [ - 'certsLocation' => $certsLocation - ]); - } - - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage federated sign-on certs expects "keys" to be set - */ - public function testRetrieveCertsInvalidData() - { - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); - $item->get() - ->shouldBeCalledTimes(1) - ->willReturn('{}'); - - $this->cache->getItem('google_auth_certs_cache|federated_signon_certs_v3') - ->shouldBeCalledTimes(1) - ->willReturn($item->reveal()); - - $token = new AccessTokenStub( - null, - $this->cache->reveal() - ); - - $token->verify($this->token); - } - - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage federated sign-on certs expects "keys" to be set - */ - public function testRetrieveCertsFromLocationLocalFileInvalidFileData() - { - $temp = tmpfile(); - fwrite($temp, '{}'); - $certsLocation = stream_get_meta_data($temp)['uri']; - - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); - $item->get() - ->shouldBeCalledTimes(1) - ->willReturn(null); - - $this->cache->getItem('google_auth_certs_cache|' . sha1($certsLocation)) - ->shouldBeCalledTimes(1) - ->willReturn($item->reveal()); - - $token = new AccessTokenStub( - null, - $this->cache->reveal() - ); - - $token->verify($this->token, [ - 'certsLocation' => $certsLocation - ]); - } - - public function testRetrieveCertsFromLocationRemote() - { - $certsLocation = __DIR__ . '/fixtures/federated-certs.json'; - $certsJson = file_get_contents($certsLocation); - $certsData = json_decode($certsJson, true); - - $httpHandler = function (RequestInterface $request) use ($certsJson) { - $this->assertEquals(AccessToken::FEDERATED_SIGNON_CERT_URL, (string) $request->getUri()); - $this->assertEquals('GET', $request->getMethod()); - - return new Response(200, [], $certsJson); - }; - - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); - $item->get() - ->shouldBeCalledTimes(1) - ->willReturn(null); - $item->set($certsData) - ->shouldBeCalledTimes(1); - $item->expiresAt(Argument::type('\DateTime')) - ->shouldBeCalledTimes(1); - - $this->cache->getItem('google_auth_certs_cache|federated_signon_certs_v3') - ->shouldBeCalledTimes(1) - ->willReturn($item->reveal()); - - $this->cache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->shouldBeCalledTimes(1); - - $token = new AccessTokenStub( - $httpHandler, - $this->cache->reveal() - ); - - $token->mocks['decode'] = function ($token, $publicKey, $allowedAlgs) { - $this->assertEquals($this->token, $token); - $this->assertEquals(['RS256'], $allowedAlgs); - - return (object) $this->payload; - }; - - $token->verify($this->token); - } - - /** - * @expectedException RuntimeException - * @expectedExceptionMessage bad news guys - */ - public function testRetrieveCertsFromLocationRemoteBadRequest() - { - $badBody = 'bad news guys'; - - $httpHandler = function (RequestInterface $request) use ($badBody) { - return new Response(500, [], $badBody); - }; - - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); - $item->get() - ->shouldBeCalledTimes(1) - ->willReturn(null); - - $this->cache->getItem('google_auth_certs_cache|federated_signon_certs_v3') - ->shouldBeCalledTimes(1) - ->willReturn($item->reveal()); - - $token = new AccessTokenStub( - $httpHandler, - $this->cache->reveal() - ); - - $token->verify($this->token); - } - - /** - * @dataProvider revokeTokens - */ - public function testRevoke($input, $expected) - { - $httpHandler = function (RequestInterface $request) use ($expected) { - $this->assertEquals('no-store', $request->getHeaderLine('Cache-Control')); - $this->assertEquals('application/x-www-form-urlencoded', $request->getHeaderLine('Content-Type')); - $this->assertEquals('POST', $request->getMethod()); - $this->assertEquals(AccessToken::OAUTH2_REVOKE_URI, (string) $request->getUri()); - $this->assertEquals('token=' . $expected, (string) $request->getBody()); - - return new Response(200); - }; - - $token = new AccessToken($httpHandler); - - $this->assertTrue($token->revoke($input)); - } - - public function revokeTokens() - { - $this->setUp(); - - return [ - [ - $this->token, - $this->token - ], [ - ['refresh_token' => $this->token, 'access_token' => 'other thing'], - $this->token - ], [ - ['access_token' => $this->token], - $this->token - ] - ]; - } - - public function testRevokeFails() - { - $httpHandler = function (RequestInterface $request) { - return new Response(500); - }; - - $token = new AccessToken($httpHandler); - - $this->assertFalse($token->revoke($this->token)); - } -} - -//@codingStandardsIgnoreStart -class AccessTokenStub extends AccessToken -{ - public $mocks = []; - - protected function callJwtStatic($method, array $args = []) - { - return isset($this->mocks[$method]) - ? call_user_func_array($this->mocks[$method], $args) - : parent::callJwtStatic($method, $args); - } - - protected function callSimpleJwtDecode(array $args = []) - { - if (isset($this->mocks['decode'])) { - $claims = call_user_func_array($this->mocks['decode'], $args); - return new SimpleJWT(null, (array) $claims); - } - - return parent::callSimpleJwtDecode($args); - } -} -//@codingStandardsIgnoreEnd diff --git a/tests/ApplicationDefaultCredentialsTest.php b/tests/ApplicationDefaultCredentialsTest.php deleted file mode 100644 index 5f0329118..000000000 --- a/tests/ApplicationDefaultCredentialsTest.php +++ /dev/null @@ -1,858 +0,0 @@ -originalHome = getenv('HOME'); - } - - protected function tearDown() - { - if ($this->originalHome != getenv('HOME')) { - putenv('HOME=' . $this->originalHome); - } - putenv(ServiceAccountCredentials::ENV_VAR); // removes it from - } - - /** - * @expectedException DomainException - */ - public function testIsFailsEnvSpecifiesNonExistentFile() - { - $keyFile = __DIR__ . '/fixtures' . '/does-not-exist-private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - ApplicationDefaultCredentials::getCredentials('a scope'); - } - - public function testLoadsOKIfEnvSpecifiedIsValid() - { - $keyFile = __DIR__ . '/fixtures' . '/private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - $this->assertNotNull( - ApplicationDefaultCredentials::getCredentials('a scope') - ); - } - - public function testLoadsDefaultFileIfPresentAndEnvVarIsNotSet() - { - putenv('HOME=' . __DIR__ . '/fixtures'); - $this->assertNotNull( - ApplicationDefaultCredentials::getCredentials('a scope') - ); - } - - /** - * @expectedException DomainException - */ - public function testFailsIfNotOnGceAndNoDefaultFileFound() - { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); - // simulate not being GCE and retry attempts by returning multiple 500s - $httpHandler = getHandler([ - buildResponse(500), - buildResponse(500), - buildResponse(500) - ]); - - ApplicationDefaultCredentials::getCredentials('a scope', $httpHandler); - } - - public function testSuccedsIfNoDefaultFilesButIsOnGCE() - { - putenv('HOME'); - - $wantedTokens = [ - 'access_token' => '1/abdef1234567890', - 'expires_in' => '57', - 'token_type' => 'Bearer', - ]; - $jsonTokens = json_encode($wantedTokens); - - // simulate the response from GCE. - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for($jsonTokens)), - ]); - - $this->assertInstanceOf( - 'Google\Auth\Credentials\GCECredentials', - ApplicationDefaultCredentials::getCredentials('a scope', $httpHandler) - ); - } -} - -class ADCDefaultScopeTest extends TestCase -{ - /** @runInSeparateProcess */ - public function testGceCredentials() - { - putenv('HOME'); - - $jsonTokens = json_encode(['access_token' => 'abc']); - - $creds = ApplicationDefaultCredentials::getCredentials( - null, // $scope - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for($jsonTokens)), - ]), // $httpHandler - null, // $cacheConfig - null, // $cache - null, // $quotaProject - 'a+default+scope' // $defaultScope - ); - - $this->assertInstanceOf( - 'Google\Auth\Credentials\GCECredentials', - $creds - ); - - $uriProperty = (new ReflectionClass($creds))->getProperty('tokenUri'); - $uriProperty->setAccessible(true); - - // used default scope - $tokenUri = $uriProperty->getValue($creds); - $this->assertContains('a+default+scope', $tokenUri); - - $creds = ApplicationDefaultCredentials::getCredentials( - 'a+user+scope', // $scope - getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for($jsonTokens)), - ]), // $httpHandler - null, // $cacheConfig - null, // $cache - null, // $quotaProject - 'a+default+scope' // $defaultScope - ); - - // did not use default scope - $tokenUri = $uriProperty->getValue($creds); - $this->assertContains('a+user+scope', $tokenUri); - } - - /** @runInSeparateProcess */ - public function testUserRefreshCredentials() - { - putenv('HOME=' . __DIR__ . '/fixtures2'); - - $creds = ApplicationDefaultCredentials::getCredentials( - null, // $scope - null, // $httpHandler - null, // $cacheConfig - null, // $cache - null, // $quotaProject - 'a default scope' // $defaultScope - ); - - $this->assertInstanceOf( - 'Google\Auth\Credentials\UserRefreshCredentials', - $creds - ); - - $authProperty = (new ReflectionClass($creds))->getProperty('auth'); - $authProperty->setAccessible(true); - - // used default scope - $auth = $authProperty->getValue($creds); - $this->assertEquals('a default scope', $auth->getScope()); - - $creds = ApplicationDefaultCredentials::getCredentials( - 'a user scope', // $scope - null, // $httpHandler - null, // $cacheConfig - null, // $cache - null, // $quotaProject - 'a default scope' // $defaultScope - ); - - // did not use default scope - $auth = $authProperty->getValue($creds); - $this->assertEquals('a user scope', $auth->getScope()); - } - - /** @runInSeparateProcess */ - public function testServiceAccountCredentials() - { - putenv('HOME=' . __DIR__ . '/fixtures'); - - $creds = ApplicationDefaultCredentials::getCredentials( - null, // $scope - null, // $httpHandler - null, // $cacheConfig - null, // $cache - null, // $quotaProject - 'a default scope' // $defaultScope - ); - - $this->assertInstanceOf( - 'Google\Auth\Credentials\ServiceAccountCredentials', - $creds - ); - - $authProperty = (new ReflectionClass($creds))->getProperty('auth'); - $authProperty->setAccessible(true); - - // did not use default scope - $auth = $authProperty->getValue($creds); - $this->assertEquals('', $auth->getScope()); - - $creds = ApplicationDefaultCredentials::getCredentials( - 'a user scope', // $scope - null, // $httpHandler - null, // $cacheConfig - null, // $cache - null, // $quotaProject - 'a default scope' // $defaultScope - ); - - // used user scope - $auth = $authProperty->getValue($creds); - $this->assertEquals('a user scope', $auth->getScope()); - } - - /** @runInSeparateProcess */ - public function testDefaultScopeArray() - { - putenv('HOME=' . __DIR__ . '/fixtures2'); - - $creds = ApplicationDefaultCredentials::getCredentials( - null, // $scope - null, // $httpHandler - null, // $cacheConfig - null, // $cache - null, // $quotaProject - ['onescope', 'twoscope'] // $defaultScope - ); - - $authProperty = (new ReflectionClass($creds))->getProperty('auth'); - $authProperty->setAccessible(true); - - // used default scope - $auth = $authProperty->getValue($creds); - $this->assertEquals('onescope twoscope', $auth->getScope()); - } -} - -class ADCGetMiddlewareTest extends TestCase -{ - private $originalHome; - - protected function setUp() - { - $this->originalHome = getenv('HOME'); - } - - protected function tearDown() - { - if ($this->originalHome != getenv('HOME')) { - putenv('HOME=' . $this->originalHome); - } - putenv(ServiceAccountCredentials::ENV_VAR); // removes it if assigned - } - - /** - * @expectedException DomainException - */ - public function testIsFailsEnvSpecifiesNonExistentFile() - { - $keyFile = __DIR__ . '/fixtures' . '/does-not-exist-private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - ApplicationDefaultCredentials::getMiddleware('a scope'); - } - - public function testLoadsOKIfEnvSpecifiedIsValid() - { - $keyFile = __DIR__ . '/fixtures' . '/private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - $this->assertNotNull(ApplicationDefaultCredentials::getMiddleware('a scope')); - } - - public function testLoadsDefaultFileIfPresentAndEnvVarIsNotSet() - { - putenv('HOME=' . __DIR__ . '/fixtures'); - $this->assertNotNull(ApplicationDefaultCredentials::getMiddleware('a scope')); - } - - /** - * @expectedException DomainException - */ - public function testFailsIfNotOnGceAndNoDefaultFileFound() - { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); - - // simulate not being GCE and retry attempts by returning multiple 500s - $httpHandler = getHandler([ - buildResponse(500), - buildResponse(500), - buildResponse(500) - ]); - - ApplicationDefaultCredentials::getMiddleware('a scope', $httpHandler); - } - - public function testWithCacheOptions() - { - $keyFile = __DIR__ . '/fixtures' . '/private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - - $httpHandler = getHandler([ - buildResponse(200), - ]); - - $cacheOptions = []; - $cachePool = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - - $middleware = ApplicationDefaultCredentials::getMiddleware( - 'a scope', - $httpHandler, - $cacheOptions, - $cachePool->reveal() - ); - } - - public function testSuccedsIfNoDefaultFilesButIsOnGCE() - { - $wantedTokens = [ - 'access_token' => '1/abdef1234567890', - 'expires_in' => '57', - 'token_type' => 'Bearer', - ]; - $jsonTokens = json_encode($wantedTokens); - - // simulate the response from GCE. - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for($jsonTokens)), - ]); - - $this->assertNotNull(ApplicationDefaultCredentials::getMiddleware('a scope', $httpHandler)); - } - - /** - * @expectedException DomainException - */ - public function testOnGceCacheWithHit() - { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); - - $mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $mockCacheItem->isHit() - ->willReturn(true); - $mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn(false); - - $mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - $mockCache->getItem(GCECache::GCE_CACHE_KEY) - ->shouldBeCalledTimes(1) - ->willReturn($mockCacheItem->reveal()); - - ApplicationDefaultCredentials::getMiddleware( - 'a scope', - null, - null, - $mockCache->reveal() - ); - } - - public function testOnGceCacheWithoutHit() - { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); - - $gceIsCalled = false; - $dummyHandler = function ($request) use (&$gceIsCalled) { - $gceIsCalled = true; - return new Psr7\Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']); - }; - $mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $mockCacheItem->isHit() - ->willReturn(false); - $mockCacheItem->set(true) - ->shouldBeCalledTimes(1); - $mockCacheItem->expiresAfter(1500) - ->shouldBeCalledTimes(1); - - $mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - $mockCache->getItem(GCECache::GCE_CACHE_KEY) - ->shouldBeCalledTimes(2) - ->willReturn($mockCacheItem->reveal()); - $mockCache->save($mockCacheItem->reveal()) - ->shouldBeCalled(); - - $creds = ApplicationDefaultCredentials::getMiddleware( - 'a scope', - $dummyHandler, - null, - $mockCache->reveal() - ); - - $this->assertTrue($gceIsCalled); - } - - public function testOnGceCacheWithOptions() - { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); - - $prefix = 'test_prefix_'; - $lifetime = '70707'; - - $gceIsCalled = false; - $dummyHandler = function ($request) use (&$gceIsCalled) { - $gceIsCalled = true; - return new Psr7\Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']); - }; - $mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $mockCacheItem->isHit() - ->willReturn(false); - $mockCacheItem->set(true) - ->shouldBeCalledTimes(1); - $mockCacheItem->expiresAfter($lifetime) - ->shouldBeCalledTimes(1); - - $mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - $mockCache->getItem($prefix . GCECache::GCE_CACHE_KEY) - ->shouldBeCalledTimes(2) - ->willReturn($mockCacheItem->reveal()); - $mockCache->save($mockCacheItem->reveal()) - ->shouldBeCalled(); - - $creds = ApplicationDefaultCredentials::getMiddleware( - 'a scope', - $dummyHandler, - ['gce_prefix' => $prefix, 'gce_lifetime' => $lifetime], - $mockCache->reveal() - ); - - $this->assertTrue($gceIsCalled); - } -} - -class ADCGetCredentialsWithTargetAudienceTest extends TestCase -{ - private $originalHome; - private $targetAudience = 'a target audience'; - - protected function setUp() - { - $this->originalHome = getenv('HOME'); - } - - protected function tearDown() - { - if ($this->originalHome != getenv('HOME')) { - putenv('HOME=' . $this->originalHome); - } - putenv(ServiceAccountCredentials::ENV_VAR); // removes environment variable - } - - /** - * @expectedException DomainException - */ - public function testIsFailsEnvSpecifiesNonExistentFile() - { - $keyFile = __DIR__ . '/fixtures' . '/does-not-exist-private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - ApplicationDefaultCredentials::getIdTokenCredentials($this->targetAudience); - } - - public function testLoadsOKIfEnvSpecifiedIsValid() - { - $keyFile = __DIR__ . '/fixtures' . '/private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - ApplicationDefaultCredentials::getIdTokenCredentials($this->targetAudience); - } - - public function testLoadsDefaultFileIfPresentAndEnvVarIsNotSet() - { - putenv('HOME=' . __DIR__ . '/fixtures'); - ApplicationDefaultCredentials::getIdTokenCredentials($this->targetAudience); - } - - /** - * @expectedException DomainException - */ - public function testFailsIfNotOnGceAndNoDefaultFileFound() - { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); - - // simulate not being GCE and retry attempts by returning multiple 500s - $httpHandler = getHandler([ - buildResponse(500), - buildResponse(500), - buildResponse(500) - ]); - - ApplicationDefaultCredentials::getIdTokenCredentials( - $this->targetAudience, - $httpHandler - ); - } - - public function testWithCacheOptions() - { - $keyFile = __DIR__ . '/fixtures' . '/private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - - $httpHandler = getHandler([ - buildResponse(200), - ]); - - $cacheOptions = []; - $cachePool = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - - $credentials = ApplicationDefaultCredentials::getIdTokenCredentials( - $this->targetAudience, - $httpHandler, - $cacheOptions, - $cachePool->reveal() - ); - - $this->assertInstanceOf('Google\Auth\FetchAuthTokenCache', $credentials); - } - - public function testSuccedsIfNoDefaultFilesButIsOnGCE() - { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); - $wantedTokens = [ - 'access_token' => '1/abdef1234567890', - 'expires_in' => '57', - 'token_type' => 'Bearer', - ]; - $jsonTokens = json_encode($wantedTokens); - - // simulate the response from GCE. - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for($jsonTokens)), - ]); - - $credentials = ApplicationDefaultCredentials::getIdTokenCredentials( - $this->targetAudience, - $httpHandler - ); - - $this->assertInstanceOf( - 'Google\Auth\Credentials\GCECredentials', - $credentials - ); - } -} - -class ADCGetCredentialsWithQuotaProjectTest extends TestCase -{ - private $originalHome; - private $quotaProject = 'a-quota-project'; - - protected function setUp() - { - $this->originalHome = getenv('HOME'); - } - - protected function tearDown() - { - if ($this->originalHome != getenv('HOME')) { - putenv('HOME=' . $this->originalHome); - } - putenv(ServiceAccountCredentials::ENV_VAR); // removes environment variable - } - - public function testWithServiceAccountCredentialsAndExplicitQuotaProject() - { - $keyFile = __DIR__ . '/fixtures' . '/private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - - $credentials = ApplicationDefaultCredentials::getCredentials( - null, - null, - null, - null, - $this->quotaProject - ); - - $this->assertInstanceOf( - 'Google\Auth\Credentials\ServiceAccountCredentials', - $credentials - ); - - $this->assertEquals( - $this->quotaProject, - $credentials->getQuotaProject() - ); - } - - public function testGetCredentialsUtilizesQuotaProjectInKeyFile() - { - $keyFile = __DIR__ . '/fixtures' . '/private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - - $credentials = ApplicationDefaultCredentials::getCredentials(); - - $this->assertEquals( - 'test_quota_project', - $credentials->getQuotaProject() - ); - } - - public function testWithFetchAuthTokenCacheAndExplicitQuotaProject() - { - $keyFile = __DIR__ . '/fixtures' . '/private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - - $httpHandler = getHandler([ - buildResponse(200), - ]); - - $cacheOptions = []; - $cachePool = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - - $credentials = ApplicationDefaultCredentials::getCredentials( - null, - $httpHandler, - $cacheOptions, - $cachePool->reveal(), - $this->quotaProject - ); - - $this->assertInstanceOf('Google\Auth\FetchAuthTokenCache', $credentials); - - $this->assertEquals( - $this->quotaProject, - $credentials->getQuotaProject() - ); - } - - public function testWithGCECredentials() - { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); - $wantedTokens = [ - 'access_token' => '1/abdef1234567890', - 'expires_in' => '57', - 'token_type' => 'Bearer', - ]; - $jsonTokens = json_encode($wantedTokens); - - // simulate the response from GCE. - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for($jsonTokens)), - ]); - - $credentials = ApplicationDefaultCredentials::getCredentials( - null, - $httpHandler, - null, - null, - $this->quotaProject - ); - - $this->assertInstanceOf( - 'Google\Auth\Credentials\GCECredentials', - $credentials - ); - - $this->assertEquals( - $this->quotaProject, - $credentials->getQuotaProject() - ); - } -} - -class ADCGetCredentialsAppEngineTest extends BaseTest -{ - private $originalHome; - private $originalServiceAccount; - private $targetAudience = 'a target audience'; - - protected function setUp() - { - // set home to be somewhere else - $this->originalHome = getenv('HOME'); - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); - - // remove service account path - $this->originalServiceAccount = getenv(ServiceAccountCredentials::ENV_VAR); - putenv(ServiceAccountCredentials::ENV_VAR); - } - - protected function tearDown() - { - // removes it if assigned - putenv('HOME=' . $this->originalHome); - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $this->originalServiceAccount); - putenv('GAE_INSTANCE'); - } - - /** - * @runInSeparateProcess - */ - public function testAppEngineStandard() - { - $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; - $this->assertInstanceOf( - 'Google\Auth\Credentials\AppIdentityCredentials', - ApplicationDefaultCredentials::getCredentials() - ); - } - - /** - * @runInSeparateProcess - */ - public function testAppEngineFlexible() - { - $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; - putenv('GAE_INSTANCE=aef-default-20180313t154438'); - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - ]); - $this->assertInstanceOf( - 'Google\Auth\Credentials\GCECredentials', - ApplicationDefaultCredentials::getCredentials(null, $httpHandler) - ); - } - - /** - * @runInSeparateProcess - */ - public function testAppEngineFlexibleIdToken() - { - $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; - putenv('GAE_INSTANCE=aef-default-20180313t154438'); - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - ]); - $creds = ApplicationDefaultCredentials::getIdTokenCredentials( - $this->targetAudience, - $httpHandler - ); - $this->assertInstanceOf( - 'Google\Auth\Credentials\GCECredentials', - $creds - ); - } -} - -// @todo consider a way to DRY this and above class up -class ADCGetSubscriberTest extends BaseTest -{ - private $originalHome; - - protected function setUp() - { - $this->onlyGuzzle5(); - - $this->originalHome = getenv('HOME'); - } - - protected function tearDown() - { - if ($this->originalHome != getenv('HOME')) { - putenv('HOME=' . $this->originalHome); - } - putenv(ServiceAccountCredentials::ENV_VAR); // removes it if assigned - } - - /** - * @expectedException DomainException - */ - public function testIsFailsEnvSpecifiesNonExistentFile() - { - $keyFile = __DIR__ . '/fixtures' . '/does-not-exist-private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - ApplicationDefaultCredentials::getSubscriber('a scope'); - } - - public function testLoadsOKIfEnvSpecifiedIsValid() - { - $keyFile = __DIR__ . '/fixtures' . '/private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - $this->assertNotNull(ApplicationDefaultCredentials::getSubscriber('a scope')); - } - - public function testLoadsDefaultFileIfPresentAndEnvVarIsNotSet() - { - putenv('HOME=' . __DIR__ . '/fixtures'); - $this->assertNotNull(ApplicationDefaultCredentials::getSubscriber('a scope')); - } - - /** - * @expectedException DomainException - */ - public function testFailsIfNotOnGceAndNoDefaultFileFound() - { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); - - // simulate not being GCE by return 500 - $httpHandler = getHandler([ - buildResponse(500), - ]); - - ApplicationDefaultCredentials::getSubscriber('a scope', $httpHandler); - } - - public function testWithCacheOptions() - { - $keyFile = __DIR__ . '/fixtures' . '/private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - - $httpHandler = getHandler([ - buildResponse(200), - ]); - - $cacheOptions = []; - $cachePool = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - - $subscriber = ApplicationDefaultCredentials::getSubscriber( - 'a scope', - $httpHandler, - $cacheOptions, - $cachePool->reveal() - ); - } - - public function testSuccedsIfNoDefaultFilesButIsOnGCE() - { - $wantedTokens = [ - 'access_token' => '1/abdef1234567890', - 'expires_in' => '57', - 'token_type' => 'Bearer', - ]; - $jsonTokens = json_encode($wantedTokens); - - // simulate the response from GCE. - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for($jsonTokens)), - ]); - - $this->assertNotNull(ApplicationDefaultCredentials::getSubscriber('a scope', $httpHandler)); - } -} diff --git a/tests/Auth/Credentials/AnonymousCredentialsTest.php b/tests/Auth/Credentials/AnonymousCredentialsTest.php new file mode 100644 index 000000000..8d15e0f1f --- /dev/null +++ b/tests/Auth/Credentials/AnonymousCredentialsTest.php @@ -0,0 +1,57 @@ +assertEquals( + ['access_token' => ''], + $credentials->fetchAuthToken() + ); + } + + public function testGetRequestMetadata() + { + $credentials = new AnonymousCredentials(); + $this->assertEquals( + ['Authorization' => 'Bearer '], + $credentials->getRequestMetadata() + ); + } + + public function testGetQuotaProject() + { + $credentials = new AnonymousCredentials(); + $this->assertNull($credentials->getQuotaProject()); + } + + public function testGetProjectId() + { + $credentials = new AnonymousCredentials(); + $this->assertNull($credentials->getProjectId()); + } +} diff --git a/tests/Auth/Credentials/ComputeCredentialsTest.php b/tests/Auth/Credentials/ComputeCredentialsTest.php new file mode 100644 index 000000000..af5fcda54 --- /dev/null +++ b/tests/Auth/Credentials/ComputeCredentialsTest.php @@ -0,0 +1,419 @@ +getHeaderLine('Metadata-Flavor') === 'Google'; + + return new Response(200, ['Metadata-Flavor' => 'Google']); + } + ); + + $onCompute = ComputeCredentials::onCompute($httpClient); + $this->assertTrue($hasHeader); + $this->assertTrue($onCompute); + } + + public function testOnComputeIsFalseOnClientErrorStatus() + { + // simulate retry attempts by returning multiple 400s + $httpClient = httpClientWithResponses([ + new Response(400), + new Response(400), + new Response(400) + ]); + $this->assertFalse(ComputeCredentials::onCompute($httpClient)); + } + + public function testOnComputeIsFalseOnServerErrorStatus() + { + // simulate retry attempts by returning multiple 500s + $httpClient = httpClientWithResponses([ + new Response(500), + new Response(500), + new Response(500) + ]); + $this->assertFalse(ComputeCredentials::onCompute($httpClient)); + } + + public function testOnComputeIsFalseOnOkStatusWithoutExpectedHeader() + { + $httpClient = httpClientWithResponses([ + new Response(200), + ]); + $this->assertFalse(ComputeCredentials::onCompute($httpClient)); + } + + public function testOnComputeIsOkIfGoogleIsTheFlavor() + { + $httpClient = httpClientWithResponses([ + new Response(200, ['Metadata-Flavor' => 'Google']), + ]); + $this->assertTrue(ComputeCredentials::onCompute($httpClient)); + } + + /** + * @runInSeparateProcess + */ + public function testOnAppEngineFlexIsFalseWhenGaeInstanceIsEmpty() + { + putenv('GAE_INSTANCE='); + $this->assertFalse(ComputeCredentials::onAppEngineFlexible()); + } + + /** + * @runInSeparateProcess + */ + public function testOnAppEngineFlexIsFalseWhenGaeInstanceIsNotAef() + { + putenv('GAE_INSTANCE=not-aef-20180313t154438'); + $this->assertFalse(ComputeCredentials::onAppEngineFlexible()); + } + + /** + * @runInSeparateProcess + */ + public function testOnAppEngineFlexIsTrueWhenGaeInstanceHasAefPrefix() + { + putenv('GAE_INSTANCE=aef-default-20180313t154438'); + $this->assertTrue(ComputeCredentials::onAppEngineFlexible()); + } + + public function testFetchAuthTokenThrowsExceptionIfNotOnCompute() + { + $this->expectException('GuzzleHttp\Exception\ServerException'); + + // simulate retry attempts by returning multiple 500s + $httpClient = httpClientWithResponses([new Response(500)]); + $compute = new ComputeCredentials(['httpClient' => $httpClient]); + $this->assertEquals(array(), $compute->fetchAuthToken()); + } + + public function testFetchAuthTokenShouldFailIfResponseIsNotJson() + { + $this->expectException('Exception'); + $this->expectExceptionMessage('Invalid JSON response'); + + $notJson = '{"foo": , this is cannot be passed as json" "bar"}'; + $httpClient = httpClientWithResponses([ + new Response(200, [], $notJson), + ]); + $compute = new ComputeCredentials(['httpClient' => $httpClient]); + $compute->fetchAuthToken(); + } + + public function testFetchAuthTokenShouldReturnTokenInfo() + { + $wantedToken = [ + 'access_token' => '1/abdef1234567890', + 'expires_in' => '57', + 'token_type' => 'Bearer', + 'expires_at' => time() + 57, + ]; + $jsonTokens = json_encode($wantedToken); + $httpClient = httpClientWithResponses([ + new Response(200, [], $jsonTokens), + ]); + + $compute = new ComputeCredentials(['httpClient' => $httpClient]); + $this->assertEquals($wantedToken, $compute->fetchAuthToken()); + } + + public function testFetchAuthTokenShouldBeIdTokenWhenTargetAudienceIsSet() + { + $expectedToken = ['id_token' => 'idtoken12345']; + $httpClient = httpClientFromCallable( + function ($request) use ($expectedToken) { + $this->assertEquals( + '/computeMetadata/v1/instance/service-accounts/default/identity', + $request->getUri()->getPath() + ); + $this->assertEquals( + 'audience=a-target-audience', + $request->getUri()->getQuery() + ); + return new Response(200, [], $expectedToken['id_token']); + } + ); + $compute = new ComputeCredentials([ + 'httpClient' => $httpClient, + 'targetAudience' => 'a-target-audience' + ]); + $this->assertEquals($expectedToken, $compute->fetchAuthToken()); + } + + public function testSettingBothScopeAndTargetAudienceThrowsException() + { + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Scope and targetAudience cannot both be supplied'); + $compute = new ComputeCredentials([ + 'scope' => 'a-scope', + 'targetAudience' => 'a-target-audience', + ]); + } + + /** + * @dataProvider scopes + */ + public function testFetchAuthTokenCustomScope($scope, $expected) + { + $uri = null; + $httpClient = httpClientFromCallable(function ($request) use (&$uri) { + $uri = $request->getUri(); + + return new Response(200, [], '{"expires_in": 0}'); + }); + + $compute = new ComputeCredentials([ + 'scope' => $scope, + 'httpClient' => $httpClient, + ]); + + $compute->fetchAuthToken(); + parse_str($uri->getQuery(), $query); + + $this->assertArrayHasKey('scopes', $query); + $this->assertEquals($expected, $query['scopes']); + } + + public function scopes() + { + return [ + ['foobar', 'foobar'], + [['foobar'], 'foobar'], + ['hello world', 'hello,world'], + [['hello', 'world'], 'hello,world'] + ]; + } + + public function testGetClientName() + { + $expected = 'foobar'; + + $httpClient = httpClientWithResponses([ + new Response(200, [], $expected), + new Response(200, [], 'notexpected') + ]); + + $compute = new ComputeCredentials(['httpClient' => $httpClient]); + $this->assertEquals($expected, $compute->getClientEmail($httpClient)); + + // call again to test cached value + $this->assertEquals($expected, $compute->getClientEmail($httpClient)); + } + + public function testGetClientNameShouldThrowExceptionIfNotOnCompute() + { + $this->expectException('GuzzleHttp\Exception\ServerException'); + + // simulate retry attempts by returning multiple 500s + $httpClient = httpClientWithResponses([new Response(500)]); + + $compute = new ComputeCredentials(['httpClient' => $httpClient]); + $this->assertEquals('', $compute->getClientEmail()); + } + + public function testSignBlob() + { + $expectedEmail = 'test@test.com'; + $expectedAccessToken = 'token'; + $expectedSignature = 'foobar'; + $token = [ + 'access_token' => $expectedAccessToken, + 'expires_in' => '57', + 'token_type' => 'Bearer', + ]; + + $httpClient = httpClientWithResponses([ + new Response(200, [], json_encode($token)), + new Response(200, [], $expectedEmail), + new Response(200, [], json_encode(['signedBlob' => $expectedSignature])) + ]); + + $compute = new ComputeCredentials([ + 'httpClient' => $httpClient, + ]); + $signature = $compute->signBlob('string-to-sign'); + $this->assertEquals($expectedSignature, $signature); + } + + public function testSignBlobFromCache() + { + $expectedEmail = 'test@test.com'; + $expectedAccessToken = 'token'; + $notExpectedAccessToken = 'othertoken'; + $expectedSignature = 'foobar'; + $token1 = [ + 'access_token' => $expectedAccessToken, + 'expires_in' => '57', + 'token_type' => 'Bearer', + ]; + + $httpClient = httpClientWithResponses([ + new Response(200, [], json_encode($token1)), + new Response(200, [], $expectedEmail), + new Response(200, [], json_encode(['signedBlob' => $expectedSignature])) + ]); + + $compute = new ComputeCredentials([ + 'httpClient' => $httpClient, + ]); + // cache a token + $compute->fetchAuthToken(); + + $signature = $compute->signBlob('string-to-sign'); + $this->assertEquals($expectedSignature, $signature); + } + + public function testGetProjectId() + { + $expected = 'foobar'; + + $httpClient = httpClientWithResponses([ + new Response(200, [], $expected), + new Response(200, [], 'notexpected') + ]); + + $compute = new ComputeCredentials([ + 'httpClient' => $httpClient, + ]); + $this->assertEquals($expected, $compute->getProjectId()); + + // call again to test cached value + $this->assertEquals($expected, $compute->getProjectId()); + } + + public function testGetProjectIdThrowsExceptionIfNotOnCompute() + { + $this->expectException('GuzzleHttp\Exception\ServerException'); + + // simulate retry attempts by returning multiple 500s + $httpClient = httpClientWithResponses([new Response(500)]); + + $compute = new ComputeCredentials([ + 'httpClient' => $httpClient, + ]); + $this->assertNull($compute->getProjectId()); + } + + public function testGetTokenUriWithServiceAccountIdentity() + { + $expectedToken = [ + 'access_token' => '123', + 'expires_in' => 1000, + ]; + $httpClient = httpClientFromCallable( + function ($request) use ($expectedToken) { + $this->assertEquals( + '/computeMetadata/v1/instance/service-accounts/foo/token', + $request->getUri()->getPath() + ); + return new Response(200, [], json_encode($expectedToken)); + } + ); + $compute = new ComputeCredentials([ + 'serviceAccountIdentity' => 'foo', + 'httpClient' => $httpClient, + ]); + $token = $compute->fetchAuthToken(); + $this->assertEquals($expectedToken['access_token'], $token['access_token']); + } + + public function testGetAccessTokenWithServiceAccountIdentity() + { + $expected = [ + 'access_token' => 'token12345', + 'expires_in' => 123, + ]; + $httpClient = httpClientFromCallable(function ($request) use ($expected) { + $this->assertEquals( + '/computeMetadata/v1/instance/service-accounts/foo/token', + $request->getUri()->getPath() + ); + $this->assertEquals('', $request->getUri()->getQuery()); + return new Response(200, [], json_encode($expected)); + }); + + $compute = new ComputeCredentials([ + 'serviceAccountIdentity' => 'foo', + 'httpClient' => $httpClient, + ]); + $this->assertEquals( + $expected['access_token'], + $compute->fetchAuthToken()['access_token'] + ); + } + + public function testGetIdTokenWithServiceAccountIdentity() + { + $expected = 'idtoken12345'; + $httpClient = httpClientFromCallable(function ($request) use ($expected) { + $this->assertEquals( + '/computeMetadata/v1/instance/service-accounts/foo/identity', + $request->getUri()->getPath() + ); + $this->assertEquals( + 'audience=a-target-audience', + $request->getUri()->getQuery() + ); + return new Response(200, [], $expected); + }); + $compute = new ComputeCredentials([ + 'httpClient' => $httpClient, + 'targetAudience' => 'a-target-audience', + 'serviceAccountIdentity' => 'foo', + ]); + $this->assertEquals( + ['id_token' => $expected], + $compute->fetchAuthToken() + ); + } + + public function testGetClientEmailWithServiceAccountIdentity() + { + $expected = 'clientemail'; + $httpClient = httpClientFromCallable(function ($request) use ($expected) { + $this->assertEquals( + '/computeMetadata/v1/instance/service-accounts/foo/email', + $request->getUri()->getPath() + ); + return new Response(200, [], $expected); + }); + $compute = new ComputeCredentials([ + 'httpClient' => $httpClient, + 'serviceAccountIdentity' => 'foo', + ]); + $this->assertEquals($expected, $compute->getClientEmail()); + } +} diff --git a/tests/Auth/Credentials/CredentialsTraitTest.php b/tests/Auth/Credentials/CredentialsTraitTest.php new file mode 100644 index 000000000..d03eea378 --- /dev/null +++ b/tests/Auth/Credentials/CredentialsTraitTest.php @@ -0,0 +1,315 @@ +mockCacheItem = $this->prophesize(CacheItemInterface::class); + $this->mockCache = $this->prophesize(CacheItemPoolInterface::class); + } + + public function testSuccessfullyPullsFromCache() + { + $expectedValue = ['1234']; + $this->mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $this->mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($expectedValue); + $this->mockCache->getItem(Argument::type('string')) + ->shouldBeCalledTimes(1) + ->willReturn($this->mockCacheItem->reveal()); + + $implementation = new CredentialsTraitImplementation([ + 'cache' => $this->mockCache->reveal(), + ]); + + $cachedValue = $implementation->gCachedToken(); + $this->assertEquals($expectedValue, $cachedValue); + } + + public function testSuccessfullyPullsFromCacheWithInvalidKey() + { + $key = 'this-key-has-@-illegal-characters'; + $expectedKey = 'thiskeyhasillegalcharacters'; + $expectedValue = ['1234']; + $this->mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $this->mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($expectedValue); + $this->mockCache->getItem($expectedKey) + ->shouldBeCalledTimes(1) + ->willReturn($this->mockCacheItem->reveal()); + + $implementation = new CredentialsTraitImplementation([ + 'cache' => $this->mockCache->reveal(), + 'key' => $key, + ]); + + $cachedValue = $implementation->gCachedToken(); + $this->assertEquals($expectedValue, $cachedValue); + } + + public function testSuccessfullyPullsFromCacheWithLongKey() + { + $key = 'this-key-is-over-64-characters-and-it-will-still-work' + . '-but-it-will-be-hashed-and-shortened'; + $expectedKey = str_replace('-', '', $key); + $expectedKey = substr(hash('sha256', $expectedKey), 0, 64); + $expectedValue = ['1234']; + $this->mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $this->mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($expectedValue); + $this->mockCache->getItem($expectedKey) + ->shouldBeCalledTimes(1) + ->willReturn($this->mockCacheItem->reveal()); + + $implementation = new CredentialsTraitImplementation([ + 'cache' => $this->mockCache->reveal(), + 'key' => $key + ]); + + $cachedValue = $implementation->gCachedToken(); + $this->assertEquals($expectedValue, $cachedValue); + } + + public function testFailsPullFromCacheWithNoCache() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cache has not been initialized'); + $implementation = new CredentialsTraitImplementation([ + 'cache' => null, + ]); + $cachedValue = $implementation->gCachedToken(); + } + + public function testSuccessfullySetsToCache() + { + $value = ['1234']; + $this->mockCacheItem->set($value) + ->shouldBeCalled(); + $this->mockCacheItem->expiresAfter(Argument::any()) + ->shouldBeCalled(); + $this->mockCache->getItem('key') + ->willReturn($this->mockCacheItem->reveal()); + $this->mockCache->save(Argument::type(CacheItemInterface::class)) + ->shouldBeCalled() + ->willReturn(true); + + $implementation = new CredentialsTraitImplementation([ + 'cache' => $this->mockCache->reveal(), + ]); + + $implementation->sCachedToken($value); + } + + public function testCacheSetsExpiresAtWhenTokenExpiresAtIsSet() + { + $token = '2/abcdef1234567890'; + $expiresAt = time() + 10; + $nextToken = [ + 'access_token' => $token, + 'expires_at' => $expiresAt, + ]; + + $this->mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(false); + $this->mockCacheItem->set($nextToken) + ->shouldBeCalledTimes(1) + ->willReturn(true); + $this->mockCacheItem->expiresAt( + \DateTime::createFromFormat('U', (string) $expiresAt) + ) + ->shouldBeCalledTimes(1); + $this->mockCache->getItem(Argument::type('string')) + ->shouldBeCalledTimes(2) + ->willReturn($this->mockCacheItem->reveal()); + $this->mockCache->save(Argument::type(CacheItemInterface::class)) + ->shouldBeCalled() + ->willReturn(true); + + $implementation = new CredentialsTraitImplementation([ + 'cache' => $this->mockCache->reveal(), + ]); + $implementation->setNextToken($nextToken); + + // First time, caches a token with bad expiration + $accessToken = $implementation->fetchAuthToken(); + + $this->assertEquals($nextToken, $accessToken); + } + + public function testCacheSetsExpiresAfterWhenTokenExpiresInIsSet() + { + $token = '2/abcdef1234567890'; + $nextToken = [ + 'access_token' => $token, + 'expires_in' => 123, + ]; + + $this->mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(false); + $this->mockCacheItem->set($nextToken) + ->shouldBeCalledTimes(1) + ->willReturn(true); + $this->mockCacheItem->expiresAfter(123) + ->shouldBeCalledTimes(1); + $this->mockCache->getItem(Argument::type('string')) + ->shouldBeCalledTimes(2) + ->willReturn($this->mockCacheItem->reveal()); + $this->mockCache->save(Argument::type(CacheItemInterface::class)) + ->shouldBeCalled() + ->willReturn(true); + + $implementation = new CredentialsTraitImplementation([ + 'cache' => $this->mockCache->reveal(), + ]); + $implementation->setNextToken($nextToken); + + // First time, caches a token with bad expiration + $accessToken = $implementation->fetchAuthToken(); + + $this->assertEquals($nextToken, $accessToken); + } + + public function testFailsSetToCacheWithNoCache() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cache has not been initialized'); + + $implementation = new CredentialsTraitImplementation([ + 'cache' => null, + ]); + $implementation->sCachedToken(['1234']); + } + + public function testFailsSetToCacheWithoutKey() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cache key cannot be empty'); + + $this->mockCache->getItem(Argument::any()) + ->shouldNotBeCalled(); + + $implementation = new CredentialsTraitImplementation([ + 'cache' => $this->mockCache->reveal(), + 'key' => '', + ]); + + $cachedValue = $implementation->sCachedToken(['1234']); + $this->assertNull($cachedValue); + } + + public function testShouldSaveValueInCacheWithCacheLifetime() + { + $token = '2/abcdef1234567890'; + $nextToken = [ + 'access_token' => $token, + // no expires_in or expires_at + ]; + + $this->mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(false); + $this->mockCacheItem->set($nextToken) + ->shouldBeCalledTimes(1) + ->willReturn(true); + $this->mockCacheItem->expiresAfter(123) + ->shouldBeCalledTimes(1); + $this->mockCache->getItem(Argument::type('string')) + ->shouldBeCalledTimes(2) + ->willReturn($this->mockCacheItem->reveal()); + $this->mockCache->save(Argument::type(CacheItemInterface::class)) + ->shouldBeCalled() + ->willReturn(true); + + $implementation = new CredentialsTraitImplementation([ + 'cache' => $this->mockCache->reveal(), + 'cacheLifetime' => 123, + ]); + $implementation->setNextToken($nextToken); + + $accessToken = $implementation->fetchAuthToken(); + $this->assertEquals($nextToken, $accessToken); + } +} + +class CredentialsTraitImplementation +{ + use CredentialsTrait; + + private $key; + private $token; + + public function __construct(array $config = [], $token = null) + { + $this->key = array_key_exists('key', $config) ? $config['key'] : 'key'; + $this->setCacheFromOptions($config); + if (array_key_exists('cache', $config)) { + // allows us to null the cache + $this->cache = $config['cache']; + } + } + + // allows us to keep trait methods private + public function gCachedToken() + { + return $this->getCachedToken($this->key); + } + + public function sCachedToken($v) + { + $this->setCachedToken($this->key, $v); + return true; + } + + public function setNextToken($token) + { + $this->token = $token; + } + + private function fetchAuthTokenNoCache(): array + { + return $this->token; + } + + private function getCacheKey() + { + return $this->key; + } +} diff --git a/tests/Auth/Credentials/OAuth2CredentialsTest.php b/tests/Auth/Credentials/OAuth2CredentialsTest.php new file mode 100644 index 000000000..af93fac39 --- /dev/null +++ b/tests/Auth/Credentials/OAuth2CredentialsTest.php @@ -0,0 +1,87 @@ +prophesize(OAuth2::class); + $oauth2->fetchAuthToken() + ->shouldBeCalledTimes(1) + ->willReturn(['access_token' => '123']); + $oauth2->getCacheKey() + ->shouldBeCalledTimes(1) + ->wilLReturn('abc'); + + $credentials = new OAuth2Credentials($oauth2->reveal()); + $this->assertEquals( + ['access_token' => '123'], + $credentials->fetchAuthToken() + ); + } + + public function testGetRequestMetadata() + { + $oauth2 = $this->prophesize(OAuth2::class); + $oauth2->fetchAuthToken() + ->shouldBeCalledTimes(1) + ->willReturn(['access_token' => '123']); + $oauth2->getCacheKey() + ->shouldBeCalledTimes(1) + ->wilLReturn('abc'); + + $credentials = new OAuth2Credentials($oauth2->reveal()); + $this->assertEquals( + ['Authorization' => 'Bearer 123'], + $credentials->getRequestMetadata() + ); + } + + public function testGetQuotaProject() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'getQuotaProject is not implemented for OAuth2 credentials' + ); + + $oauth2 = $this->prophesize(OAuth2::class); + $credentials = new OAuth2Credentials($oauth2->reveal()); + $credentials->getQuotaProject(); + } + + public function testGetProjectId() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'getProjectId is not implemented for OAuth2 credentials' + ); + + $oauth2 = $this->prophesize(OAuth2::class); + $credentials = new OAuth2Credentials($oauth2->reveal()); + $credentials->getProjectId(); + } +} diff --git a/tests/Auth/Credentials/ServiceAccountCredentialsTest.php b/tests/Auth/Credentials/ServiceAccountCredentialsTest.php new file mode 100644 index 000000000..237da0d9c --- /dev/null +++ b/tests/Auth/Credentials/ServiceAccountCredentialsTest.php @@ -0,0 +1,333 @@ + 'key123', + 'private_key' => 'privatekey', + 'client_email' => 'test@example.com', + 'client_id' => 'client123', + 'type' => 'service_account', + 'project_id' => 'example_project' + ]; + + public static function setUpBeforeClass(): void + { + self::$privateKey = file_get_contents( + __DIR__ . '/../fixtures/private.pem' + ); + } + + public function testShouldBeTheSameAsOAuth2WithTheSameScope() + { + $scope = ['scope/1', 'scope/2']; + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => $scope + ]); + + $reflection = new \ReflectionClass($credentials); + $method = $reflection->getMethod('getCacheKey'); + $method->setAccessible(true); + $cacheKey = $method->invoke($credentials); + + $o = new OAuth2(['scope' => $scope]); + $this->assertEquals( + $this->testJson['client_email'] . ':' . $o->getCacheKey(), + $cacheKey + ); + } + + public function testShouldBeTheSameAsOAuth2WithTheSameScopeWithSub() + { + $scope = ['scope/1', 'scope/2']; + $sub = 'sub123'; + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => $scope, + 'subject' => $sub + ]); + + $reflection = new \ReflectionClass($credentials); + $method = $reflection->getMethod('getCacheKey'); + $method->setAccessible(true); + $cacheKey = $method->invoke($credentials); + + $o = new OAuth2(['scope' => $scope]); + $this->assertEquals( + $this->testJson['client_email'] . ':' . $o->getCacheKey() . ':' . $sub, + $cacheKey + ); + } + + public function testShouldFailIfScopeIsNotAValidType() + { + $this->expectException(InvalidArgumentException::class); + + $notAnArrayOrString = new \stdClass(); + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => $notAnArrayOrString, + ]); + } + + public function testShouldFailIfJsonDoesNotHaveClientEmail() + { + $this->expectException(InvalidArgumentException::class); + + unset($this->testJson['client_email']); + $scope = ['scope/1', 'scope/2']; + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => $scope, + ]); + } + + public function testShouldFailIfJsonDoesNotHavePrivateKey() + { + $this->expectException(InvalidArgumentException::class); + + unset($this->testJson['private_key']); + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => ['scope/1', 'scope/2'], + ]); + } + + public function testFailsToInitalizeFromANonExistentFile() + { + $this->expectException(InvalidArgumentException::class); + + $keyFile = __DIR__ . '/../fixtures' . '/does-not-exist-private.json'; + $credentials = new ServiceAccountCredentials($keyFile, [ + 'scope' => ['scope/1', 'scope/2'], + ]); + } + + public function testInitalizeFromAFile() + { + $keyFile = __DIR__ . '/../fixtures' . '/private.json'; + $credentials = new ServiceAccountCredentials($keyFile, [ + 'scope' => 'scope/1' + ]); + $this->assertNotNull($credentials); + } + + public function testFailsToInitializeFromInvalidJsonData() + { + $this->expectException(LogicException::class); + + $tmp = tmpfile(); + fwrite($tmp, '{'); + + $path = stream_get_meta_data($tmp)['uri']; + + try { + new ServiceAccountCredentials($path, [ + 'scope' => 'scope/1' + ]); + } finally { + fclose($tmp); + } + } + + public function testFailsOnClientErrors() + { + $this->expectException(ClientException::class); + $this->testJson['private_key'] = self::$privateKey; + $scope = ['scope/1', 'scope/2']; + $httpClient = httpClientWithResponses([ + new Response(400), + ]); + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => $scope, + 'httpClient' => $httpClient + ]); + $credentials->fetchAuthToken(); + } + + public function testFailsOnServerErrors() + { + $this->expectException(ServerException::class); + $this->testJson['private_key'] = self::$privateKey; + $scope = ['scope/1', 'scope/2']; + $httpClient = httpClientWithResponses([ + new Response(500), + ]); + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => $scope, + 'httpClient' => $httpClient + ]); + $credentials->fetchAuthToken(); + } + + public function testCanFetchCredsOK() + { + $this->testJson['private_key'] = self::$privateKey; + $testJsonText = json_encode($this->testJson); + $httpClient = httpClientWithResponses([ + new Response(200, [], $testJsonText), + ]); + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => ['scope/1', 'scope/2'], + 'httpClient' => $httpClient + ]); + $tokens = $credentials->fetchAuthToken(); + $this->assertEquals($this->testJson, $tokens); + } + + public function testGetRequestMetadata() + { + $this->testJson['private_key'] = self::$privateKey; + $scope = ['scope/1', 'scope/2']; + $access_token = 'accessToken123'; + $responseText = json_encode(array('access_token' => $access_token)); + $httpClient = httpClientWithResponses([ + new Response(200, [], $responseText), + ]); + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => $scope, + 'httpClient' => $httpClient + ]); + $metadata = $credentials->getRequestMetadata(); + $this->assertIsArray($metadata); + + $this->assertArrayHasKey('Authorization', $metadata); + $this->assertEquals( + $metadata['Authorization'], + 'Bearer ' . $access_token + ); + } + + public function testShouldBeIdTokenWhenTargetAudienceIsSet() + { + $this->testJson['private_key'] = self::$privateKey; + $expectedToken = ['id_token' => 'idtoken12345']; + $timesCalled = 0; + $httpClient = httpClientFromCallable( + function ($request) use (&$timesCalled, $expectedToken) { + $timesCalled++; + parse_str($request->getBody(), $post); + $this->assertArrayHasKey('assertion', $post); + list($header, $payload, $sig) = explode('.', $post['assertion']); + $jwtParams = json_decode(base64_decode($payload), true); + $this->assertArrayHasKey('target_audience', $jwtParams); + $this->assertEquals('a target audience', $jwtParams['target_audience']); + + return new Response(200, [], json_encode($expectedToken)); + } + ); + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'targetAudience' => 'a target audience', + 'httpClient' => $httpClient, + ]); + $this->assertEquals($expectedToken, $credentials->fetchAuthToken()); + $this->assertEquals(1, $timesCalled); + } + + public function testSettingBothScopeAndTargetAudienceThrowsException() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Scope and targetAudience cannot both be supplied'); + + $this->testJson['private_key'] = self::$privateKey; + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => 'a-scope', + 'targetAudience' => 'a-target-audience', + ]); + } + + public function testReturnsClientEmail() + { + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => 'scope/1' + ]); + $this->assertEquals( + $this->testJson['client_email'], + $credentials->getClientEmail() + ); + } + + public function testGetProjectId() + { + $credentials = new ServiceAccountCredentials($this->testJson, [ + 'scope' => 'scope/1' + ]); + $this->assertEquals( + $this->testJson['project_id'], + $credentials->getProjectId() + ); + } + + public function testGetQuotaProject() + { + $keyFile = __DIR__ . '/../fixtures' . '/private.json'; + $credentials = new ServiceAccountCredentials($keyFile, [ + 'scope' => 'scope/1' + ]); + $this->assertEquals( + 'test_quota_project', + $credentials->getQuotaProject() + ); + } + public function testNoScopeUsesJwtAccess() + { + $this->testJson['private_key'] = self::$privateKey; + + // no scope, jwt access should be used, no outbound + // call should be made + $credentials = new ServiceAccountCredentials($this->testJson); + + $authUri = 'https://example.com/service'; + $metadata = $credentials->getRequestMetadata($authUri); + + $this->assertArrayHasKey('Authorization', $metadata); + + $bearer_token = $metadata['Authorization']; + $this->assertIsString($bearer_token); + $this->assertEquals(0, strpos($bearer_token, 'Bearer ')); + $this->assertGreaterThan(30, strlen($bearer_token)); + } + + public function testNoScopeAndNoAuthUri() + { + $this->testJson['private_key'] = self::$privateKey; + + // no scope, jwt access should be used, no outbound + // call should be made + $credentials = new ServiceAccountCredentials($this->testJson); + $this->assertNotNull($credentials); + + $metadata = $credentials->getRequestMetadata(null); + + // no access_token is added to the metadata hash + // but also, no error should be thrown + $this->assertEquals([], $metadata); + } +} diff --git a/tests/Auth/Credentials/ServiceAccountJwtAccessCredentialsTest.php b/tests/Auth/Credentials/ServiceAccountJwtAccessCredentialsTest.php new file mode 100644 index 000000000..056d7abbb --- /dev/null +++ b/tests/Auth/Credentials/ServiceAccountJwtAccessCredentialsTest.php @@ -0,0 +1,175 @@ + 'key123', + 'private_key' => 'privatekey', + 'client_email' => 'test@example.com', + 'client_id' => 'client123', + 'type' => 'service_account', + 'project_id' => 'example_project' + ]; + + private static $privateKey; + + public static function setUpBeforeClass(): void + { + self::$privateKey = file_get_contents( + __DIR__ . '/../fixtures/private.pem' + ); + } + + public function testFailsToInitalizeFromANonExistentFile() + { + $this->expectException(InvalidArgumentException::class); + $keyFile = __DIR__ . '/does-not-exist-private.json'; + new ServiceAccountJwtAccessCredentials($keyFile); + } + + public function testInitalizeFromAFile() + { + $keyFile = __DIR__ . '/../fixtures/private.json'; + $this->assertNotNull( + new ServiceAccountJwtAccessCredentials($keyFile) + ); + } + + public function testFailsToInitializeFromInvalidJsonData() + { + $this->expectException(LogicException::class); + $tmp = tmpfile(); + fwrite($tmp, '{'); + + $path = stream_get_meta_data($tmp)['uri']; + + try { + new ServiceAccountJwtAccessCredentials($path); + } finally { + fclose($tmp); + } + } + + public function testFailsOnMissingClientEmail() + { + $this->expectException(InvalidArgumentException::class); + unset($this->testJson['client_email']); + $credentials = new ServiceAccountJwtAccessCredentials($this->testJson); + } + + public function testFailsOnMissingPrivateKey() + { + $this->expectException(InvalidArgumentException::class); + unset($this->testJson['private_key']); + $credentials = new ServiceAccountJwtAccessCredentials($this->testJson); + } + + public function testCanInitializeFromJson() + { + $credentials = new ServiceAccountJwtAccessCredentials($this->testJson); + $this->assertNotNull($credentials); + } + + public function testGetRequestMetadata() + { + $this->testJson['private_key'] = self::$privateKey; + + $credentials = new ServiceAccountJwtAccessCredentials($this->testJson); + + $metadata = $credentials->getRequestMetadata(self::AUDIENCE); + + $this->assertArrayHasKey('Authorization', $metadata); + + $bearer_token = $metadata['Authorization']; + + $this->assertIsString($bearer_token); + $this->assertEquals(0, strpos($bearer_token, 'Bearer ')); + $this->assertGreaterThan(30, strlen($bearer_token)); + + $authUri = 'https://example.com/anotherService'; + $metadata2 = $credentials->getRequestMetadata($authUri); + + $this->assertArrayHasKey('Authorization', $metadata2); + + $bearer_token2 = $metadata2['Authorization']; + + $this->assertIsString($bearer_token2); + $this->assertEquals(0, strpos($bearer_token2, 'Bearer ')); + $this->assertGreaterThan(30, strlen($bearer_token2)); + $this->assertNotEquals($bearer_token2, $bearer_token); + } + + public function testCacheKeyShouldBeTheAudience() + { + $this->testJson['private_key'] = self::$privateKey; + $credentials = new ServiceAccountJwtAccessCredentials($this->testJson); + $credentials->getRequestMetadata(self::AUDIENCE); + + $reflection = new \ReflectionClass($credentials); + $method = $reflection->getMethod('getCacheKey'); + $method->setAccessible(true); + $cacheKey = $method->invoke($credentials); + + $this->assertEquals(self::AUDIENCE, $cacheKey); + } + + public function testReturnsClientEmail() + { + $this->testJson['private_key'] = self::$privateKey; + $credentials = new ServiceAccountJwtAccessCredentials($this->testJson); + $this->assertEquals( + $this->testJson['client_email'], + $credentials->getClientEmail() + ); + } + + public function testGetProjectId() + { + $this->testJson['private_key'] = self::$privateKey; + $credentials = new ServiceAccountJwtAccessCredentials($this->testJson); + $this->assertEquals( + $this->testJson['project_id'], + $credentials->getProjectId() + ); + } + + public function testGetQuotaProject() + { + $keyFile = __DIR__ . '/../fixtures/private.json'; + $credentials = new ServiceAccountJwtAccessCredentials( + $keyFile + ); + $this->assertEquals( + 'test_quota_project', + $credentials->getQuotaProject() + ); + } +} diff --git a/tests/Auth/Credentials/UserRefreshCredentialsTest.php b/tests/Auth/Credentials/UserRefreshCredentialsTest.php new file mode 100644 index 000000000..e82e1a1e1 --- /dev/null +++ b/tests/Auth/Credentials/UserRefreshCredentialsTest.php @@ -0,0 +1,193 @@ + 'client123', + 'client_secret' => 'clientSecret123', + 'refresh_token' => 'refreshToken123', + 'type' => 'authorized_user', + ]; + + public function testCacheKeyShouldBeTheSameAsOAuth2WithTheSameScope() + { + $scope = ['scope/1', 'scope/2']; + $credentials = new UserRefreshCredentials($this->testJson, [ + 'scope' => $scope, + ]); + + $reflection = new \ReflectionClass($credentials); + $method = $reflection->getMethod('getCacheKey'); + $method->setAccessible(true); + $cacheKey = $method->invoke($credentials); + + $o = new OAuth2(['scope' => $scope]); + $this->assertSame( + $this->testJson['client_id'] . ':' . $o->getCacheKey(), + $cacheKey + ); + } + + public function testShouldFailIfScopeIsNotAValidType() + { + $this->expectException(InvalidArgumentException::class); + + $notAnArrayOrString = new \stdClass(); + $credentials = new UserRefreshCredentials($this->testJson, [ + 'scope' => $notAnArrayOrString + ]); + } + + public function testShouldFailIfJsonDoesNotHaveClientSecret() + { + $this->expectException(InvalidArgumentException::class); + + unset($this->testJson['client_secret']); + $scope = ['scope/1', 'scope/2']; + $credentials = new UserRefreshCredentials($this->testJson, [ + 'scope' => $scope, + ]); + } + + public function testShouldFailIfJsonDoesNotHaveRefreshToken() + { + $this->expectException(InvalidArgumentException::class); + + unset($this->testJson['refresh_token']); + $scope = ['scope/1', 'scope/2']; + $credentials = new UserRefreshCredentials($this->testJson, [ + 'scope' => $scope, + ]); + } + + public function testShouldFailIfJsonDoesNotHaveClientId() + { + $this->expectException(InvalidArgumentException::class); + + unset($this->testJson['client_id']); + $scope = ['scope/1', 'scope/2']; + $credentials = new UserRefreshCredentials($this->testJson, [ + 'scope' => $scope, + ]); + } + + public function testFailsToInitalizeFromANonExistentFile() + { + $this->expectException(InvalidArgumentException::class); + $keyFile = __DIR__ . '/../fixtures/does-not-exist-private.json'; + new UserRefreshCredentials($keyFile, [ + 'scope' => 'scope/1', + ]); + } + + public function testInitalizeFromAFile() + { + $keyFile = __DIR__ . '/../fixtures/client_credentials.json'; + $credentials = new UserRefreshCredentials($keyFile, [ + 'scope' => 'scope/1', + ]); + $this->assertNotNull($credentials); + } + + public function testFailsToInitializeFromInvalidJsonData() + { + $this->expectException(LogicException::class); + + $tmp = tmpfile(); + fwrite($tmp, '{'); + + $path = stream_get_meta_data($tmp)['uri']; + + try { + new UserRefreshCredentials($path, [ + 'scope' => 'scope/1', + ]); + } finally { + fclose($tmp); + } + } + + public function testFailsOnClientErrors() + { + $this->expectException(ClientException::class); + + $scope = ['scope/1', 'scope/2']; + $httpClient = httpClientWithResponses([ + new Response(400), + ]); + $credentials = new UserRefreshCredentials($this->testJson, [ + 'scope' => $scope, + 'httpClient' => $httpClient, + ]); + $credentials->fetchAuthToken(); + } + + public function testFailsOnServerErrors() + { + $this->expectException(ServerException::class); + + $scope = ['scope/1', 'scope/2']; + $httpClient = httpClientWithResponses([ + new Response(500), + ]); + $credentials = new UserRefreshCredentials($this->testJson, [ + 'scope' => $scope, + 'httpClient' => $httpClient, + ]); + $credentials->fetchAuthToken(); + } + + public function testCanFetchCredsOK() + { + $jsonText = json_encode($this->testJson); + $scope = ['scope/1', 'scope/2']; + $httpClient = httpClientWithResponses([ + new Response(200, [], $jsonText), + ]); + $credentials = new UserRefreshCredentials($this->testJson, [ + 'scope' => $scope, + 'httpClient' => $httpClient, + ]); + $tokens = $credentials->fetchAuthToken($httpClient); + $this->assertEquals($this->testJson, $tokens); + } + + public function testGetQuotaProject() + { + $keyFile = __DIR__ . '/../fixtures/client_credentials.json'; + $credentials = new UserRefreshCredentials($keyFile, [ + 'scope' => 'a-scope' + ]); + $this->assertEquals( + 'test_quota_project', + $credentials->getQuotaProject() + ); + } +} diff --git a/tests/Auth/CredentialsTest.php b/tests/Auth/CredentialsTest.php new file mode 100644 index 000000000..71faac99d --- /dev/null +++ b/tests/Auth/CredentialsTest.php @@ -0,0 +1,321 @@ + 'fakeclientemail', + 'private_key' => 'fakeprivatekey', + ]; + private $userRefreshCreds = [ + 'client_id' => 'fakeclientid', + 'client_secret' => 'fakeclientsecret', + 'refresh_token' => 'fakerefreshtoken', + ]; + + public function provideIdTokenCredentials() + { + return [ + [ComputeCredentials::class], + [ServiceAccountCredentials::class, $this->serviceAccountCreds], + ]; + } + + public function provideAccessTokenCredentials() + { + return [ + [ComputeCredentials::class], + [OAuth2Credentials::class, new OAuth2()], + [ServiceAccountCredentials::class, $this->serviceAccountCreds], + [ + ServiceAccountJwtAccessCredentials::class, + $this->serviceAccountCreds, + ['audience' => 'foo'], + ], + [UserRefreshCredentials::class, $this->userRefreshCreds], + ]; + } + + public function provideGetRequestMetadataCredentials() + { + return [ + [ComputeCredentials::class], + [OAuth2Credentials::class, new OAuth2()], + [ + ServiceAccountCredentials::class, + $this->serviceAccountCreds, + ['scope' => '123'], + ], + [ + ServiceAccountJwtAccessCredentials::class, + $this->serviceAccountCreds, + ['audience' => 'foo'], + 'http://authuri/' + ], + [UserRefreshCredentials::class, $this->userRefreshCreds], + ]; + } + + /** + * @dataProvider provideAccessTokenCredentials + */ + public function testUsesCachedAccessToken( + string $credentialsClass, + $firstArgument = null, + array $options = [] + ) { + $cachedValue = ['access_token' => '2/abcdef1234567890']; + $mockCacheItem = $this->prophesize(CacheItemInterface::class); + $mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($cachedValue); + $mockCache = $this->prophesize(CacheItemPoolInterface::class); + $mockCache->getItem(Argument::type('string')) + ->shouldBeCalledTimes(1) + ->willReturn($mockCacheItem->reveal()); + + $options['cache'] = $mockCache->reveal(); + + // Run the test. + $credentials = $this->createCredentials( + $credentialsClass, + $firstArgument, + $options + ); + + $accessToken = $credentials->fetchAuthToken(); + $this->assertEquals($accessToken, $cachedValue); + } + + /** + * @dataProvider provideIdTokenCredentials + */ + public function testUsesCachedIdToken( + string $credentialsClass, + $firstArgument = null, + array $options = [] + ) { + // Fetch an access token first + $cacheKey = null; + $cachedValue = ['access_token' => '2/abcdef1234567890']; + $phpunit = $this; + $mockCacheItem = $this->prophesize(CacheItemInterface::class); + $mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($cachedValue); + $mockCache = $this->prophesize(CacheItemPoolInterface::class); + $mockCache->getItem(Argument::type('string')) + ->shouldBeCalledTimes(1) + ->will(function ($args) use (&$cacheKey, $mockCacheItem) { + $cacheKey = $args[0]; // save the cache key + return $mockCacheItem->reveal(); + }); + + $options['cache'] = $mockCache->reveal(); + + $credentials = $this->createCredentials( + $credentialsClass, + $firstArgument, + $options + ); + $accessToken = $credentials->fetchAuthToken(); + + // Now fetch an ID token + $cachedValue = ['id_token' => '2/abcdef1234567890']; + $mockCacheItem = $this->prophesize(CacheItemInterface::class); + $mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($cachedValue); + $mockCache = $this->prophesize(CacheItemPoolInterface::class); + $mockCache->getItem(Argument::type('string')) + ->shouldBeCalledTimes(1) + ->will(function ($args) use ($phpunit, $cacheKey, $mockCacheItem) { + // Assert the cache key is different for ID tokens + $phpunit->assertNotEquals($cacheKey, $args[0]); + return $mockCacheItem->reveal(); + }); + + $targetAudience = 'a-target-audience'; + $options['targetAudience'] = $targetAudience; + $options['cache'] = $mockCache->reveal(); + $credentials = $this->createCredentials( + $credentialsClass, + $firstArgument, + $options + ); + + $idToken = $credentials->fetchAuthToken(); + $this->assertEquals($idToken, $cachedValue); + } + + /** + * @dataProvider provideGetRequestMetadataCredentials + */ + public function testGetRequestMetadataWithCache( + string $credentialsClass, + $firstArgument = null, + array $options = [], + string $authUri = null + ) { + $token = '2/abcdef1234567890'; + $cachedValue = ['access_token' => $token]; + $mockCacheItem = $this->prophesize(CacheItemInterface::class); + $mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($cachedValue); + $mockCache = $this->prophesize(CacheItemPoolInterface::class); + $mockCache->getItem(Argument::type('string')) + ->shouldBeCalledTimes(1) + ->willReturn($mockCacheItem->reveal()); + + $options['cache'] = $mockCache->reveal(); + + $credentials = $this->createCredentials( + $credentialsClass, + $firstArgument, + $options + ); + + $metadata = $credentials->getRequestMetadata($authUri); + + $this->assertArrayHasKey('Authorization', $metadata); + $this->assertEquals("Bearer $token", $metadata['Authorization']); + } + + /** + * @dataProvider provideAccessTokenCredentials + */ + public function testShouldReturnValueWhenNotExpired( + string $credentialsClass, + $firstArgument = null, + array $options = [] + ) { + $token = '2/abcdef1234567890'; + $expiresAt = time() + 10; + $cachedValue = [ + 'access_token' => $token, + 'expires_at' => $expiresAt, + ]; + $mockCacheItem = $this->prophesize(CacheItemInterface::class); + $mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($cachedValue); + $mockCache = $this->prophesize(CacheItemPoolInterface::class); + $mockCache->getItem(Argument::type('string')) + ->shouldBeCalledTimes(1) + ->willReturn($mockCacheItem->reveal()); + + // Run the test. + $options['cache'] = $mockCache->reveal(); + $credentials = $this->createCredentials( + $credentialsClass, + $firstArgument, + $options + ); + + $accessToken = $credentials->fetchAuthToken(); + $this->assertEquals($accessToken, [ + 'access_token' => $token, + 'expires_at' => $expiresAt + ]); + } + + /** + * @dataProvider provideAccessTokenCredentials + */ + public function testShouldSaveValueInCacheWithCachePrefix( + string $credentialsClass, + $firstArgument = null, + array $options = [] + ) { + $phpunit = $this; + $prefix = 'mycacheprefix'; + $cachedValue = [ + 'access_token' => '2/abcdef1234567890', + 'expires_at' => time() + 10, + ]; + $mockCacheItem = $this->prophesize(CacheItemInterface::class); + $mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($cachedValue); + $mockCache = $this->prophesize(CacheItemPoolInterface::class); + $mockCache->getItem(Argument::type('string')) + ->shouldBeCalledTimes(1) + ->will(function ($args) use ($phpunit, $prefix, $mockCacheItem) { + $cacheKey = $args[0]; + $phpunit->assertStringStartsWith($prefix, $cacheKey); + return $mockCacheItem->reveal(); + }); + + // Run the test. + $options['cache'] = $mockCache->reveal(); + $options['cachePrefix'] = $prefix; + $credentials = $this->createCredentials( + $credentialsClass, + $firstArgument, + $options + ); + + $accessToken = $credentials->fetchAuthToken(); + $this->assertEquals($cachedValue, $accessToken); + } + + private function createCredentials( + string $credentialsClass, + $firstArgument, + array $options + ): CredentialsInterface { + if ($firstArgument) { + return new $credentialsClass($firstArgument, $options); + } + + return new $credentialsClass($options); + } +} diff --git a/tests/Auth/GoogleAuthTest.php b/tests/Auth/GoogleAuthTest.php new file mode 100644 index 000000000..e880e506f --- /dev/null +++ b/tests/Auth/GoogleAuthTest.php @@ -0,0 +1,985 @@ +mockCacheItem = $this->prophesize(CacheItemInterface::class); + $this->mockCache = $this->prophesize(CacheItemPoolInterface::class); + } + + public function testCachedOnComputeTrueValue() + { + $cachedValue = true; + $this->mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $this->mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($cachedValue); + $this->mockCache->getItem('google_auth_on_gce_cache') + ->shouldBeCalledTimes(1) + ->willReturn($this->mockCacheItem->reveal()); + + // Run the test. + $googleAuth = new GoogleAuth(['cache' => $this->mockCache->reveal()]); + $this->assertTrue($googleAuth->onCompute()); + } + + public function testCachedOnComputeFalseValue() + { + $cachedValue = false; + $this->mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $this->mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($cachedValue); + $this->mockCache->getItem('google_auth_on_gce_cache') + ->shouldBeCalledTimes(1) + ->willReturn($this->mockCacheItem->reveal()); + + // Run the test. + $googleAuth = new GoogleAuth(['cache' => $this->mockCache->reveal()]); + $this->assertFalse($googleAuth->onCompute()); + } + + public function testUncachedOnCompute() + { + $gceIsCalled = false; + $httpClient = httpClientFromCallable(function ($request) use (&$gceIsCalled) { + $gceIsCalled = true; + return new Response(200, ['Metadata-Flavor' => 'Google']); + }); + + $this->mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(false); + $this->mockCacheItem->set(true) + ->shouldBeCalledTimes(1); + $this->mockCacheItem->expiresAfter(1500) + ->shouldBeCalledTimes(1); + $this->mockCache->getItem('google_auth_on_gce_cache') + ->shouldBeCalledTimes(1) + ->willReturn($this->mockCacheItem->reveal()); + $this->mockCache->save($this->mockCacheItem->reveal()) + ->shouldBeCalledTimes(1); + + // Run the test. + $googleAuth = new GoogleAuth([ + 'cache' => $this->mockCache->reveal(), + 'httpClient' => $httpClient, + ]); + + $this->assertTrue($googleAuth->onCompute()); + $this->assertTrue($gceIsCalled); + } + + public function testShouldFetchFromCacheWithCacheOptions() + { + $prefix = 'test_prefix_'; + $lifetime = '70707'; + $cachedValue = true; + + $this->mockCacheItem->isHit() + ->willReturn(true); + $this->mockCacheItem->get() + ->willReturn($cachedValue); + $this->mockCache->getItem($prefix . 'google_auth_on_gce_cache') + ->shouldBeCalledTimes(1) + ->willReturn($this->mockCacheItem->reveal()); + + // Run the test + $googleAuth = new GoogleAuth([ + 'cachePrefix' => $prefix, + 'cacheLifetime' => $lifetime, + 'cache' => $this->mockCache->reveal(), + ]); + $this->assertTrue($googleAuth->onCompute()); + } + + public function testShouldSaveValueInCacheWithCacheOptions() + { + $prefix = 'test_prefix_'; + $lifetime = '70707'; + $gceIsCalled = false; + $httpClient = httpClientFromCallable(function ($request) use (&$gceIsCalled) { + $gceIsCalled = true; + return new Response(200, ['Metadata-Flavor' => 'Google']); + }); + $this->mockCacheItem->isHit() + ->willReturn(false); + $this->mockCacheItem->set(true) + ->shouldBeCalledTimes(1); + $this->mockCacheItem->expiresAfter($lifetime) + ->shouldBeCalledTimes(1); + $this->mockCache->getItem($prefix . 'google_auth_on_gce_cache') + ->shouldBeCalledTimes(1) + ->willReturn($this->mockCacheItem->reveal()); + $this->mockCache->save($this->mockCacheItem->reveal()) + ->shouldBeCalled(); + + // Run the test + $googleAuth = new GoogleAuth([ + 'cachePrefix' => $prefix, + 'cacheLifetime' => $lifetime, + 'cache' => $this->mockCache->reveal(), + 'httpClient' => $httpClient, + ]); + $onCompute = $googleAuth->onCompute(); + $this->assertTrue($onCompute); + $this->assertTrue($gceIsCalled); + } + + public function testIsFailsEnvSpecifiesNonExistentFile() + { + $this->expectException('DomainException'); + + $keyFile = __DIR__ . '/does-not-exist-private.json'; + putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $keyFile); + (new GoogleAuth())->makeCredentials(['scope' => 'a scope']); + } + + public function testLoadsOKIfEnvSpecifiedIsValid() + { + $keyFile = __DIR__ . '/fixtures/private.json'; + putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $keyFile); + $this->assertNotNull( + (new GoogleAuth())->makeCredentials(['scope' => 'a scope']) + ); + } + + public function testLoadsDefaultFileIfPresentAndEnvVarIsNotSet() + { + putenv('HOME=' . __DIR__ . '/fixtures/gcloud1'); + $this->assertNotNull( + (new GoogleAuth())->makeCredentials(['scope' => 'a scope']) + ); + } + + public function testFailsIfNotOnGceAndNoDefaultFileFound() + { + $this->expectException('DomainException'); + + putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + // simulate not being GCE and retry attempts by returning multiple 500s + $httpClient = httpClientWithResponses([ + new Response(500), + new Response(500), + new Response(500) + ]); + $googleAuth = new GoogleAuth([ + 'httpClient' => $httpClient, + ]); + $googleAuth->makeCredentials(['scope' => 'a scope']); + } + + public function testSuccedsIfNoDefaultFilesButIsOnCompute() + { + $wantedTokens = [ + 'access_token' => '1/abdef1234567890', + 'expires_in' => '57', + 'token_type' => 'Bearer', + ]; + $jsonTokens = json_encode($wantedTokens); + + // simulate the response from GCE. + $httpClient = httpClientWithResponses([ + new Response(200, ['Metadata-Flavor' => 'Google']), + new Response(200, [], Psr7\stream_for($jsonTokens)), + ]); + $googleAuth = new GoogleAuth([ + 'httpClient' => $httpClient, + ]); + $this->assertNotNull( + $googleAuth->makeCredentials(['scope' => 'a scope']) + ); + } + + public function testComputeCredentials() + { + $jsonTokens = json_encode(['access_token' => 'abc']); + $httpClient = httpClientWithResponses([ + new Response(200, ['Metadata-Flavor' => 'Google']), + new Response(200, [], Psr7\stream_for($jsonTokens)), + ]); + $googleAuth = new GoogleAuth([ + 'httpClient' => $httpClient, + ]); + $credentials = $googleAuth->makeCredentials([ + 'defaultScope' => 'a-default-scope' + ]); + + $this->assertInstanceOf(ComputeCredentials::class, $credentials); + + $uriMethod = (new ReflectionClass($credentials))->getMethod('getTokenUri'); + $uriMethod->setAccessible(true); + + // used default scope + $tokenUri = $uriMethod->invoke($credentials); + $this->assertStringContainsString('a-default-scope', $tokenUri); + + $credentials = $googleAuth->makeCredentials([ + 'scope' => 'a-user-scope', + 'defaultScope' => 'a-default-scope' + ]); + + // did not use default scope + $tokenUri = $uriMethod->invoke($credentials); + $this->assertStringContainsString('a-user-scope', $tokenUri); + } + + public function testUserRefreshCredentials() + { + putenv('HOME=' . __DIR__ . '/fixtures/gcloud2'); + + $credentials = (new GoogleAuth())->makeCredentials(); + + $this->assertInstanceOf(UserRefreshCredentials::class, $credentials); + } + + public function testServiceAccountCredentialsDoNotUseDefaultScope() + { + putenv('HOME=' . __DIR__ . '/fixtures/gcloud1'); + + $credentials = (new GoogleAuth())->makeCredentials([ + 'defaultScope' => 'a-default-scope', + ]); + $this->assertInstanceOf(ServiceAccountCredentials::class, $credentials); + + $authProp = (new ReflectionClass($credentials))->getProperty('oauth2'); + $authProp->setAccessible(true); + $oauth2 = $authProp->getValue($credentials); + + // used default scope + $this->assertNull($oauth2->getScope()); + + $credentials = (new GoogleAuth())->makeCredentials([ + 'scope' => 'a-user-scope', + 'defaultScope' => 'a-default-scope', + ]); + + $oauth2 = $authProp->getValue($credentials); + + // used user scope + $this->assertEquals('a-user-scope', $oauth2->getScope()); + } + + public function testComputeCredentialsDefaultScopeArray() + { + $jsonTokens = json_encode(['access_token' => 'abc']); + $httpClient = httpClientWithResponses([ + new Response(200, ['Metadata-Flavor' => 'Google']), + new Response(200, [], Psr7\stream_for($jsonTokens)), + ]); + + $googleAuth = new GoogleAuth(['httpClient' => $httpClient]); + $credentials = $googleAuth->makeCredentials([ + 'defaultScope' => ['default-scope-one', 'default-scope-two'] + ]); + $this->assertInstanceOf(ComputeCredentials::class, $credentials); + $uriMethod = (new ReflectionClass($credentials))->getMethod('getTokenUri'); + $uriMethod->setAccessible(true); + $tokenUri = $uriMethod->invoke($credentials); + + // used default scope + $this->assertStringContainsString( + 'default-scope-one,default-scope-two', + $tokenUri + ); + } + + + // TODO: Refactor Middleware Tests + + // /** + // * @expectedException DomainException + // */ + // public function testOnComputeCacheWithHit() + // { + // putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + + // $mockCacheItem = $this->prophesize(CacheItemInterface::class); + // $mockCacheItem->isHit() + // ->willReturn(true); + // $mockCacheItem->get() + // ->shouldBeCalledTimes(1) + // ->willReturn(false); + + // $mockCache = $this->prophesize(CacheItemPoolInterface::class); + // $mockCache->getItem('google_auth_on_gce_cache') + // ->shouldBeCalledTimes(1) + // ->willReturn($mockCacheItem->reveal()); + + // ApplicationDefaultCredentials::getMiddleware( + // 'a scope', + // null, + // null, + // $mockCache->reveal() + // ); + // } + + // public function testOnComputeCacheWithoutHit() + // { + // putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + + // $gceIsCalled = false; + // $dummyHandler = function ($request) use (&$gceIsCalled) { + // $gceIsCalled = true; + // return new Response(200, ['Metadata-Flavor' => 'Google']); + // }; + // $mockCacheItem = $this->prophesize(CacheItemInterface::class); + // $mockCacheItem->isHit() + // ->willReturn(false); + // $mockCacheItem->set(true) + // ->shouldBeCalledTimes(1); + // $mockCacheItem->expiresAfter(1500) + // ->shouldBeCalledTimes(1); + + // $mockCache = $this->prophesize(CacheItemPoolInterface::class); + // $mockCache->getItem('google_auth_on_gce_cache') + // ->shouldBeCalledTimes(1) + // ->willReturn($mockCacheItem->reveal()); + // $mockCache->save($mockCacheItem->reveal()) + // ->shouldBeCalled(); + + // $credentials = ApplicationDefaultCredentials::getMiddleware( + // 'a scope', + // $dummyHandler, + // null, + // $mockCache->reveal() + // ); + + // $this->assertTrue($gceIsCalled); + // } + + // public function testOnComputeCacheWithOptions() + // { + // putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + + // $prefix = 'test_prefix_'; + // $lifetime = '70707'; + + // $gceIsCalled = false; + // $dummyHandler = function ($request) use (&$gceIsCalled) { + // $gceIsCalled = true; + // return new Response(200, ['Metadata-Flavor' => 'Google']); + // }; + // $mockCacheItem = $this->prophesize(CacheItemInterface::class); + // $mockCacheItem->isHit() + // ->willReturn(false); + // $mockCacheItem->set(true) + // ->shouldBeCalledTimes(1); + // $mockCacheItem->expiresAfter($lifetime) + // ->shouldBeCalledTimes(1); + + // $mockCache = $this->prophesize(CacheItemPoolInterface::class); + // $mockCache->getItem($prefix . 'google_auth_on_gce_cache') + // ->shouldBeCalledTimes(1) + // ->willReturn($mockCacheItem->reveal()); + // $mockCache->save($mockCacheItem->reveal()) + // ->shouldBeCalled(); + + // $credentials = ApplicationDefaultCredentials::getMiddleware( + // 'a scope', + // $dummyHandler, + // ['gce_prefix' => $prefix, 'gce_lifetime' => $lifetime], + // $mockCache->reveal() + // ); + + // $this->assertTrue($gceIsCalled); + // } + + // /** + // * @expectedException DomainException + // */ + // public function testFailsIfNotOnGceAndNoDefaultFileFound() + // { + // putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + + // // simulate not being GCE and retry attempts by returning multiple 500s + // $httpClient = httpClientWithResponses([ + // new Response(500), + // new Response(500), + // new Response(500) + // ]); + + // GoogleAuth::getIdTokenCredentials( + // self::TEST_TARGET_AUDIENCE, + // $httpClient + // ); + // } + + // public function testIdTokenIfNoDefaultFilesButIsOnCompute() + // { + // putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + // $wantedTokens = [ + // 'access_token' => '1/abdef1234567890', + // 'expires_in' => '57', + // 'token_type' => 'Bearer', + // ]; + // $jsonTokens = json_encode($wantedTokens); + + // // simulate the response from GCE. + // $httpClient = httpClientWithResponses([ + // new Response(200, ['Metadata-Flavor' => 'Google']), + // new Response(200, [], Psr7\stream_for($jsonTokens)), + // ]); + + // $credentials = GoogleAuth::getIdTokenCredentials( + // self::TEST_TARGET_AUDIENCE, + // $httpClient + // ); + + // $this->assertInstanceOf( + // ComputeCredentials::class, + // $credentials + // ); + // } + + public function testWithServiceAccountCredentialsAndExplicitQuotaProject() + { + $keyFile = __DIR__ . '/fixtures/private.json'; + putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $keyFile); + + $credentials = (new GoogleAuth())->makeCredentials([ + 'quotaProject' => self::TEST_QUOTA_PROJECT + ]); + $this->assertInstanceOf(ServiceAccountCredentials::class, $credentials); + + $this->assertEquals( + self::TEST_QUOTA_PROJECT, + $credentials->getQuotaProject() + ); + } + + public function testGetCredentialsUtilizesQuotaProjectInKeyFile() + { + $keyFile = __DIR__ . '/fixtures/private.json'; + putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $keyFile); + + $credentials = (new GoogleAuth())->makeCredentials(); + + $this->assertEquals( + 'test_quota_project', + $credentials->getQuotaProject() + ); + } + + public function testWithFetchAuthTokenCacheAndExplicitQuotaProject() + { + $keyFile = __DIR__ . '/fixtures/private.json'; + putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $keyFile); + + $httpClient = httpClientWithResponses([ + new Response(200), + ]); + + $cachePool = $this->prophesize(CacheItemPoolInterface::class); + + $googleAuth = new GoogleAuth([ + 'cache' => $cachePool->reveal(), + 'httpClient' => $httpClient, + ]); + + $credentials = $googleAuth->makeCredentials([ + 'quotaProject' => self::TEST_QUOTA_PROJECT + ]); + + $this->assertInstanceOf(ServiceAccountCredentials::class, $credentials); + + $this->assertEquals( + self::TEST_QUOTA_PROJECT, + $credentials->getQuotaProject() + ); + } + + public function testWithComputeCredentials() + { + putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + $wantedTokens = [ + 'access_token' => '1/abdef1234567890', + 'expires_in' => '57', + 'token_type' => 'Bearer', + ]; + $jsonTokens = json_encode($wantedTokens); + + // simulate the response from GCE. + $httpClient = httpClientWithResponses([ + new Response(200, ['Metadata-Flavor' => 'Google']), + new Response(200, [], Psr7\stream_for($jsonTokens)), + ]); + + $googleAuth = new GoogleAuth([ + 'httpClient' => $httpClient + ]); + $credentials = $googleAuth->makeCredentials([ + 'quotaProject' => self::TEST_QUOTA_PROJECT, + ]); + + $this->assertInstanceOf(ComputeCredentials::class, $credentials); + + $this->assertEquals( + self::TEST_QUOTA_PROJECT, + $credentials->getQuotaProject() + ); + } + + // START ADCGetCredentialsAppEngineTest + + public function testAppEngineFlexible() + { + $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; + putenv('GAE_INSTANCE=aef-default-20180313t154438'); + $httpClient = httpClientWithResponses([ + new Response(200, ['Metadata-Flavor' => 'Google']), + ]); + $googleAuth = new GoogleAuth([ + 'httpClient' => $httpClient + ]); + $this->assertInstanceOf( + ComputeCredentials::class, + $googleAuth->makeCredentials() + ); + } + + public function testAppEngineFlexibleIdToken() + { + $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; + putenv('GAE_INSTANCE=aef-default-20180313t154438'); + $httpClient = httpClientWithResponses([ + new Response(200, ['Metadata-Flavor' => 'Google']), + ]); + $googleAuth = new GoogleAuth(['httpClient' => $httpClient]); + $credentials = $googleAuth->makeCredentials([ + 'targetAudience' => self::TEST_TARGET_AUDIENCE, + ]); + $this->assertInstanceOf(ComputeCredentials::class, $credentials); + $uriMethod = (new ReflectionClass($credentials))->getMethod('getTokenUri'); + $uriMethod->setAccessible(true); + $tokenUri = $uriMethod->invoke($credentials); + $this->assertStringContainsString('/identity', $tokenUri); + $this->assertStringContainsString(self::TEST_TARGET_AUDIENCE, $tokenUri); + } + + // START GoogleAuthFetchCertsTest + + public function testGetCertsForIap() + { + $iapJwkUrl = 'https://www.gstatic.com/iap/verify/public_key-jwk'; + $googleAuth = new GoogleAuth(); + $reflector = new \ReflectionClass($googleAuth); + $getCertsMethod = $reflector->getMethod('getCerts'); + $getCertsMethod->setAccessible(true); + $cacheKey = 'test_cache_key'; + $certs = $getCertsMethod->invoke( + $googleAuth, + $iapJwkUrl, + $cacheKey + ); + + $this->assertTrue(is_array($certs)); + $this->assertEquals(5, count($certs['keys'])); + } + + public function testRetrieveCertsFromLocationLocalFile() + { + $validToken = ['iss' => 'https://accounts.google.com']; + $certsLocation = __DIR__ . '/fixtures/federated-certs.json'; + $certsData = json_decode(file_get_contents($certsLocation), true); + $parsedCertsData = []; + + $item = $this->prophesize(CacheItemInterface::class); + $item->get() + ->shouldBeCalledTimes(1) + ->willReturn(null); + $item->set($certsData) + ->shouldBeCalledTimes(1); + $item->expiresAfter(Argument::type('int')) + ->shouldBeCalledTimes(1); + + $this->mockCache->getItem('google_auth_certs_cache|' . sha1($certsLocation)) + ->shouldBeCalledTimes(1) + ->willReturn($item->reveal()); + + $this->mockCache->save(Argument::type(CacheItemInterface::class)) + ->shouldBeCalledTimes(1); + + $jwt = $this->prophesize(JwtClientInterface::class); + $jwt->parseKeySet($certsData) + ->shouldBeCalledTimes(1) + ->willReturn($parsedCertsData); + $jwt->decode(self::TEST_TOKEN, $parsedCertsData, ['RS256']) + ->shouldBeCalledTimes(1) + ->willReturn($validToken); + + $googleAuth = new GoogleAuth([ + 'cache' => $this->mockCache->reveal(), + 'jwtClient' => $jwt->reveal(), + ]); + + $this->assertEquals($validToken, $googleAuth->verify(self::TEST_TOKEN, [ + 'certsLocation' => $certsLocation + ])); + } + + public function testRetrieveCertsFromLocationLocalFileInvalidFilePath() + { + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Failed to retrieve verification certificates from path'); + + $certsLocation = __DIR__ . '/fixtures/federated-certs-does-not-exist.json'; + + $item = $this->prophesize(CacheItemInterface::class); + $item->get() + ->shouldBeCalledTimes(1) + ->willReturn(null); + + $this->mockCache->getItem('google_auth_certs_cache|' . sha1($certsLocation)) + ->shouldBeCalledTimes(1) + ->willReturn($item->reveal()); + + $googleAuth = new GoogleAuth(['cache' => $this->mockCache->reveal()]); + + $googleAuth->verify(self::TEST_TOKEN, [ + 'certsLocation' => $certsLocation + ]); + } + + public function testRetrieveCertsInvalidData() + { + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('certs expects "keys" to be set'); + + $item = $this->prophesize(CacheItemInterface::class); + $item->get() + ->shouldBeCalledTimes(1) + ->willReturn('{}'); + + $this->mockCache->getItem('google_auth_certs_cache|' . self::OIDC_CERTS_HASH) + ->shouldBeCalledTimes(1) + ->willReturn($item->reveal()); + + $googleAuth = new GoogleAuth(['cache' => $this->mockCache->reveal()]); + + $googleAuth->verify(self::TEST_TOKEN); + } + + public function testRetrieveCertsFromLocationLocalFileInvalidFileData() + { + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('certs expects "keys" to be set'); + + $temp = tmpfile(); + fwrite($temp, '{}'); + $certsLocation = stream_get_meta_data($temp)['uri']; + + $item = $this->prophesize(CacheItemInterface::class); + $item->get() + ->shouldBeCalledTimes(1) + ->willReturn(null); + + $this->mockCache->getItem('google_auth_certs_cache|' . sha1($certsLocation)) + ->shouldBeCalledTimes(1) + ->willReturn($item->reveal()); + + $googleAuth = new GoogleAuth(['cache' => $this->mockCache->reveal()]); + + $googleAuth->verify(self::TEST_TOKEN, [ + 'certsLocation' => $certsLocation + ]); + } + + public function testRetrieveCertsFromLocationRemote() + { + $validToken = ['iss' => 'https://accounts.google.com']; + $certsLocation = __DIR__ . '/fixtures/federated-certs.json'; + $certsJson = file_get_contents($certsLocation); + $certsData = json_decode($certsJson, true); + $parsedCertsData = []; + + $httpClient = httpClientFromCallable( + function ($request) use ($certsJson) { + $this->assertEquals( + 'https://www.googleapis.com/oauth2/v3/certs', + (string) $request->getUri() + ); + $this->assertEquals('GET', $request->getMethod()); + + return new Response(200, [], $certsJson); + } + ); + + $item = $this->prophesize(CacheItemInterface::class); + $item->get() + ->shouldBeCalledTimes(1) + ->willReturn(null); + $item->set($certsData) + ->shouldBeCalledTimes(1); + $item->expiresAfter(1500) + ->shouldBeCalledTimes(1); + + $this->mockCache->getItem('google_auth_certs_cache|' . self::OIDC_CERTS_HASH) + ->shouldBeCalledTimes(1) + ->willReturn($item->reveal()); + + $this->mockCache->save(Argument::type(CacheItemInterface::class)) + ->shouldBeCalledTimes(1); + + $jwt = $this->prophesize(JwtClientInterface::class); + $jwt->parseKeySet($certsData) + ->shouldBeCalledTimes(1) + ->willReturn($parsedCertsData); + $jwt->decode(self::TEST_TOKEN, $parsedCertsData, ['RS256']) + ->shouldBeCalledTimes(1) + ->willReturn($validToken); + + $googleAuth = new GoogleAuth([ + 'cache' => $this->mockCache->reveal(), + 'httpClient' => $httpClient, + 'jwtClient' => $jwt->reveal(), + ]); + + $this->assertEquals($validToken, $googleAuth->verify(self::TEST_TOKEN)); + } + + public function testRetrieveCertsFromLocationRemoteBadRequest() + { + $this->expectException('RuntimeException'); + $this->expectExceptionMessage('bad news guys'); + + $badBody = 'bad news guys'; + + $httpClient = httpClientWithResponses([ + new Response(500, [], $badBody), + ]); + + $item = $this->prophesize(CacheItemInterface::class); + $item->get() + ->shouldBeCalledTimes(1) + ->willReturn(null); + + $this->mockCache->getItem('google_auth_certs_cache|' . self::OIDC_CERTS_HASH) + ->shouldBeCalledTimes(1) + ->willReturn($item->reveal()); + + $googleAuth = new GoogleAuth([ + 'httpClient' => $httpClient, + 'cache' => $this->mockCache->reveal() + ]); + + $googleAuth->verify(self::TEST_TOKEN); + } + + /** + * @dataProvider provideVerify + */ + public function testVerify( + array $payload, + array $options = [], + array $expectedException = null, + string $expectedAlg = 'RS256' + ) { + $parsedCertsData = []; + $jwtClient = $this->prophesize(JwtClientInterface::class); + $jwtClient->parseKeySet(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($parsedCertsData); + $jwtClient->decode(self::TEST_TOKEN, $parsedCertsData, [$expectedAlg]) + ->shouldBeCalledTimes(1) + ->willReturn($payload); + + $googleAuth = new GoogleAuth([ + 'jwtClient' => $jwtClient->reveal(), + ]); + + if ($expectedException) { + $this->expectException($expectedException['class']); + $this->expectExceptionMessage($expectedException['message']); + } + + $ret = $googleAuth->verify(self::TEST_TOKEN, $options); + + // This is only called when exceptions are not thrown + $this->assertEquals($payload, $ret); + } + + public function provideVerify() + { + $audienceDoesNotMatchException = [ + 'class' => UnexpectedValueException::class, + 'message' => 'Audience does not match' + ]; + $issuerDoesNotMatchException = [ + 'class' => UnexpectedValueException::class, + 'message' => 'Issuer does not match' + ]; + return [ + [ + 'payload' => [ + 'iss' => GoogleAuth::OIDC_ISSUERS[1], + ] + ], + [ + 'payload' => [ + 'iss' => GoogleAuth::OIDC_ISSUERS[1], + 'aud' => 'foo' + ], + 'options' => ['audience' => 'foo'] + ], + [ + 'payload' => [ + 'iss' => GoogleAuth::OIDC_ISSUERS[1], + 'aud' => 'foo' + ], + 'options' => ['audience' => 'bar'], + 'expectedException' => $audienceDoesNotMatchException, + + ], + [ + 'payload' => [ + 'iss' => 'invalid' + ], + 'options' => [], + 'expectedException' => $issuerDoesNotMatchException, + ], + [ + 'payload' => [ + 'iss' => 'baz' + ], + 'options' => ['issuer' => 'baz'] + ], + [ + 'payload' => [ + 'iss' => GoogleAuth::IAP_ISSUERS[0] + ], + 'options' => ['certsLocation' => GoogleAuth::IAP_JWK_URI], + 'expectedException' => null, + 'expectedAlg' => 'ES256', + ], + [ + 'payload' => [ + 'iss' => 'invalid', + ], + 'options' => ['certsLocation' => GoogleAuth::IAP_JWK_URI], + 'expectedException' => $issuerDoesNotMatchException, + 'expectedAlg' => 'ES256', + ], + [ + 'payload' => [ + 'iss' => 'baz' + ], + 'options' => [ + 'issuer' => 'baz', + 'certsLocation' => GoogleAuth::IAP_JWK_URI, + ], + 'expectedException' => null, + 'expectedAlg' => 'ES256', + ], + [ + 'payload' => [ + 'iss' => GoogleAuth::IAP_ISSUERS[0], + 'aud' => 'foo' + ], + 'options' => [ + 'audience' => 'bar', + 'certsLocation' => GoogleAuth::IAP_JWK_URI + ], + 'expectedException' => $audienceDoesNotMatchException, + 'expectedAlg' => 'ES256', + ], [ + 'payload' => [ + 'iss' => 'baz' + ], + 'options' => [ + 'certsLocation' => GoogleAuth::IAP_JWK_URI + ], + 'expectedException' => $issuerDoesNotMatchException, + 'expectedAlg' => 'ES256', + ] + ]; + } + + public function testEsVerifyEndToEnd() + { + if (!$idToken = getenv('IAP_IDENTITY_TOKEN')) { + $this->markTestSkipped('Set the IAP_IDENTITY_TOKEN env var'); + } + + $googleAuth = new GoogleAuth(); + + $parsedCertsData = []; + $jwtClient = $this->prophesize(JwtClientInterface::class); + $jwtClient->parseKeySet(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($parsedCertsData); + + $jwtClient->decode($idToken, $parsedCertsData, ['ES256']) + ->shouldBeCalledTimes(1) + ->will(function (array $args) { + // Skip validation + $tks = \explode('.', $args[0]); + list($headb64, $bodyb64, $cryptob64) = $tks; + return (array) JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64)); + }); + + $googleAuth = new GoogleAuth([ + 'jwtClient' => $jwtClient->reveal(), + ]); + + // Use Iap Cert URL + $payload = $googleAuth->verify($idToken, [ + 'certsLocation' => GoogleAuth::IAP_JWK_URI, + 'issuer' => 'https://cloud.google.com/iap', + ]); + + $this->assertIsArray($payload); + $this->assertArrayHasKey('iss', $payload); + $this->assertEquals('https://cloud.google.com/iap', $payload['iss']); + } +} diff --git a/tests/Auth/Http/ApiKeyClientTest.php b/tests/Auth/Http/ApiKeyClientTest.php new file mode 100644 index 000000000..54f448b39 --- /dev/null +++ b/tests/Auth/Http/ApiKeyClientTest.php @@ -0,0 +1,67 @@ +prophesize(ClientInterface::class); + $client->send(Argument::type(RequestInterface::class), []) + ->shouldBeCalledTimes(1) + ->will(function (array $args) use ($phpunit) { + $request = $args[0]; + $uri = $request->getUri(); + $phpunit->assertEquals('key=apikey123', $uri->getQuery()); + return new Response(200); + }); + + $apiKeyClient = new ApiKeyClient($apiKey, $client->reveal()); + $apiKeyClient->send(new Request('GET', 'http://foo/')); + } + + public function testSendAsync() + { + $apiKey = 'apikey123'; + $phpunit = $this; + $promise = $this->prophesize(PromiseInterface::class); + $client = $this->prophesize(ClientInterface::class); + $client->sendAsync(Argument::type(RequestInterface::class), []) + ->shouldBeCalledTimes(1) + ->will(function (array $args) use ($phpunit, $promise) { + $request = $args[0]; + $uri = $request->getUri(); + $phpunit->assertEquals('key=apikey123', $uri->getQuery()); + return $promise; + }); + + $apiKeyClient = new ApiKeyClient($apiKey, $client->reveal()); + $apiKeyClient->sendAsync(new Request('GET', 'http://foo/')); + } +} diff --git a/tests/Auth/Http/ClientFactoryTest.php b/tests/Auth/Http/ClientFactoryTest.php new file mode 100644 index 000000000..7ff0cd50b --- /dev/null +++ b/tests/Auth/Http/ClientFactoryTest.php @@ -0,0 +1,44 @@ +assertInstanceOf(GuzzleClient::class, $client); + + $reflection = new \ReflectionClass($client); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $guzzleClient = $property->getValue($client); + + if (defined(sprintf('%s::MAJOR_VERSION', get_class($guzzleClient)))) { + // Assert Guzzle 7 + $this->assertEquals(7, $guzzleClient::MAJOR_VERSION); + } else { + $version = $guzzleClient::VERSION; + $this->assertEquals('6', $version[0]); + } + } +} diff --git a/tests/Auth/Http/CredentialsClientTest.php b/tests/Auth/Http/CredentialsClientTest.php new file mode 100644 index 000000000..af5b55138 --- /dev/null +++ b/tests/Auth/Http/CredentialsClientTest.php @@ -0,0 +1,84 @@ +prophesize(CredentialsInterface::class); + $credentials->getRequestMetadata() + ->shouldBeCalledTimes(1) + ->willReturn(['Authorization' => 'Bearer 123']); + + $client = $this->prophesize(ClientInterface::class); + $client->send(Argument::type(RequestInterface::class), []) + ->will(function (array $args) use ($phpunit) { + $request = $args[0]; + $phpunit->assertEquals( + 'Bearer 123', + $request->getHeaderLine('Authorization') + ); + return new Response(200); + }); + + $credentialsClient = new CredentialsClient( + $credentials->reveal(), + $client->reveal() + ); + $credentialsClient->send(new Request('GET', 'http://foo/')); + } + + public function testSendAsync() + { + $phpunit = $this; + $credentials = $this->prophesize(CredentialsInterface::class); + $credentials->getRequestMetadata() + ->shouldBeCalledTimes(1) + ->willReturn(['Authorization' => 'Bearer 123']); + $promise = $this->prophesize(PromiseInterface::class); + + $client = $this->prophesize(ClientInterface::class); + $client->sendAsync(Argument::type(RequestInterface::class), []) + ->will(function (array $args) use ($phpunit, $promise) { + $request = $args[0]; + $phpunit->assertEquals( + 'Bearer 123', + $request->getHeaderLine('Authorization') + ); + return $promise; + }); + + $credentialsClient = new CredentialsClient( + $credentials->reveal(), + $client->reveal() + ); + $credentialsClient->sendAsync(new Request('GET', 'http://foo/')); + } +} diff --git a/tests/Auth/Jwt/FirebaseJwtClientTest.php b/tests/Auth/Jwt/FirebaseJwtClientTest.php new file mode 100644 index 000000000..5d0acfb8f --- /dev/null +++ b/tests/Auth/Jwt/FirebaseJwtClientTest.php @@ -0,0 +1,147 @@ +prophesize(JWK::class)->reveal() + ); + + if ($expectedException) { + $this->expectException($expectedException['class']); + $this->expectExceptionMessage($expectedException['message']); + } + + $token = 'test.to.ken'; + $keys = []; + $allowedAlgs = []; + $response = $jwtClient->decode($token, $keys, $allowedAlgs); + + // This is only called when exceptions are not thrown + $this->assertEquals($payload, $res); + } + + public function provideDecode() + { + $payload = [ + 'iat' => time(), + 'exp' => time() + 30, + 'name' => 'foo', + 'iss' => GoogleAuth::OIDC_ISSUERS[0], + ]; + return [ + [ + 'payload' => $payload, + 'expectedException' => [ + 'class' => ExpiredException::class, + 'message' => 'expired!' + ] + ], + [ + 'payload' => $payload, + 'expectedException' => [ + 'class' => SignatureInvalidException::class, + 'message' => 'invalid signature!' + ] + ], + [ + 'payload' => $payload, + 'expectedException' => [ + 'class' => UnexpectedValueException::class, + 'message' => 'invalid token!' + ] + ], + [ + 'payload' => $payload, + 'expectedException' => [ + 'class' => BeforeValidException::class, + 'message' => 'ineligible cbf!' + ] + ], + ]; + } + + public function testDecodeFailsIfTokenIsInvalid() + { + $this->expectException('UnexpectedValueException'); + + $not_a_jwt = 'not a jwt'; + $jwtClient = new FirebaseJwtClient(new JWT, new JWK); + $jwtClient->decode($not_a_jwt, ['keys' => []], ['algs']); + } + + public function testEncodeDecode() + { + $publicKey = file_get_contents(__DIR__ . '/../fixtures/public.pem'); + $privateKey = file_get_contents(__DIR__ . '/../fixtures/private.pem'); + + $now = time(); + $jwtPayload = [ + 'aud' => 'myaccount.on.host.issuer.com', + 'iss' => 'an.issuer.com', + 'exp' => $now + 65, // arbitrary + 'iat' => $now, + ]; + $jwtClient = new FirebaseJwtClient(new JWT, new JWK); + $jwt = $jwtClient->encode($jwtPayload, $privateKey, 'RS256', 'kid'); + + $decoded = $jwtClient->decode($jwt, ['kid' => $publicKey], ['RS256']); + $this->assertEquals($jwtPayload['aud'], $decoded['aud']); + } +} diff --git a/tests/OAuth2Test.php b/tests/Auth/OAuth2Test.php similarity index 64% rename from tests/OAuth2Test.php rename to tests/Auth/OAuth2Test.php index 6f3916f65..6a2c5e17c 100644 --- a/tests/OAuth2Test.php +++ b/tests/Auth/OAuth2Test.php @@ -17,32 +17,85 @@ namespace Google\Auth\Tests; +use Firebase\JWT\JWT; +use Firebase\JWT\JWK; +use Google\Auth\Jwt\FirebaseJwtClient; +use Google\Auth\Credentials\CredentialsInterface; +use Google\Auth\GoogleAuth; use Google\Auth\OAuth2; use GuzzleHttp\Psr7; +use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\ServerException; +use InvalidArgumentException; +use Psr\Http\Message\RequestInterface; use PHPUnit\Framework\TestCase; +use UnexpectedValueException; -class OAuth2AuthorizationUriTest extends TestCase +class OAuth2Test extends TestCase { + private $privateKey; + private $cache; + private $payload; + private $publicKey; + private $allowedAlgs; + + private $justClientId = [ + 'clientID' => 'aClientID', + ]; + private $minimal = [ 'authorizationUri' => 'https://accounts.test.org/insecure/url', 'redirectUri' => 'https://accounts.test.org/redirect/url', 'clientId' => 'aClientID', ]; - /** - * @expectedException InvalidArgumentException - */ + private $fetchAuthTokenMinimal = [ + 'tokenCredentialUri' => 'https://tokens_r_us/test', + 'scope' => 'https://www.googleapis.com/auth/userinfo.profile', + 'signingKey' => 'example_key', + 'signingAlgorithm' => 'HS256', + 'issuer' => 'app@example.com', + 'audience' => 'accounts.google.com', + 'clientId' => 'aClientID', + ]; + + private $tokenRequestMinimal = [ + 'tokenCredentialUri' => 'https://tokens_r_us/test', + 'scope' => 'https://www.googleapis.com/auth/userinfo.profile', + 'issuer' => 'app@example.com', + 'audience' => 'accounts.google.com', + 'clientId' => 'aClientID', + ]; + + private $signingMinimal = [ + 'signingKey' => 'example_key', + 'signingAlgorithm' => 'HS256', + 'signingKeyId' => 'keyid', + 'scope' => 'https://www.googleapis.com/auth/userinfo.profile', + 'issuer' => 'app@example.com', + 'audience' => 'accounts.google.com', + 'clientId' => 'aClientID', + ]; + + public function setUp(): void + { + $this->publicKey = + file_get_contents(__DIR__ . '/fixtures/public.pem'); + $this->privateKey = + file_get_contents(__DIR__ . '/fixtures/private.pem'); + } + public function testIsNullIfAuthorizationUriIsNull() { - $o = new OAuth2([]); + $this->expectException('InvalidArgumentException'); + $o = new OAuth2(); $this->assertNull($o->buildFullAuthorizationUri()); } - /** - * @expectedException InvalidArgumentException - */ public function testRequiresTheClientId() { + $this->expectException('InvalidArgumentException'); $o = new OAuth2([ 'authorizationUri' => 'https://accounts.test.org/auth/url', 'redirectUri' => 'https://accounts.test.org/redirect/url', @@ -50,11 +103,9 @@ public function testRequiresTheClientId() $o->buildFullAuthorizationUri(); } - /** - * @expectedException InvalidArgumentException - */ public function testRequiresTheRedirectUri() { + $this->expectException('InvalidArgumentException'); $o = new OAuth2([ 'authorizationUri' => 'https://accounts.test.org/auth/url', 'clientId' => 'aClientID', @@ -62,11 +113,9 @@ public function testRequiresTheRedirectUri() $o->buildFullAuthorizationUri(); } - /** - * @expectedException InvalidArgumentException - */ public function testCannotHavePromptAndApprovalPrompt() { + $this->expectException('InvalidArgumentException'); $o = new OAuth2([ 'authorizationUri' => 'https://accounts.test.org/auth/url', 'clientId' => 'aClientID', @@ -77,11 +126,9 @@ public function testCannotHavePromptAndApprovalPrompt() ]); } - /** - * @expectedException InvalidArgumentException - */ public function testCannotHaveInsecureAuthorizationUri() { + $this->expectException('InvalidArgumentException'); $o = new OAuth2([ 'authorizationUri' => 'http://accounts.test.org/insecure/url', 'redirectUri' => 'https://accounts.test.org/redirect/url', @@ -90,11 +137,9 @@ public function testCannotHaveInsecureAuthorizationUri() $o->buildFullAuthorizationUri(); } - /** - * @expectedException InvalidArgumentException - */ public function testCannotHaveRelativeRedirectUri() { + $this->expectException('InvalidArgumentException'); $o = new OAuth2([ 'authorizationUri' => 'http://accounts.test.org/insecure/url', 'redirectUri' => '/redirect/url', @@ -111,15 +156,6 @@ public function testHasDefaultXXXTypeParams() $this->assertEquals('offline', $q['access_type']); } - public function testCanBeUrlObject() - { - $config = array_merge($this->minimal, [ - 'authorizationUri' => Psr7\uri_for('https://another/uri'), - ]); - $o = new OAuth2($config); - $this->assertEquals('/uri', $o->buildFullAuthorizationUri()->getPath()); - } - public function testCanOverrideParams() { $overrides = [ @@ -168,15 +204,6 @@ public function testRedirectUriPostmessageIsAllowed() $this->assertArrayHasKey('redirect_uri', $query); $this->assertEquals('postmessage', $query['redirect_uri']); } -} - -class OAuth2GrantTypeTest extends TestCase -{ - private $minimal = [ - 'authorizationUri' => 'https://accounts.test.org/insecure/url', - 'redirectUri' => 'https://accounts.test.org/redirect/url', - 'clientId' => 'aClientID', - ]; public function testReturnsNullIfCannotBeInferred() { @@ -220,7 +247,13 @@ public function testInfersJwtBearer() public function testSetsKnownTypes() { $o = new OAuth2($this->minimal); - foreach (OAuth2::$knownGrantTypes as $t) { + + $reflection = new \ReflectionClass($o); + $property = $reflection->getProperty('knownGrantTypes'); + $property->setAccessible(true); + $knownGrantTypes = $property->getValue($o); + + foreach ($knownGrantTypes as $t) { $o->setGrantType($t); $this->assertEquals($t, $o->getGrantType()); } @@ -232,30 +265,23 @@ public function testSetsUrlAsGrantType() $o->setGrantType('http://a/grant/url'); $this->assertEquals('http://a/grant/url', $o->getGrantType()); } -} - -class OAuth2GetCacheKeyTest extends TestCase -{ - private $minimal = [ - 'clientID' => 'aClientID', - ]; public function testIsNullWithNoScopesOrAudience() { - $o = new OAuth2($this->minimal); + $o = new OAuth2($this->justClientId); $this->assertNull($o->getCacheKey()); } public function testIsScopeIfSingleScope() { - $o = new OAuth2($this->minimal); + $o = new OAuth2($this->justClientId); $o->setScope('test/scope/1'); $this->assertEquals('test/scope/1', $o->getCacheKey()); } public function testIsAllScopesWhenScopeIsArray() { - $o = new OAuth2($this->minimal); + $o = new OAuth2($this->justClientId); $o->setScope(['test/scope/1', 'test/scope/2']); $this->assertEquals('test/scope/1:test/scope/2', $o->getCacheKey()); } @@ -263,19 +289,10 @@ public function testIsAllScopesWhenScopeIsArray() public function testIsAudienceWhenScopeIsNull() { $aud = 'https://drive.googleapis.com'; - $o = new OAuth2($this->minimal); + $o = new OAuth2($this->justClientId); $o->setAudience($aud); $this->assertEquals($aud, $o->getCacheKey()); } -} - -class OAuth2TimingTest extends TestCase -{ - private $minimal = [ - 'authorizationUri' => 'https://accounts.test.org/insecure/url', - 'redirectUri' => 'https://accounts.test.org/redirect/url', - 'clientId' => 'aClientID', - ]; public function testIssuedAtDefaultsToNull() { @@ -315,10 +332,10 @@ public function testSettingExpiresInSetsExpireAt() $this->assertEquals($aShortWhile, $o->getExpiresAt() - $o->getIssuedAt()); } - public function testIsNotExpiredByDefault() + public function testIExpiredByDefault() { $o = new OAuth2($this->minimal); - $this->assertFalse($o->isExpired()); + $this->assertTrue($o->isExpired()); } public function testIsNotExpiredIfExpiresAtIsOld() @@ -327,21 +344,10 @@ public function testIsNotExpiredIfExpiresAtIsOld() $o->setExpiresAt(time() - 2); $this->assertTrue($o->isExpired()); } -} -class OAuth2GeneralTest extends TestCase -{ - private $minimal = [ - 'authorizationUri' => 'https://accounts.test.org/insecure/url', - 'redirectUri' => 'https://accounts.test.org/redirect/url', - 'clientId' => 'aClientID', - ]; - - /** - * @expectedException InvalidArgumentException - */ public function testFailsOnUnknownSigningAlgorithm() { + $this->expectException('InvalidArgumentException'); $o = new OAuth2($this->minimal); $o->setSigningAlgorithm('this is definitely not an algorithm name'); } @@ -349,17 +355,21 @@ public function testFailsOnUnknownSigningAlgorithm() public function testAllowsKnownSigningAlgorithms() { $o = new OAuth2($this->minimal); - foreach (OAuth2::$knownSigningAlgorithms as $a) { + + $reflection = new \ReflectionClass($o); + $property = $reflection->getProperty('knownSigningAlgorithms'); + $property->setAccessible(true); + $knownSigningAlgorithms = $property->getValue($o); + + foreach ($knownSigningAlgorithms as $a) { $o->setSigningAlgorithm($a); $this->assertEquals($a, $o->getSigningAlgorithm()); } } - /** - * @expectedException InvalidArgumentException - */ public function testFailsOnRelativeRedirectUri() { + $this->expectException('InvalidArgumentException'); $o = new OAuth2($this->minimal); $o->setRedirectUri('/relative/url'); } @@ -371,35 +381,19 @@ public function testAllowsUrnRedirectUri() $o->setRedirectUri($urn); $this->assertEquals($urn, $o->getRedirectUri()); } -} - -class OAuth2JwtTest extends TestCase -{ - private $signingMinimal = [ - 'signingKey' => 'example_key', - 'signingAlgorithm' => 'HS256', - 'scope' => 'https://www.googleapis.com/auth/userinfo.profile', - 'issuer' => 'app@example.com', - 'audience' => 'accounts.google.com', - 'clientId' => 'aClientID', - ]; - /** - * @expectedException DomainException - */ public function testFailsWithMissingAudience() { + $this->expectException('DomainException'); $testConfig = $this->signingMinimal; unset($testConfig['audience']); $o = new OAuth2($testConfig); $o->toJwt(); } - /** - * @expectedException DomainException - */ public function testFailsWithMissingIssuer() { + $this->expectException('DomainException'); $testConfig = $this->signingMinimal; unset($testConfig['issuer']); $o = new OAuth2($testConfig); @@ -413,25 +407,21 @@ public function testCanHaveNoScope() $testConfig = $this->signingMinimal; unset($testConfig['scope']); $o = new OAuth2($testConfig); - $o->toJwt(); + $this->assertNotNull($o->toJwt()); } - /** - * @expectedException DomainException - */ public function testFailsWithMissingSigningKey() { + $this->expectException('DomainException'); $testConfig = $this->signingMinimal; unset($testConfig['signingKey']); $o = new OAuth2($testConfig); $o->toJwt(); } - /** - * @expectedException DomainException - */ public function testFailsWithMissingSigningAlgorithm() { + $this->expectException('DomainException'); $testConfig = $this->signingMinimal; unset($testConfig['signingAlgorithm']); $o = new OAuth2($testConfig); @@ -441,42 +431,59 @@ public function testFailsWithMissingSigningAlgorithm() public function testCanHS256EncodeAValidPayloadWithSigningKeyId() { $testConfig = $this->signingMinimal; - $keys = array( + $keys = [ 'example_key_id1' => 'example_key1', 'example_key_id2' => 'example_key2' - ); + ]; $testConfig['signingKey'] = $keys['example_key_id2']; $testConfig['signingKeyId'] = 'example_key_id2'; $o = new OAuth2($testConfig); $payload = $o->toJwt(); - $roundTrip = $this->jwtDecode($payload, $keys, array('HS256')); - $this->assertEquals($roundTrip->iss, $testConfig['issuer']); - $this->assertEquals($roundTrip->aud, $testConfig['audience']); - $this->assertEquals($roundTrip->scope, $testConfig['scope']); + $result = $this->jwtDecode($payload, $keys, ['HS256']); + $this->assertEquals($result['iss'], $testConfig['issuer']); + $this->assertEquals($result['aud'], $testConfig['audience']); + $this->assertEquals($result['scope'], $testConfig['scope']); } - public function testFailDecodeWithoutSigningKeyId() + public function testFailDecodeWithIncorrectSigningKeyId() { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage( + '"kid" invalid, unable to lookup correct key' + ); + $testConfig = $this->signingMinimal; - $keys = array( + $keys = [ 'example_key_id1' => 'example_key1', 'example_key_id2' => 'example_key2' + ]; + + $testConfig['signingKey'] = $keys['example_key_id2']; + $o = new OAuth2($testConfig); + $payload = $o->toJwt(); + + $this->jwtDecode($payload, $keys, ['HS256']); + } + + public function testFailDecodeWithoutSigningKeyId() + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage( + '"kid" empty, unable to lookup correct key' ); + + $testConfig = $this->signingMinimal; + unset($testConfig['signingKeyId']); + + $keys = [ + 'example_key_id1' => 'example_key1', + 'example_key_id2' => 'example_key2' + ]; $testConfig['signingKey'] = $keys['example_key_id2']; $o = new OAuth2($testConfig); $payload = $o->toJwt(); - try { - $this->jwtDecode($payload, $keys, array('HS256')); - } catch (\Exception $e) { - if (($e instanceof \DomainException || $e instanceof \UnexpectedValueException) && - $e->getMessage() === '"kid" empty, unable to lookup correct key') { - // Workaround: In old JWT versions throws DomainException - return; - } - throw $e; - } - $this->fail("Expected exception about problem with decode"); + $this->jwtDecode($payload, $keys, ['HS256']); } public function testCanHS256EncodeAValidPayload() @@ -484,80 +491,63 @@ public function testCanHS256EncodeAValidPayload() $testConfig = $this->signingMinimal; $o = new OAuth2($testConfig); $payload = $o->toJwt(); - $roundTrip = $this->jwtDecode($payload, $testConfig['signingKey'], array('HS256')); - $this->assertEquals($roundTrip->iss, $testConfig['issuer']); - $this->assertEquals($roundTrip->aud, $testConfig['audience']); - $this->assertEquals($roundTrip->scope, $testConfig['scope']); + $result = $this->jwtDecode( + $payload, + ['keyid' => $testConfig['signingKey']], + ['HS256'] + ); + $this->assertEquals($result['iss'], $testConfig['issuer']); + $this->assertEquals($result['aud'], $testConfig['audience']); + $this->assertEquals($result['scope'], $testConfig['scope']); } public function testCanRS256EncodeAValidPayload() { - $publicKey = file_get_contents(__DIR__ . '/fixtures' . '/public.pem'); - $privateKey = file_get_contents(__DIR__ . '/fixtures' . '/private.pem'); $testConfig = $this->signingMinimal; $o = new OAuth2($testConfig); $o->setSigningAlgorithm('RS256'); - $o->setSigningKey($privateKey); + $o->setSigningKey($this->privateKey); + $o->setSigningKeyId('keyid'); $payload = $o->toJwt(); - $roundTrip = $this->jwtDecode($payload, $publicKey, array('RS256')); - $this->assertEquals($roundTrip->iss, $testConfig['issuer']); - $this->assertEquals($roundTrip->aud, $testConfig['audience']); - $this->assertEquals($roundTrip->scope, $testConfig['scope']); + $result = $this->jwtDecode( + $payload, + ['keyid' => $this->publicKey], + ['RS256'] + ); + $this->assertEquals($result['iss'], $testConfig['issuer']); + $this->assertEquals($result['aud'], $testConfig['audience']); + $this->assertEquals($result['scope'], $testConfig['scope']); } public function testCanHaveAdditionalClaims() { - $publicKey = file_get_contents(__DIR__ . '/fixtures' . '/public.pem'); - $privateKey = file_get_contents(__DIR__ . '/fixtures' . '/private.pem'); $testConfig = $this->signingMinimal; $targetAud = '123@456.com'; $testConfig['additionalClaims'] = ['target_audience' => $targetAud]; $o = new OAuth2($testConfig); $o->setSigningAlgorithm('RS256'); - $o->setSigningKey($privateKey); + $o->setSigningKey($this->privateKey); $payload = $o->toJwt(); - $roundTrip = $this->jwtDecode($payload, $publicKey, array('RS256')); - $this->assertEquals($roundTrip->target_audience, $targetAud); - } - - private function jwtDecode() - { - $args = func_get_args(); - $class = 'JWT'; - if (class_exists('Firebase\JWT\JWT')) { - $class = 'Firebase\JWT\JWT'; - } - - return call_user_func_array("$class::decode", $args); + $result = $this->jwtDecode( + $payload, + ['keyid' => $this->publicKey], + ['RS256'] + ); + $this->assertEquals($result['target_audience'], $targetAud); } -} - -class OAuth2GenerateAccessTokenRequestTest extends TestCase -{ - private $tokenRequestMinimal = [ - 'tokenCredentialUri' => 'https://tokens_r_us/test', - 'scope' => 'https://www.googleapis.com/auth/userinfo.profile', - 'issuer' => 'app@example.com', - 'audience' => 'accounts.google.com', - 'clientId' => 'aClientID', - ]; - /** - * @expectedException DomainException - */ public function testFailsIfNoTokenCredentialUri() { + $this->expectException('DomainException'); $testConfig = $this->tokenRequestMinimal; unset($testConfig['tokenCredentialUri']); $o = new OAuth2($testConfig); $o->generateCredentialsRequest(); } - /** - * @expectedException DomainException - */ public function testFailsIfAuthorizationCodeIsMissing() { + $this->expectException('DomainException'); $testConfig = $this->tokenRequestMinimal; $testConfig['redirectUri'] = 'https://has/redirect/uri'; $o = new OAuth2($testConfig); @@ -573,7 +563,7 @@ public function testGeneratesAuthorizationCodeRequests() // Generate the request and confirm that it's correct. $req = $o->generateCredentialsRequest(); - $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $req); + $this->assertInstanceOf(RequestInterface::class, $req); $this->assertEquals('POST', $req->getMethod()); $fields = Psr7\parse_query((string)$req->getBody()); $this->assertEquals('authorization_code', $fields['grant_type']); @@ -589,7 +579,7 @@ public function testGeneratesPasswordRequests() // Generate the request and confirm that it's correct. $req = $o->generateCredentialsRequest(); - $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $req); + $this->assertInstanceOf(RequestInterface::class, $req); $this->assertEquals('POST', $req->getMethod()); $fields = Psr7\parse_query((string)$req->getBody()); $this->assertEquals('password', $fields['grant_type']); @@ -605,7 +595,7 @@ public function testGeneratesRefreshTokenRequests() // Generate the request and confirm that it's correct. $req = $o->generateCredentialsRequest(); - $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $req); + $this->assertInstanceOf(RequestInterface::class, $req); $this->assertEquals('POST', $req->getMethod()); $fields = Psr7\parse_query((string)$req->getBody()); $this->assertEquals('refresh_token', $fields['grant_type']); @@ -656,7 +646,7 @@ public function testGeneratesAssertionRequests() // Generate the request and confirm that it's correct. $req = $o->generateCredentialsRequest(); - $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $req); + $this->assertInstanceOf(RequestInterface::class, $req); $this->assertEquals('POST', $req->getMethod()); $fields = Psr7\parse_query((string)$req->getBody()); $this->assertEquals(OAuth2::JWT_URN, $fields['grant_type']); @@ -672,76 +662,58 @@ public function testGeneratesExtendedRequests() // Generate the request and confirm that it's correct. $req = $o->generateCredentialsRequest(); - $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $req); + $this->assertInstanceOf(RequestInterface::class, $req); $this->assertEquals('POST', $req->getMethod()); $fields = Psr7\parse_query((string)$req->getBody()); $this->assertEquals('my_value', $fields['my_param']); $this->assertEquals('urn:my_test_grant_type', $fields['grant_type']); } -} -class OAuth2FetchAuthTokenTest extends TestCase -{ - private $fetchAuthTokenMinimal = [ - 'tokenCredentialUri' => 'https://tokens_r_us/test', - 'scope' => 'https://www.googleapis.com/auth/userinfo.profile', - 'signingKey' => 'example_key', - 'signingAlgorithm' => 'HS256', - 'issuer' => 'app@example.com', - 'audience' => 'accounts.google.com', - 'clientId' => 'aClientID', - ]; - - /** - * @expectedException GuzzleHttp\Exception\ClientException - */ public function testFailsOn400() { + $this->expectException(ClientException::class); $testConfig = $this->fetchAuthTokenMinimal; - $httpHandler = getHandler([ - buildResponse(400), + $httpClient = httpClientWithResponses([ + new Response(400), ]); - $o = new OAuth2($testConfig); - $o->fetchAuthToken($httpHandler); + $o = new OAuth2($testConfig + ['httpClient' => $httpClient]); + $o->fetchAuthToken(); } - /** - * @expectedException GuzzleHttp\Exception\ServerException - */ public function testFailsOn500() { + $this->expectException(ServerException::class); $testConfig = $this->fetchAuthTokenMinimal; - $httpHandler = getHandler([ - buildResponse(500), + $httpClient = httpClientWithResponses([ + new Response(500), ]); - $o = new OAuth2($testConfig); - $o->fetchAuthToken($httpHandler); + $o = new OAuth2($testConfig + ['httpClient' => $httpClient]); + $o->fetchAuthToken(); } - /** - * @expectedException Exception - * @expectedExceptionMessage Invalid JSON response - */ public function testFailsOnNoContentTypeIfResponseIsNotJSON() { + $this->expectException('Exception'); + $this->expectExceptionMessage('Invalid JSON response'); + $testConfig = $this->fetchAuthTokenMinimal; $notJson = '{"foo": , this is cannot be passed as json" "bar"}'; - $httpHandler = getHandler([ - buildResponse(200, [], Psr7\stream_for($notJson)), + $httpClient = httpClientWithResponses([ + new Response(200, [], $notJson), ]); - $o = new OAuth2($testConfig); - $o->fetchAuthToken($httpHandler); + $o = new OAuth2($testConfig + ['httpClient' => $httpClient]); + $o->fetchAuthToken(); } public function testFetchesJsonResponseOnNoContentTypeOK() { $testConfig = $this->fetchAuthTokenMinimal; $json = '{"foo": "bar"}'; - $httpHandler = getHandler([ - buildResponse(200, [], Psr7\stream_for($json)), + $httpClient = httpClientWithResponses([ + new Response(200, [], $json), ]); - $o = new OAuth2($testConfig); - $tokens = $o->fetchAuthToken($httpHandler); + $o = new OAuth2($testConfig + ['httpClient' => $httpClient]); + $tokens = $o->fetchAuthToken(); $this->assertEquals($tokens['foo'], 'bar'); } @@ -749,15 +721,15 @@ public function testFetchesFromFormEncodedResponseOK() { $testConfig = $this->fetchAuthTokenMinimal; $json = 'foo=bar&spice=nice'; - $httpHandler = getHandler([ - buildResponse( + $httpClient = httpClientWithResponses([ + new Response( 200, ['Content-Type' => 'application/x-www-form-urlencoded'], - Psr7\stream_for($json) + $json ), ]); - $o = new OAuth2($testConfig); - $tokens = $o->fetchAuthToken($httpHandler); + $o = new OAuth2($testConfig + ['httpClient' => $httpClient]); + $tokens = $o->fetchAuthToken(); $this->assertEquals($tokens['foo'], 'bar'); $this->assertEquals($tokens['spice'], 'nice'); } @@ -766,25 +738,25 @@ public function testUpdatesTokenFieldsOnFetch() { $testConfig = $this->fetchAuthTokenMinimal; $wanted_updates = [ - 'expires_at' => '1', - 'expires_in' => '57', - 'issued_at' => '2', + 'expires_at' => 1, + 'expires_in' => 57, + 'issued_at' => 2, 'access_token' => 'an_access_token', 'id_token' => 'an_id_token', 'refresh_token' => 'a_refresh_token', ]; $json = json_encode($wanted_updates); - $httpHandler = getHandler([ - buildResponse(200, [], Psr7\stream_for($json)), + $httpClient = httpClientWithResponses([ + new Response(200, [], $json), ]); - $o = new OAuth2($testConfig); + $o = new OAuth2($testConfig + ['httpClient' => $httpClient]); $this->assertNull($o->getExpiresAt()); $this->assertNull($o->getExpiresIn()); $this->assertNull($o->getIssuedAt()); $this->assertNull($o->getAccessToken()); $this->assertNull($o->getIdToken()); $this->assertNull($o->getRefreshToken()); - $tokens = $o->fetchAuthToken($httpHandler); + $tokens = $o->fetchAuthToken(); $this->assertEquals(1, $o->getExpiresAt()); $this->assertEquals(57, $o->getExpiresIn()); $this->assertEquals(2, $o->getIssuedAt()); @@ -796,26 +768,26 @@ public function testUpdatesTokenFieldsOnFetch() public function testUpdatesTokenFieldsOnFetchMissingRefreshToken() { $testConfig = $this->fetchAuthTokenMinimal; - $testConfig['refresh_token'] = 'a_refresh_token'; + $testConfig['refreshToken'] = 'a_refresh_token'; $wanted_updates = [ - 'expires_at' => '1', - 'expires_in' => '57', - 'issued_at' => '2', + 'expires_at' => 1, + 'expires_in' => 57, + 'issued_at' => 2, 'access_token' => 'an_access_token', 'id_token' => 'an_id_token', ]; $json = json_encode($wanted_updates); - $httpHandler = getHandler([ - buildResponse(200, [], Psr7\stream_for($json)), + $httpClient = httpClientWithResponses([ + new Response(200, [], $json), ]); - $o = new OAuth2($testConfig); + $o = new OAuth2($testConfig + ['httpClient' => $httpClient]); $this->assertNull($o->getExpiresAt()); $this->assertNull($o->getExpiresIn()); $this->assertNull($o->getIssuedAt()); $this->assertNull($o->getAccessToken()); $this->assertNull($o->getIdToken()); $this->assertEquals('a_refresh_token', $o->getRefreshToken()); - $tokens = $o->fetchAuthToken($httpHandler); + $tokens = $o->fetchAuthToken(); $this->assertEquals(1, $o->getExpiresAt()); $this->assertEquals(57, $o->getExpiresIn()); $this->assertEquals(2, $o->getIssuedAt()); @@ -824,173 +796,71 @@ public function testUpdatesTokenFieldsOnFetchMissingRefreshToken() $this->assertEquals('a_refresh_token', $o->getRefreshToken()); } - /** - * @dataProvider provideGetLastReceivedToken - */ - public function testGetLastReceivedToken( - $updateToken, - $expectedToken = null - ) { - $testConfig = $this->fetchAuthTokenMinimal; - $o = new OAuth2($testConfig); - $o->updateToken($updateToken); - $this->assertEquals( - $expectedToken ?: $updateToken, - $o->getLastReceivedToken() + public function testRevoke() + { + $testToken = 'testtoken'; + $httpClient = httpClientFromCallable( + function (RequestInterface $request) use ($testToken) { + $this->assertEquals( + 'no-store', + $request->getHeaderLine('Cache-Control') + ); + $this->assertEquals( + 'application/x-www-form-urlencoded', + $request->getHeaderLine('Content-Type') + ); + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals( + CredentialsInterface::TOKEN_REVOKE_URI, + (string) $request->getUri() + ); + $this->assertEquals( + 'token=' . $testToken, + (string) $request->getBody() + ); + + return new Response(200); + } ); - } - public function provideGetLastReceivedToken() - { - $time = time(); - return [ - [ - ['access_token' => 'abc'], - ['access_token' => 'abc', 'expires_at' => null], - ], - [ - ['access_token' => 'abc', 'invalid-field' => 'foo'], - ['access_token' => 'abc', 'expires_at' => null], - ], - [ - ['access_token' => 'abc', 'expires_at' => 1234567890], - ['access_token' => 'abc', 'expires_at' => 1234567890], - ], - [ - ['id_token' => 'def'], - ['id_token' => 'def', 'expires_at' => null], - ], - [ - ['id_token' => 'def', 'expires_at' => 1234567890], - ['id_token' => 'def', 'expires_at' => 1234567890], - ], - [ - [ - 'access_token' => 'abc', - 'expires_in' => 3600, - 'issued_at' => $time - ], - [ - 'access_token' => 'abc', - 'expires_at' => $time + 3600, - 'expires_in' => 3600, - 'issued_at' => $time - ], - ], - [ - ['access_token' => 'abc', 'issued_at' => 1234567890], - [ - 'access_token' => 'abc', - 'expires_at' => null, - 'issued_at' => 1234567890 - ], - ], - [ - ['access_token' => 'abc', 'refresh_token' => 'xyz'], - [ - 'access_token' => 'abc', - 'expires_at' => null, - 'refresh_token' => 'xyz' - ], - ], - ]; - } -} - -class OAuth2VerifyIdTokenTest extends TestCase -{ - private $publicKey; - private $privateKey; - private $verifyIdTokenMinimal = [ - 'scope' => 'https://www.googleapis.com/auth/userinfo.profile', - 'audience' => 'myaccount.on.host.issuer.com', - 'issuer' => 'an.issuer.com', - 'clientId' => 'myaccount.on.host.issuer.com', - ]; + $oauth = new OAuth2([ + 'httpClient' => $httpClient, + 'tokenRevokeUri' => CredentialsInterface::TOKEN_REVOKE_URI, + ]); - public function setUp() - { - $this->publicKey = - file_get_contents(__DIR__ . '/fixtures' . '/public.pem'); - $this->privateKey = - file_get_contents(__DIR__ . '/fixtures' . '/private.pem'); + $this->assertTrue($oauth->revoke($testToken)); } - /** - * @expectedException UnexpectedValueException - */ - public function testFailsIfIdTokenIsInvalid() + public function testRevokeFails() { - $testConfig = $this->verifyIdTokenMinimal; - $not_a_jwt = 'not a jot'; - $o = new OAuth2($testConfig); - $o->setIdToken($not_a_jwt); - $o->verifyIdToken($this->publicKey); - } + $this->expectException(ServerException::class); - /** - * @expectedException DomainException - */ - public function testFailsIfAudienceIsMissing() - { - $testConfig = $this->verifyIdTokenMinimal; - $now = time(); - $origIdToken = [ - 'issuer' => $testConfig['issuer'], - 'exp' => $now + 65, // arbitrary - 'iat' => $now, - ]; - $o = new OAuth2($testConfig); - $jwtIdToken = $this->jwtEncode($origIdToken, $this->privateKey, 'RS256'); - $o->setIdToken($jwtIdToken); - $o->verifyIdToken($this->publicKey, ['RS256']); - } + $httpClient = httpClientWithResponses([ + new Response(500), + ]); - /** - * @expectedException DomainException - */ - public function testFailsIfAudienceIsWrong() - { - $now = time(); - $testConfig = $this->verifyIdTokenMinimal; - $origIdToken = [ - 'aud' => 'a different audience', - 'iss' => $testConfig['issuer'], - 'exp' => $now + 65, // arbitrary - 'iat' => $now, - ]; - $o = new OAuth2($testConfig); - $jwtIdToken = $this->jwtEncode($origIdToken, $this->privateKey, 'RS256'); - $o->setIdToken($jwtIdToken); - $o->verifyIdToken($this->publicKey, ['RS256']); + $oauth = new OAuth2([ + 'httpClient' => $httpClient, + 'tokenRevokeUri' => CredentialsInterface::TOKEN_REVOKE_URI, + ]); + + $this->assertFalse($oauth->revoke('testtoken')); } - public function testShouldReturnAValidIdToken() + public function testRevokeFailsWithNoTokenRevokeUri() { - $testConfig = $this->verifyIdTokenMinimal; - $now = time(); - $origIdToken = [ - 'aud' => $testConfig['audience'], - 'iss' => $testConfig['issuer'], - 'exp' => $now + 65, // arbitrary - 'iat' => $now, - ]; - $o = new OAuth2($testConfig); - $alg = 'RS256'; - $jwtIdToken = $this->jwtEncode($origIdToken, $this->privateKey, $alg); - $o->setIdToken($jwtIdToken); - $roundTrip = $o->verifyIdToken($this->publicKey, array($alg)); - $this->assertEquals($origIdToken['aud'], $roundTrip->aud); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'requires an tokenRevokeUri to have been set' + ); + + (new OAuth2())->revoke('testtoken'); } - private function jwtEncode() + private function jwtDecode(string $jwt, array $keys, array $algs): array { - $args = func_get_args(); - $class = 'JWT'; - if (class_exists('Firebase\JWT\JWT')) { - $class = 'Firebase\JWT\JWT'; - } + $jwtClient = new FirebaseJwtClient(new JWT, new JWK); - return call_user_func_array("$class::encode", $args); + return $jwtClient->decode($jwt, $keys, $algs); } } diff --git a/tests/ServiceAccountSignerTraitTest.php b/tests/Auth/SignBlob/PrivateKeySignBlobTraitTest.php similarity index 56% rename from tests/ServiceAccountSignerTraitTest.php rename to tests/Auth/SignBlob/PrivateKeySignBlobTraitTest.php index 2e14a7191..76e938637 100644 --- a/tests/ServiceAccountSignerTraitTest.php +++ b/tests/Auth/SignBlob/PrivateKeySignBlobTraitTest.php @@ -15,14 +15,14 @@ * limitations under the License. */ -namespace Google\Auth\Tests; +namespace Google\Auth\SignBlob\Tests; -use Google\Auth\ServiceAccountSignerTrait; +use Google\Auth\SignBlob\PrivateKeySignBlobTrait; use PHPUnit\Framework\TestCase; -class ServiceAccountSignerTraitTest extends TestCase +class PrivateKeySignBlobTraitTest extends TestCase { - const STRING_TO_SIGN = 'hello world'; + private const STRING_TO_SIGN = 'hello world'; private $signedString = [ 'ZPeNGA9xcqwMQ7OEfNdLuwgxO+rJ59mhetIZrqWncY0uv+IZN0', @@ -30,45 +30,31 @@ class ServiceAccountSignerTraitTest extends TestCase 'UCyxkaPdB6eRczMXwJReu6q4LCJmx/Xr46kU/ZDNhrBkj6vjoD8yo=' ]; - /** - * @dataProvider useOpenSsl - */ - public function testSignBlob($useOpenSsl) + public function testSignBlob() { - $trait = new ServiceAccountSignerTraitImpl( - file_get_contents(__DIR__ . '/fixtures/private.pem') + $trait = new PrivateKeySignBlobTraitImpl( + file_get_contents(__DIR__ . '/../fixtures/private.pem') ); - $res = $trait->signBlob(self::STRING_TO_SIGN, $useOpenSsl); + $res = $trait->signBlob(self::STRING_TO_SIGN); $this->assertEquals(implode('', $this->signedString), $res); } - - public function useOpenSsl() - { - return [[true], [false]]; - } } -class ServiceAccountSignerTraitImpl +class PrivateKeySignBlobTraitImpl { - use ServiceAccountSignerTrait; + use PrivateKeySignBlobTrait; - private $auth; + private $signingKey; public function __construct($signingKey) { - $this->auth = new AuthStub; - $this->auth->signingKey = $signingKey; + $this->signingKey = $signingKey; } -} - -class AuthStub -{ - public $signingKey; - public function getSigningKey() + public function signBlob($stringToSign) { - return $this->signingKey; + return $this->signBlobWithPrivateKey($stringToSign, $this->signingKey); } } diff --git a/tests/IamTest.php b/tests/Auth/SignBlob/ServiceAccountApiSignBlobTraitTest.php similarity index 66% rename from tests/IamTest.php rename to tests/Auth/SignBlob/ServiceAccountApiSignBlobTraitTest.php index 611d2d2e2..dabc30d15 100644 --- a/tests/IamTest.php +++ b/tests/Auth/SignBlob/ServiceAccountApiSignBlobTraitTest.php @@ -15,16 +15,17 @@ * limitations under the License. */ -namespace Google\Auth\Tests; +namespace Google\Auth\SignBlob\Tests; -use Google\Auth\Iam; +use Google\Auth\SignBlob\ServiceAccountApiSignBlobTrait; +use Google\Http\ClientInterface; use GuzzleHttp\Psr7; use PHPUnit\Framework\TestCase; /** * @group iam */ -class IamTest extends TestCase +class ServiceAccountApiSignBlobTraitTest extends TestCase { /** * @dataProvider delegates @@ -35,9 +36,13 @@ public function testSignBlob(array $delegates = []) $expectedAccessToken = 'token'; $expectedString = 'toSign'; - $expectedServiceAccount = sprintf(Iam::SERVICE_ACCOUNT_NAME, $expectedEmail); - $expectedUri = Iam::IAM_API_ROOT . '/' . sprintf( - Iam::SIGN_BLOB_PATH, + $expectedServiceAccount = sprintf( + 'projects/-/serviceAccounts/%s', + $expectedEmail + ); + + $expectedUri = sprintf( + 'https://iamcredentials.googleapis.com/v1/%s:signBlob?alt=json', $expectedServiceAccount ); @@ -46,13 +51,13 @@ public function testSignBlob(array $delegates = []) if ($delegates) { $expectedDelegates = $delegates; foreach ($expectedDelegates as &$delegate) { - $delegate = sprintf(Iam::SERVICE_ACCOUNT_NAME, $delegate); + $delegate = sprintf('projects/-/serviceAccounts/%s', $delegate); } } else { $expectedDelegates[] = $expectedServiceAccount; } - $httpHandler = function (Psr7\Request $request) use ( + $httpClient = httpClientFromCallable(function (Psr7\Request $request) use ( $expectedEmail, $expectedAccessToken, $expectedString, @@ -71,13 +76,13 @@ public function testSignBlob(array $delegates = []) return new Psr7\Response(200, [], Psr7\stream_for(json_encode([ 'signedBlob' => $expectedResponse ]))); - }; + }); - $iam = new Iam($httpHandler); - $res = $iam->signBlob( + $trait = new ServiceAccountApiSignBlobTraitImpl($httpClient); + $res = $trait->signBlob( + $expectedString, $expectedEmail, $expectedAccessToken, - $expectedString, $delegates ); @@ -98,3 +103,30 @@ public function delegates() ]; } } + +class ServiceAccountApiSignBlobTraitImpl +{ + use ServiceAccountApiSignBlobTrait; + + private $httpClient; + + public function __construct(ClientInterface $httpClient) + { + $this->httpClient = $httpClient; + } + + public function signBlob( + $stringToSign, + $email, + $accessToken, + $delegates + ) { + return $this->signBlobWithServiceAccountApi( + $stringToSign, + $email, + $accessToken, + $this->httpClient, + $delegates + ); + } +} diff --git a/tests/fixtures2/private.json b/tests/Auth/fixtures/client_credentials.json similarity index 98% rename from tests/fixtures2/private.json rename to tests/Auth/fixtures/client_credentials.json index 20bb61793..05dcfca8e 100644 --- a/tests/fixtures2/private.json +++ b/tests/Auth/fixtures/client_credentials.json @@ -4,4 +4,4 @@ "refresh_token": "refreshToken123", "type": "authorized_user", "quota_project_id": "test_quota_project" -} +} \ No newline at end of file diff --git a/tests/fixtures/federated-certs.json b/tests/Auth/fixtures/federated-certs.json similarity index 100% rename from tests/fixtures/federated-certs.json rename to tests/Auth/fixtures/federated-certs.json diff --git a/tests/fixtures/.config/gcloud/application_default_credentials.json b/tests/Auth/fixtures/gcloud1/.config/gcloud/application_default_credentials.json similarity index 100% rename from tests/fixtures/.config/gcloud/application_default_credentials.json rename to tests/Auth/fixtures/gcloud1/.config/gcloud/application_default_credentials.json diff --git a/tests/fixtures2/.config/gcloud/application_default_credentials.json b/tests/Auth/fixtures/gcloud2/.config/gcloud/application_default_credentials.json similarity index 100% rename from tests/fixtures2/.config/gcloud/application_default_credentials.json rename to tests/Auth/fixtures/gcloud2/.config/gcloud/application_default_credentials.json diff --git a/tests/fixtures/private.json b/tests/Auth/fixtures/private.json similarity index 100% rename from tests/fixtures/private.json rename to tests/Auth/fixtures/private.json diff --git a/tests/fixtures/private.pem b/tests/Auth/fixtures/private.pem similarity index 100% rename from tests/fixtures/private.pem rename to tests/Auth/fixtures/private.pem diff --git a/tests/fixtures/public.pem b/tests/Auth/fixtures/public.pem similarity index 100% rename from tests/fixtures/public.pem rename to tests/Auth/fixtures/public.pem diff --git a/tests/BaseTest.php b/tests/BaseTest.php deleted file mode 100644 index 550c2cfee..000000000 --- a/tests/BaseTest.php +++ /dev/null @@ -1,58 +0,0 @@ -getGuzzleMajorVersion() !== 5) { - $this->markTestSkipped('Guzzle 5 only'); - } - } - - protected function onlyGuzzle6() - { - if ($this->getGuzzleMajorVersion() !== 6) { - $this->markTestSkipped('Guzzle 6 only'); - } - } - - protected function onlyGuzzle6And7() - { - if (!in_array($this->getGuzzleMajorVersion(), [6, 7])) { - $this->markTestSkipped('Guzzle 6 and 7 only'); - } - } - - protected function onlyGuzzle7() - { - if ($this->getGuzzleMajorVersion() !== 7) { - $this->markTestSkipped('Guzzle 7 only'); - } - } - - protected function getGuzzleMajorVersion() - { - if (defined('GuzzleHttp\ClientInterface::MAJOR_VERSION')) { - return ClientInterface::MAJOR_VERSION; - } - - if (defined('GuzzleHttp\ClientInterface::VERSION')) { - return (int) substr(ClientInterface::VERSION, 0, 1); - } - - $this->fail('Unable to determine the currently used Guzzle Version'); - } - - /** - * @see Google\Auth\$this->getValidKeyName - */ - public function getValidKeyName($key) - { - return preg_replace('|[^a-zA-Z0-9_\.! ]|', '', $key); - } -} diff --git a/tests/Cache/ItemTest.php b/tests/Cache/ItemTest.php index 9312d82ad..c26aa855e 100644 --- a/tests/Cache/ItemTest.php +++ b/tests/Cache/ItemTest.php @@ -15,9 +15,9 @@ * limitations under the License. */ -namespace Google\Auth\Tests\Cache; +namespace Google\Auth\Cache\Tests; -use Google\Auth\Cache\Item; +use Google\Cache\Item; use PHPUnit\Framework\TestCase; class ItemTest extends TestCase diff --git a/tests/Cache/MemoryCacheItemPoolTest.php b/tests/Cache/MemoryCacheItemPoolTest.php index b0942e861..5559a6c06 100644 --- a/tests/Cache/MemoryCacheItemPoolTest.php +++ b/tests/Cache/MemoryCacheItemPoolTest.php @@ -15,9 +15,9 @@ * limitations under the License. */ -namespace Google\Auth\Tests\Cache; +namespace Google\Auth\Cache\Tests; -use Google\Auth\Cache\MemoryCacheItemPool; +use Google\Cache\MemoryCacheItemPool; use PHPUnit\Framework\TestCase; use Psr\Cache\InvalidArgumentException; @@ -25,7 +25,7 @@ class MemoryCacheItemPoolTest extends TestCase { private $pool; - public function setUp() + public function setUp(): void { $this->pool = new MemoryCacheItemPool(); } @@ -43,7 +43,7 @@ public function testGetsFreshItem() { $item = $this->pool->getItem('item'); - $this->assertInstanceOf('Google\Auth\Cache\Item', $item); + $this->assertInstanceOf('Google\Cache\Item', $item); $this->assertNull($item->get()); $this->assertFalse($item->isHit()); } @@ -55,7 +55,7 @@ public function testGetsExistingItem() $this->saveItem($key, $value); $item = $this->pool->getItem($key); - $this->assertInstanceOf('Google\Auth\Cache\Item', $item); + $this->assertInstanceOf('Google\Cache\Item', $item); $this->assertEquals($value, $item->get()); $this->assertTrue($item->isHit()); } @@ -66,7 +66,7 @@ public function testGetsMultipleItems() $items = $this->pool->getItems($keys); $this->assertEquals($keys, array_keys($items)); - $this->assertContainsOnlyInstancesOf('Google\Auth\Cache\Item', $items); + $this->assertContainsOnlyInstancesOf('Google\Cache\Item', $items); } public function testHasItem() @@ -157,47 +157,47 @@ public function testCommitsDeferredItems() } /** - * @expectedException \Psr\Cache\InvalidArgumentException * @dataProvider invalidKeys */ public function testCheckInvalidKeysOnGetItem($key) { + $this->expectException(InvalidArgumentException::class); $this->pool->getItem($key); } /** - * @expectedException \Psr\Cache\InvalidArgumentException * @dataProvider invalidKeys */ public function testCheckInvalidKeysOnGetItems($key) { + $this->expectException(InvalidArgumentException::class); $this->pool->getItems([$key]); } /** - * @expectedException \Psr\Cache\InvalidArgumentException * @dataProvider invalidKeys */ public function testCheckInvalidKeysOnHasItem($key) { + $this->expectException(InvalidArgumentException::class); $this->pool->hasItem($key); } /** - * @expectedException \Psr\Cache\InvalidArgumentException * @dataProvider invalidKeys */ public function testCheckInvalidKeysOnDeleteItem($key) { + $this->expectException(InvalidArgumentException::class); $this->pool->deleteItem($key); } /** - * @expectedException \Psr\Cache\InvalidArgumentException * @dataProvider invalidKeys */ public function testCheckInvalidKeysOnDeleteItems($key) { + $this->expectException(InvalidArgumentException::class); $this->pool->deleteItems([$key]); } diff --git a/tests/Cache/SysVCacheItemPoolTest.php b/tests/Cache/SysVCacheItemPoolTest.php index 8fa056270..9f05d0357 100644 --- a/tests/Cache/SysVCacheItemPoolTest.php +++ b/tests/Cache/SysVCacheItemPoolTest.php @@ -15,16 +15,16 @@ * limitations under the License. */ -namespace Google\Auth\Tests\Cache; +namespace Google\Auth\Cache\Tests; -use Google\Auth\Cache\SysVCacheItemPool; +use Google\Cache\SysVCacheItemPool; use PHPUnit\Framework\TestCase; class SysVCacheItemPoolTest extends TestCase { private $pool; - public function setUp() + public function setUp(): void { if (! extension_loaded('sysvshm')) { $this->markTestSkipped( @@ -48,7 +48,7 @@ public function testGetsFreshItem() { $item = $this->pool->getItem('item'); - $this->assertInstanceOf('Google\Auth\Cache\Item', $item); + $this->assertInstanceOf('Google\Cache\Item', $item); $this->assertNull($item->get()); $this->assertFalse($item->isHit()); } @@ -70,7 +70,7 @@ public function testGetsExistingItem() $this->saveItem($key, $value); $item = $this->pool->getItem($key); - $this->assertInstanceOf('Google\Auth\Cache\Item', $item); + $this->assertInstanceOf('Google\Cache\Item', $item); $this->assertEquals($value, $item->get()); $this->assertTrue($item->isHit()); } @@ -81,7 +81,7 @@ public function testGetsMultipleItems() $items = $this->pool->getItems($keys); $this->assertEquals($keys, array_keys($items)); - $this->assertContainsOnlyInstancesOf('Google\Auth\Cache\Item', $items); + $this->assertContainsOnlyInstancesOf('Google\Cache\Item', $items); } public function testHasItem() diff --git a/tests/Cache/sysv_cache_creator.php b/tests/Cache/sysv_cache_creator.php index 618097d23..7996bce3b 100644 --- a/tests/Cache/sysv_cache_creator.php +++ b/tests/Cache/sysv_cache_creator.php @@ -15,12 +15,12 @@ * limitations under the License. */ -namespace Google\Auth\Tests\Cache; +namespace Google\Auth\Cache\Tests; require_once __DIR__ . '/../../vendor/autoload.php'; -use Google\Auth\Cache\Item; -use Google\Auth\Cache\SysVCacheItemPool; +use Google\Cache\Item; +use Google\Cache\SysVCacheItemPool; $value = $argv[1]; // Use the same variableKey in the test. diff --git a/tests/CacheTraitTest.php b/tests/CacheTraitTest.php deleted file mode 100644 index 583beea98..000000000 --- a/tests/CacheTraitTest.php +++ /dev/null @@ -1,194 +0,0 @@ -mockFetcher = $this->prophesize('Google\Auth\FetchAuthTokenInterface'); - $this->mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $this->mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - } - - public function testSuccessfullyPullsFromCache() - { - $expectedValue = '1234'; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($expectedValue); - $this->mockCache->getItem(Argument::type('string')) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - - $implementation = new CacheTraitImplementation([ - 'cache' => $this->mockCache->reveal(), - ]); - - $cachedValue = $implementation->gCachedValue(); - $this->assertEquals($expectedValue, $cachedValue); - } - - public function testSuccessfullyPullsFromCacheWithInvalidKey() - { - $key = 'this-key-has-@-illegal-characters'; - $expectedKey = 'thiskeyhasillegalcharacters'; - $expectedValue = '1234'; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($expectedValue); - $this->mockCache->getItem($expectedKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - - $implementation = new CacheTraitImplementation([ - 'cache' => $this->mockCache->reveal(), - 'key' => $key, - ]); - - $cachedValue = $implementation->gCachedValue(); - $this->assertEquals($expectedValue, $cachedValue); - } - - public function testSuccessfullyPullsFromCacheWithLongKey() - { - $key = 'this-key-is-over-64-characters-and-it-will-still-work' - . '-but-it-will-be-hashed-and-shortened'; - $expectedKey = str_replace('-', '', $key); - $expectedKey = substr(hash('sha256', $expectedKey), 0, 64); - $expectedValue = '1234'; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($expectedValue); - $this->mockCache->getItem($expectedKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - - $implementation = new CacheTraitImplementation([ - 'cache' => $this->mockCache->reveal(), - 'key' => $key - ]); - - $cachedValue = $implementation->gCachedValue(); - $this->assertEquals($expectedValue, $cachedValue); - } - - public function testFailsPullFromCacheWithNoCache() - { - $implementation = new CacheTraitImplementation(); - - $cachedValue = $implementation->gCachedValue(); - $this->assertEquals(null, $cachedValue); - } - - public function testFailsPullFromCacheWithoutKey() - { - $implementation = new CacheTraitImplementation([ - 'cache' => $this->mockCache->reveal(), - 'key' => null, - ]); - - $cachedValue = $implementation->gCachedValue(); - } - - public function testSuccessfullySetsToCache() - { - $value = '1234'; - $this->mockCacheItem->set($value) - ->shouldBeCalled(); - $this->mockCacheItem->expiresAfter(Argument::any()) - ->shouldBeCalled(); - $this->mockCache->getItem('key') - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->shouldBeCalled(); - - $implementation = new CacheTraitImplementation([ - 'cache' => $this->mockCache->reveal(), - ]); - - $implementation->sCachedValue($value); - } - - public function testFailsSetToCacheWithNoCache() - { - $implementation = new CacheTraitImplementation(); - - $implementation->sCachedValue('1234'); - - $cachedValue = $implementation->sCachedValue('1234'); - $this->assertNull($cachedValue); - } - - public function testFailsSetToCacheWithoutKey() - { - $implementation = new CacheTraitImplementation([ - 'cache' => $this->mockCache, - 'key' => null, - ]); - - $cachedValue = $implementation->sCachedValue('1234'); - $this->assertNull($cachedValue); - } -} - -class CacheTraitImplementation -{ - use CacheTrait; - - private $cache; - private $cacheConfig; - - public function __construct(array $config = []) - { - $this->key = array_key_exists('key', $config) ? $config['key'] : 'key'; - $this->cache = isset($config['cache']) ? $config['cache'] : null; - $this->cacheConfig = [ - 'prefix' => '', - 'lifetime' => 1000, - ]; - } - - // allows us to keep trait methods private - public function gCachedValue() - { - return $this->getCachedValue($this->key); - } - - public function sCachedValue($v) - { - $this->setCachedValue($this->key, $v); - } -} diff --git a/tests/Credentials/AppIdentityCredentialsTest.php b/tests/Credentials/AppIdentityCredentialsTest.php deleted file mode 100644 index 37cce7145..000000000 --- a/tests/Credentials/AppIdentityCredentialsTest.php +++ /dev/null @@ -1,246 +0,0 @@ -assertFalse(AppIdentityCredentials::onAppEngine()); - } - - /** - * @runInSeparateProcess - */ - public function testOnAppEngineIsTrueWhenServerSoftwareIsGoogleAppEngine() - { - $this->imitateInAppEngine(); - $this->assertTrue(AppIdentityCredentials::onAppEngine()); - } - - /** - * @runInSeparateProcess - */ - public function testOnAppEngineIsTrueWhenAppEngineRuntimeIsPhp() - { - $this->imitateInAppEngine(); - $this->assertTrue(AppIdentityCredentials::onAppEngine()); - } - - /** - * @runInSeparateProcess - */ - public function testOnAppEngineIsTrueInDevelopmentServer() - { - $_SERVER['APPENGINE_RUNTIME'] = 'php'; - $this->assertTrue(AppIdentityCredentials::onAppEngine()); - } - - public function testGetCacheKeyShouldBeEmpty() - { - $g = new AppIdentityCredentials(); - $this->assertEmpty($g->getCacheKey()); - } - - public function testFetchAuthTokenShouldBeEmptyIfNotOnAppEngine() - { - $g = new AppIdentityCredentials(); - $this->assertEquals(array(), $g->fetchAuthToken()); - } - - /* @expectedException */ - public function testThrowsExceptionIfClassDoesntExist() - { - $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; - $g = new AppIdentityCredentials(); - } - - /** - * @runInSeparateProcess - */ - public function testFetchAuthTokenReturnsExpectedToken() - { - $this->imitateInAppEngine(); - - $wantedToken = [ - 'access_token' => '1/abdef1234567890', - 'expires_in' => '57', - 'token_type' => 'Bearer', - ]; - - AppIdentityService::$accessToken = $wantedToken; - - $g = new AppIdentityCredentials(); - $this->assertEquals($wantedToken, $g->fetchAuthToken()); - } - - /** - * @runInSeparateProcess - */ - public function testScopeIsAlwaysArray() - { - $this->imitateInAppEngine(); - - $scope1 = ['scopeA', 'scopeB']; - $scope2 = 'scopeA scopeB'; - $scope3 = 'scopeA'; - - $g = new AppIdentityCredentials($scope1); - $g->fetchAuthToken(); - $this->assertEquals($scope1, AppIdentityService::$scope); - - $g = new AppIdentityCredentials($scope2); - $g->fetchAuthToken(); - $this->assertEquals(explode(' ', $scope2), AppIdentityService::$scope); - - $g = new AppIdentityCredentials($scope3); - $g->fetchAuthToken(); - $this->assertEquals([$scope3], AppIdentityService::$scope); - } - - /** - * @dataProvider appEngineRequired - */ - public function testMethodsFailWhenNotInAppEngine($method, $args = [], $expected = null) - { - if ($expected === null) { - if (method_exists($this, 'expectException')) { - $this->expectException('\Exception'); - } else { - $this->setExpectedException('\Exception'); - } - } - - $creds = new AppIdentityCredentials; - $res = call_user_func_array([$creds, $method], $args); - - if ($expected) { - $this->assertEquals($expected, $res); - } - } - - public function appEngineRequired() - { - return [ - ['fetchAuthToken', [], []], - ['signBlob', ['foo']], - ['getClientName'] - ]; - } - - /** - * @runInSeparateProcess - */ - public function testSignBlob() - { - $this->imitateInAppEngine(); - - $creds = new AppIdentityCredentials; - $string = 'test'; - $res = $creds->signBlob($string); - - $this->assertEquals(base64_encode('Signed: ' . $string), $res); - } - - /** - * @runInSeparateProcess - */ - public function testGetClientName() - { - $this->imitateInAppEngine(); - - $creds = new AppIdentityCredentials; - - $expected = 'foobar'; - AppIdentityService::$serviceAccountName = $expected; - - $this->assertEquals($expected, $creds->getClientName()); - - AppIdentityService::$serviceAccountName = 'notreturned'; - $this->assertEquals($expected, $creds->getClientName()); - } - - public function testGetLastReceivedTokenNullByDefault() - { - $creds = new AppIdentityCredentials; - $this->assertNull($creds->getLastReceivedToken()); - } - - /** - * @runInSeparateProcess - */ - public function testGetLastReceviedTokenCaches() - { - $this->imitateInAppEngine(); - - $creds = new AppIdentityCredentials; - - $wantedToken = [ - 'access_token' => '1/abdef1234567890', - 'expires_in' => '57', - 'expiration_time' => time() + 57, - 'token_type' => 'Bearer', - ]; - - AppIdentityService::$accessToken = $wantedToken; - - $creds->fetchAuthToken(); - - $this->assertEquals([ - 'access_token' => $wantedToken['access_token'], - 'expires_at' => $wantedToken['expiration_time'] - ], $creds->getLastReceivedToken()); - } - - /** - * @runInSeparateProcess - */ - public function testGetProjectId() - { - $this->imitateInAppEngine(); - - $projectId = 'foobar'; - AppIdentityService::$applicationId = $projectId; - $this->assertEquals($projectId, (new AppIdentityCredentials)->getProjectId()); - } - - public function testGetProjectOutsideAppEngine() - { - $this->assertNull((new AppIdentityCredentials)->getProjectId()); - } - - private function imitateInAppEngine() - { - // include the mock AppIdentityService class - require_once __DIR__ . '/../mocks/AppIdentityService.php'; - $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; - // $_SERVER['APPENGINE_RUNTIME'] = 'php'; - } -} diff --git a/tests/Credentials/GCECredentialsTest.php b/tests/Credentials/GCECredentialsTest.php deleted file mode 100644 index 79775b651..000000000 --- a/tests/Credentials/GCECredentialsTest.php +++ /dev/null @@ -1,469 +0,0 @@ -getHeaderLine(GCECredentials::FLAVOR_HEADER) === 'Google'; - - return new Psr7\Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']); - }; - - $onGce = GCECredentials::onGce($dummyHandler); - $this->assertTrue($hasHeader); - $this->assertTrue($onGce); - } - - public function testOnGCEIsFalseOnClientErrorStatus() - { - // simulate retry attempts by returning multiple 400s - $httpHandler = getHandler([ - buildResponse(400), - buildResponse(400), - buildResponse(400) - ]); - $this->assertFalse(GCECredentials::onGCE($httpHandler)); - } - - public function testOnGCEIsFalseOnServerErrorStatus() - { - // simulate retry attempts by returning multiple 500s - $httpHandler = getHandler([ - buildResponse(500), - buildResponse(500), - buildResponse(500) - ]); - $this->assertFalse(GCECredentials::onGCE($httpHandler)); - } - - public function testOnGCEIsFalseOnOkStatusWithoutExpectedHeader() - { - $httpHandler = getHandler([ - buildResponse(200), - ]); - $this->assertFalse(GCECredentials::onGCE($httpHandler)); - } - - public function testOnGCEIsOkIfGoogleIsTheFlavor() - { - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - ]); - $this->assertTrue(GCECredentials::onGCE($httpHandler)); - } - - public function testOnAppEngineFlexIsFalseByDefault() - { - $this->assertFalse(GCECredentials::onAppEngineFlexible()); - } - - public function testOnAppEngineFlexIsTrueWhenGaeInstanceHasAefPrefix() - { - putenv('GAE_INSTANCE=aef-default-20180313t154438'); - $this->assertTrue(GCECredentials::onAppEngineFlexible()); - putenv('GAE_INSTANCE'); - } - - public function testGetCacheKeyShouldNotBeEmpty() - { - $g = new GCECredentials(); - $this->assertNotEmpty($g->getCacheKey()); - } - - public function testFetchAuthTokenShouldBeEmptyIfNotOnGCE() - { - // simulate retry attempts by returning multiple 500s - $httpHandler = getHandler([ - buildResponse(500), - buildResponse(500), - buildResponse(500) - ]); - $g = new GCECredentials(); - $this->assertEquals(array(), $g->fetchAuthToken($httpHandler)); - } - - /** - * @expectedException Exception - * @expectedExceptionMessage Invalid JSON response - */ - public function testFetchAuthTokenShouldFailIfResponseIsNotJson() - { - $notJson = '{"foo": , this is cannot be passed as json" "bar"}'; - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], $notJson), - ]); - $g = new GCECredentials(); - $g->fetchAuthToken($httpHandler); - } - - public function testFetchAuthTokenShouldReturnTokenInfo() - { - $wantedTokens = [ - 'access_token' => '1/abdef1234567890', - 'expires_in' => '57', - 'token_type' => 'Bearer', - ]; - $jsonTokens = json_encode($wantedTokens); - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for($jsonTokens)), - ]); - $g = new GCECredentials(); - $receivedToken = $g->fetchAuthToken($httpHandler); - $this->assertEquals( - $wantedTokens['access_token'], - $receivedToken['access_token'] - ); - $this->assertEquals(time() + 57, $receivedToken['expires_at']); - $this->assertEquals(time() + 57, $g->getLastReceivedToken()['expires_at']); - } - - public function testFetchAuthTokenShouldBeIdTokenWhenTargetAudienceIsSet() - { - $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( - '/computeMetadata/' . GCECredentials::ID_TOKEN_URI_PATH, - $request->getUri()->getPath() - ); - $this->assertEquals( - 'audience=a+target+audience', - $request->getUri()->getQuery() - ); - return new Psr7\Response(200, [], Psr7\stream_for($expectedToken['id_token'])); - }; - $g = new GCECredentials(null, null, 'a+target+audience'); - $this->assertEquals($expectedToken, $g->fetchAuthToken($httpHandler)); - $this->assertEquals(2, $timesCalled); - } - - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Scope and targetAudience cannot both be supplied - */ - public function testSettingBothScopeAndTargetAudienceThrowsException() - { - $g = new GCECredentials(null, 'a-scope', 'a+target+audience'); - } - - /** - * @dataProvider scopes - */ - public function testFetchAuthTokenCustomScope($scope, $expected) - { - $this->onlyGuzzle6And7(); - - $uri = null; - $client = $this->prophesize('GuzzleHttp\ClientInterface'); - $client->send(Argument::any(), Argument::any()) - ->will(function () use (&$uri) { - $this->send(Argument::any(), Argument::any())->will(function ($args) use (&$uri) { - $uri = $args[0]->getUri(); - - return buildResponse(200, [], Psr7\stream_for('{"expires_in": 0}')); - }); - - return buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']); - }); - - HttpClientCache::setHttpClient($client->reveal()); - - $g = new GCECredentials(null, $scope); - $g->fetchAuthToken(); - parse_str($uri->getQuery(), $query); - - $this->assertArrayHasKey('scopes', $query); - $this->assertEquals($expected, $query['scopes']); - } - - public function scopes() - { - return [ - ['foobar', 'foobar'], - [['foobar'], 'foobar'], - ['hello world', 'hello,world'], - [['hello', 'world'], 'hello,world'] - ]; - } - - public function testGetLastReceivedTokenIsNullByDefault() - { - $creds = new GCECredentials; - $this->assertNull($creds->getLastReceivedToken()); - } - - public function testGetClientName() - { - $expected = 'foobar'; - - $httpHandler = getHandler([ - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for($expected)), - buildResponse(200, [], Psr7\stream_for('notexpected')) - ]); - - $creds = new GCECredentials; - $this->assertEquals($expected, $creds->getClientName($httpHandler)); - - // call again to test cached value - $this->assertEquals($expected, $creds->getClientName($httpHandler)); - } - - public function testGetClientNameShouldBeEmptyIfNotOnGCE() - { - // simulate retry attempts by returning multiple 500s - $httpHandler = getHandler([ - buildResponse(500), - buildResponse(500), - buildResponse(500) - ]); - - $creds = new GCECredentials; - $this->assertEquals('', $creds->getClientName($httpHandler)); - } - - public function testSignBlob() - { - $this->onlyGuzzle6And7(); - - $expectedEmail = 'test@test.com'; - $expectedAccessToken = 'token'; - $stringToSign = 'inputString'; - $resultString = 'foobar'; - $token = [ - 'access_token' => $expectedAccessToken, - 'expires_in' => '57', - 'token_type' => 'Bearer', - ]; - - $iam = $this->prophesize('Google\Auth\Iam'); - $iam->signBlob($expectedEmail, $expectedAccessToken, $stringToSign) - ->shouldBeCalled() - ->willReturn($resultString); - - $client = $this->prophesize('GuzzleHttp\ClientInterface'); - $client->send(Argument::any(), Argument::any()) - ->willReturn( - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for($expectedEmail)), - buildResponse(200, [], Psr7\stream_for(json_encode($token))) - ); - - HttpClientCache::setHttpClient($client->reveal()); - - $creds = new GCECredentials($iam->reveal()); - $signature = $creds->signBlob($stringToSign); - } - - public function testSignBlobWithLastReceivedAccessToken() - { - $this->onlyGuzzle6And7(); - - $expectedEmail = 'test@test.com'; - $expectedAccessToken = 'token'; - $notExpectedAccessToken = 'othertoken'; - $stringToSign = 'inputString'; - $resultString = 'foobar'; - $token1 = [ - 'access_token' => $expectedAccessToken, - 'expires_in' => '57', - 'token_type' => 'Bearer', - ]; - $token2 = [ - 'access_token' => $notExpectedAccessToken, - 'expires_in' => '57', - 'token_type' => 'Bearer', - ]; - - $iam = $this->prophesize('Google\Auth\Iam'); - $iam->signBlob($expectedEmail, $expectedAccessToken, $stringToSign) - ->shouldBeCalled() - ->willReturn($resultString); - - $client = $this->prophesize('GuzzleHttp\ClientInterface'); - $client->send(Argument::any(), Argument::any()) - ->willReturn( - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for(json_encode($token1))), - buildResponse(200, [], Psr7\stream_for($expectedEmail)), - buildResponse(200, [], Psr7\stream_for(json_encode($token2))) - ); - - HttpClientCache::setHttpClient($client->reveal()); - - $creds = new GCECredentials($iam->reveal()); - // cache a token - $creds->fetchAuthToken(); - - $signature = $creds->signBlob($stringToSign); - } - - public function testGetProjectId() - { - $this->onlyGuzzle6And7(); - - $expected = 'foobar'; - - $client = $this->prophesize('GuzzleHttp\ClientInterface'); - $client->send(Argument::any(), Argument::any()) - ->willReturn( - buildResponse(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - buildResponse(200, [], Psr7\stream_for($expected)), - buildResponse(200, [], Psr7\stream_for('notexpected')) - ); - - HttpClientCache::setHttpClient($client->reveal()); - - $creds = new GCECredentials; - $this->assertEquals($expected, $creds->getProjectId()); - - // call again to test cached value - $this->assertEquals($expected, $creds->getProjectId()); - } - - public function testGetProjectIdShouldBeEmptyIfNotOnGCE() - { - $this->onlyGuzzle6And7(); - - // simulate retry attempts by returning multiple 500s - $client = $this->prophesize('GuzzleHttp\ClientInterface'); - $client->send(Argument::any(), Argument::any()) - ->willReturn( - buildResponse(500), - buildResponse(500), - buildResponse(500) - ); - - HttpClientCache::setHttpClient($client->reveal()); - - $creds = new GCECredentials; - $this->assertNull($creds->getProjectId()); - } - - public function testGetTokenUriWithServiceAccountIdentity() - { - $tokenUri = GCECredentials::getTokenUri('foo'); - $this->assertEquals( - 'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/foo/token', - $tokenUri - ); - } - - public function testGetAccessTokenWithServiceAccountIdentity() - { - $expected = [ - 'access_token' => 'token12345', - 'expires_in' => 123, - ]; - $timesCalled = 0; - $httpHandler = function ($request) use (&$timesCalled, $expected) { - $timesCalled++; - if ($timesCalled == 1) { - return new Psr7\Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']); - } - $this->assertEquals( - '/computeMetadata/v1/instance/service-accounts/foo/token', - $request->getUri()->getPath() - ); - $this->assertEquals('', $request->getUri()->getQuery()); - return new Psr7\Response(200, [], Psr7\stream_for(json_encode($expected))); - }; - - $g = new GCECredentials(null, null, null, null, 'foo'); - $this->assertEquals( - $expected['access_token'], - $g->fetchAuthToken($httpHandler)['access_token'] - ); - } - - public function testGetIdTokenWithServiceAccountIdentity() - { - $expected = 'idtoken12345'; - $timesCalled = 0; - $httpHandler = function ($request) use (&$timesCalled, $expected) { - $timesCalled++; - if ($timesCalled == 1) { - return new Psr7\Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']); - } - $this->assertEquals( - '/computeMetadata/v1/instance/service-accounts/foo/identity', - $request->getUri()->getPath() - ); - $this->assertEquals( - 'audience=a+target+audience', - $request->getUri()->getQuery() - ); - return new Psr7\Response(200, [], Psr7\stream_for($expected)); - }; - $g = new GCECredentials(null, null, 'a+target+audience', null, 'foo'); - $this->assertEquals( - ['id_token' => $expected], - $g->fetchAuthToken($httpHandler) - ); - } - - public function testGetClientNameUriWithServiceAccountIdentity() - { - $clientNameUri = GCECredentials::getClientNameUri('foo'); - $this->assertEquals( - 'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/foo/email', - $clientNameUri - ); - } - - public function testGetClientNameWithServiceAccountIdentity() - { - $expected = 'expected'; - $timesCalled = 0; - $httpHandler = function ($request) use (&$timesCalled, $expected) { - $timesCalled++; - if ($timesCalled == 1) { - return new Psr7\Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']); - } - $this->assertEquals( - '/computeMetadata/v1/instance/service-accounts/foo/email', - $request->getUri()->getPath() - ); - $this->assertEquals('', $request->getUri()->getQuery()); - return new Psr7\Response(200, [], Psr7\stream_for($expected)); - }; - - $creds = new GCECredentials(null, null, null, null, 'foo'); - $this->assertEquals($expected, $creds->getClientName($httpHandler)); - } -} diff --git a/tests/Credentials/IAMCredentialsTest.php b/tests/Credentials/IAMCredentialsTest.php deleted file mode 100644 index 12c05cb0a..000000000 --- a/tests/Credentials/IAMCredentialsTest.php +++ /dev/null @@ -1,90 +0,0 @@ -assertNotNull( - new IAMCredentials('iam-selector', 'iam-token') - ); - } -} - -class IAMUpdateMetadataCallbackTest extends TestCase -{ - public function testUpdateMetadataFunc() - { - $selector = 'iam-selector'; - $token = 'iam-token'; - $iam = new IAMCredentials( - $selector, - $token - ); - - $update_metadata = $iam->getUpdateMetadataFunc(); - $this->assertInternalType('callable', $update_metadata); - - $actual_metadata = call_user_func( - $update_metadata, - $metadata = array('foo' => 'bar') - ); - $this->assertArrayHasKey(IAMCredentials::SELECTOR_KEY, $actual_metadata); - $this->assertEquals( - $actual_metadata[IAMCredentials::SELECTOR_KEY], - $selector - ); - $this->assertArrayHasKey(IAMCredentials::TOKEN_KEY, $actual_metadata); - $this->assertEquals( - $actual_metadata[IAMCredentials::TOKEN_KEY], - $token - ); - } -} diff --git a/tests/Credentials/InsecureCredentialsTest.php b/tests/Credentials/InsecureCredentialsTest.php deleted file mode 100644 index 62279096f..000000000 --- a/tests/Credentials/InsecureCredentialsTest.php +++ /dev/null @@ -1,46 +0,0 @@ -assertEquals(['access_token' => ''], $insecure->fetchAuthToken()); - } - - public function testGetCacheKey() - { - $insecure = new InsecureCredentials(); - $this->assertNull($insecure->getCacheKey()); - } - - public function testGetLastReceivedToken() - { - $insecure = new InsecureCredentials(); - $this->assertEquals(['access_token' => ''], $insecure->getLastReceivedToken()); - } -} diff --git a/tests/Credentials/ServiceAccountCredentialsTest.php b/tests/Credentials/ServiceAccountCredentialsTest.php deleted file mode 100644 index 85c2ded78..000000000 --- a/tests/Credentials/ServiceAccountCredentialsTest.php +++ /dev/null @@ -1,770 +0,0 @@ - 'key123', - 'private_key' => 'privatekey', - 'client_email' => 'test@example.com', - 'client_id' => 'client123', - 'type' => 'service_account', - 'project_id' => 'example_project' - ]; -} - -class SACGetCacheKeyTest extends TestCase -{ - public function testShouldBeTheSameAsOAuth2WithTheSameScope() - { - $testJson = createTestJson(); - $scope = ['scope/1', 'scope/2']; - $sa = new ServiceAccountCredentials( - $scope, - $testJson - ); - $o = new OAuth2(['scope' => $scope]); - $this->assertSame( - $testJson['client_email'] . ':' . $o->getCacheKey(), - $sa->getCacheKey() - ); - } - - public function testShouldBeTheSameAsOAuth2WithTheSameScopeWithSub() - { - $testJson = createTestJson(); - $scope = ['scope/1', 'scope/2']; - $sub = 'sub123'; - $sa = new ServiceAccountCredentials( - $scope, - $testJson, - $sub - ); - $o = new OAuth2(['scope' => $scope]); - $this->assertSame( - $testJson['client_email'] . ':' . $o->getCacheKey() . ':' . $sub, - $sa->getCacheKey() - ); - } - - public function testShouldBeTheSameAsOAuth2WithTheSameScopeWithSubAddedLater() - { - $testJson = createTestJson(); - $scope = ['scope/1', 'scope/2']; - $sub = 'sub123'; - $sa = new ServiceAccountCredentials( - $scope, - $testJson, - null - ); - $sa->setSub($sub); - - $o = new OAuth2(['scope' => $scope]); - $this->assertSame( - $testJson['client_email'] . ':' . $o->getCacheKey() . ':' . $sub, - $sa->getCacheKey() - ); - } -} - -class SACConstructorTest extends TestCase -{ - /** - * @expectedException InvalidArgumentException - */ - public function testShouldFailIfScopeIsNotAValidType() - { - $testJson = createTestJson(); - $notAnArrayOrString = new \stdClass(); - $sa = new ServiceAccountCredentials( - $notAnArrayOrString, - $testJson - ); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testShouldFailIfJsonDoesNotHaveClientEmail() - { - $testJson = createTestJson(); - unset($testJson['client_email']); - $scope = ['scope/1', 'scope/2']; - $sa = new ServiceAccountCredentials( - $scope, - $testJson - ); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testShouldFailIfJsonDoesNotHavePrivateKey() - { - $testJson = createTestJson(); - unset($testJson['private_key']); - $scope = ['scope/1', 'scope/2']; - $sa = new ServiceAccountCredentials( - $scope, - $testJson - ); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testFailsToInitalizeFromANonExistentFile() - { - $keyFile = __DIR__ . '/../fixtures' . '/does-not-exist-private.json'; - new ServiceAccountCredentials('scope/1', $keyFile); - } - - public function testInitalizeFromAFile() - { - $keyFile = __DIR__ . '/../fixtures' . '/private.json'; - $this->assertNotNull( - new ServiceAccountCredentials('scope/1', $keyFile) - ); - } - - /** - * @expectedException LogicException - */ - public function testFailsToInitializeFromInvalidJsonData() - { - $tmp = tmpfile(); - fwrite($tmp, '{'); - - $path = stream_get_meta_data($tmp)['uri']; - - try { - new ServiceAccountCredentials('scope/1', $path); - } catch (\Exception $e) { - fclose($tmp); - throw $e; - } - } -} - -class SACFromEnvTest extends TestCase -{ - protected function tearDown() - { - putenv(ServiceAccountCredentials::ENV_VAR); // removes it from - } - - public function testIsNullIfEnvVarIsNotSet() - { - $this->assertNull(ServiceAccountCredentials::fromEnv()); - } - - /** - * @expectedException DomainException - */ - public function testFailsIfEnvSpecifiesNonExistentFile() - { - $keyFile = __DIR__ . '/../fixtures' . '/does-not-exist-private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - ApplicationDefaultCredentials::getCredentials('a scope'); - } - - public function testSucceedIfFileExists() - { - $keyFile = __DIR__ . '/../fixtures' . '/private.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - $this->assertNotNull(ApplicationDefaultCredentials::getCredentials('a scope')); - } -} - -class SACFromWellKnownFileTest extends TestCase -{ - private $originalHome; - - protected function setUp() - { - $this->originalHome = getenv('HOME'); - } - - protected function tearDown() - { - if ($this->originalHome != getenv('HOME')) { - putenv('HOME=' . $this->originalHome); - } - } - - public function testIsNullIfFileDoesNotExist() - { - putenv('HOME=' . __DIR__ . '/../not_exists_fixtures'); - $this->assertNull( - ServiceAccountCredentials::fromWellKnownFile() - ); - } - - public function testSucceedIfFileIsPresent() - { - putenv('HOME=' . __DIR__ . '/../fixtures'); - $this->assertNotNull( - ApplicationDefaultCredentials::getCredentials('a scope') - ); - } -} - -class SACFetchAuthTokenTest extends TestCase -{ - private $privateKey; - - public function setUp() - { - $this->privateKey = - file_get_contents(__DIR__ . '/../fixtures' . '/private.pem'); - } - - private function createTestJson() - { - $testJson = createTestJson(); - $testJson['private_key'] = $this->privateKey; - - return $testJson; - } - - /** - * @expectedException GuzzleHttp\Exception\ClientException - */ - public function testFailsOnClientErrors() - { - $testJson = $this->createTestJson(); - $scope = ['scope/1', 'scope/2']; - $httpHandler = getHandler([ - buildResponse(400), - ]); - $sa = new ServiceAccountCredentials( - $scope, - $testJson - ); - $sa->fetchAuthToken($httpHandler); - } - - /** - * @expectedException GuzzleHttp\Exception\ServerException - */ - public function testFailsOnServerErrors() - { - $testJson = $this->createTestJson(); - $scope = ['scope/1', 'scope/2']; - $httpHandler = getHandler([ - buildResponse(500), - ]); - $sa = new ServiceAccountCredentials( - $scope, - $testJson - ); - $sa->fetchAuthToken($httpHandler); - } - - public function testCanFetchCredsOK() - { - $testJson = $this->createTestJson(); - $testJsonText = json_encode($testJson); - $scope = ['scope/1', 'scope/2']; - $httpHandler = getHandler([ - buildResponse(200, [], Psr7\stream_for($testJsonText)), - ]); - $sa = new ServiceAccountCredentials( - $scope, - $testJson - ); - $tokens = $sa->fetchAuthToken($httpHandler); - $this->assertEquals($testJson, $tokens); - } - - public function testUpdateMetadataFunc() - { - $testJson = $this->createTestJson(); - $scope = ['scope/1', 'scope/2']; - $access_token = 'accessToken123'; - $responseText = json_encode(array('access_token' => $access_token)); - $httpHandler = getHandler([ - buildResponse(200, [], Psr7\stream_for($responseText)), - ]); - $sa = new ServiceAccountCredentials( - $scope, - $testJson - ); - $update_metadata = $sa->getUpdateMetadataFunc(); - $this->assertInternalType('callable', $update_metadata); - - $actual_metadata = call_user_func( - $update_metadata, - $metadata = array('foo' => 'bar'), - $authUri = null, - $httpHandler - ); - $this->assertArrayHasKey( - CredentialsLoader::AUTH_METADATA_KEY, - $actual_metadata - ); - $this->assertEquals( - $actual_metadata[CredentialsLoader::AUTH_METADATA_KEY], - array('Bearer ' . $access_token) - ); - } - - public function testShouldBeIdTokenWhenTargetAudienceIsSet() - { - $testJson = $this->createTestJson(); - $expectedToken = ['id_token' => 'idtoken12345']; - $timesCalled = 0; - $httpHandler = function ($request) use (&$timesCalled, $expectedToken) { - $timesCalled++; - parse_str($request->getBody(), $post); - $this->assertArrayHasKey('assertion', $post); - list($header, $payload, $sig) = explode('.', $post['assertion']); - $jwtParams = json_decode(base64_decode($payload), true); - $this->assertArrayHasKey('target_audience', $jwtParams); - $this->assertEquals('a target audience', $jwtParams['target_audience']); - - return new Psr7\Response(200, [], Psr7\stream_for(json_encode($expectedToken))); - }; - $sa = new ServiceAccountCredentials(null, $testJson, null, 'a target audience'); - $this->assertEquals($expectedToken, $sa->fetchAuthToken($httpHandler)); - $this->assertEquals(1, $timesCalled); - } - - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Scope and targetAudience cannot both be supplied - */ - public function testSettingBothScopeAndTargetAudienceThrowsException() - { - $testJson = $this->createTestJson(); - $sa = new ServiceAccountCredentials( - 'a-scope', - $testJson, - null, - 'a-target-audience' - ); - } -} - -class SACGetClientNameTest extends TestCase -{ - public function testReturnsClientEmail() - { - $testJson = createTestJson(); - $sa = new ServiceAccountCredentials('scope/1', $testJson); - $this->assertEquals($testJson['client_email'], $sa->getClientName()); - } -} - -class SACGetProjectIdTest extends TestCase -{ - public function testGetProjectId() - { - $testJson = createTestJson(); - $sa = new ServiceAccountCredentials('scope/1', $testJson); - $this->assertEquals($testJson['project_id'], $sa->getProjectId()); - } -} - -class SACGetQuotaProjectTest extends TestCase -{ - public function testGetQuotaProject() - { - $keyFile = __DIR__ . '/../fixtures' . '/private.json'; - $sa = new ServiceAccountCredentials('scope/1', $keyFile); - $this->assertEquals('test_quota_project', $sa->getQuotaProject()); - } -} - -class SACJwtAccessTest extends TestCase -{ - private $privateKey; - - public function setUp() - { - $this->privateKey = - file_get_contents(__DIR__ . '/../fixtures' . '/private.pem'); - } - - private function createTestJson() - { - $testJson = createTestJson(); - $testJson['private_key'] = $this->privateKey; - - return $testJson; - } - - /** - * @expectedException InvalidArgumentException - */ - public function testFailsToInitalizeFromANonExistentFile() - { - $keyFile = __DIR__ . '/../fixtures' . '/does-not-exist-private.json'; - new ServiceAccountJwtAccessCredentials($keyFile); - } - - public function testInitalizeFromAFile() - { - $keyFile = __DIR__ . '/../fixtures' . '/private.json'; - $this->assertNotNull( - new ServiceAccountJwtAccessCredentials($keyFile) - ); - } - - /** - * @expectedException LogicException - */ - public function testFailsToInitializeFromInvalidJsonData() - { - $tmp = tmpfile(); - fwrite($tmp, '{'); - - $path = stream_get_meta_data($tmp)['uri']; - - try { - new ServiceAccountJwtAccessCredentials($path); - } catch (\Exception $e) { - fclose($tmp); - throw $e; - } - } - - /** - * @expectedException InvalidArgumentException - */ - public function testFailsOnMissingClientEmail() - { - $testJson = $this->createTestJson(); - unset($testJson['client_email']); - $sa = new ServiceAccountJwtAccessCredentials( - $testJson - ); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testFailsOnMissingPrivateKey() - { - $testJson = $this->createTestJson(); - unset($testJson['private_key']); - $sa = new ServiceAccountJwtAccessCredentials( - $testJson - ); - } - - public function testCanInitializeFromJson() - { - $testJson = $this->createTestJson(); - $sa = new ServiceAccountJwtAccessCredentials( - $testJson - ); - $this->assertNotNull($sa); - } - - public function testNoOpOnFetchAuthToken() - { - $testJson = $this->createTestJson(); - $sa = new ServiceAccountJwtAccessCredentials( - $testJson - ); - $this->assertNotNull($sa); - - $httpHandler = getHandler([ - buildResponse(200), - ]); - $result = $sa->fetchAuthToken($httpHandler); // authUri has not been set - $this->assertNull($result); - } - - public function testAuthUriIsNotSet() - { - $testJson = $this->createTestJson(); - $sa = new ServiceAccountJwtAccessCredentials( - $testJson - ); - $this->assertNotNull($sa); - - $update_metadata = $sa->getUpdateMetadataFunc(); - $this->assertInternalType('callable', $update_metadata); - - $actual_metadata = call_user_func( - $update_metadata, - $metadata = array('foo' => 'bar'), - $authUri = null - ); - $this->assertArrayNotHasKey( - CredentialsLoader::AUTH_METADATA_KEY, - $actual_metadata - ); - } - - public function testGetLastReceivedToken() - { - $testJson = $this->createTestJson(); - $sa = new ServiceAccountJwtAccessCredentials($testJson); - $token = $sa->fetchAuthToken(); - $this->assertEquals($token, $sa->getLastReceivedToken()); - } - - public function testUpdateMetadataFunc() - { - $testJson = $this->createTestJson(); - $sa = new ServiceAccountJwtAccessCredentials( - $testJson - ); - $this->assertNotNull($sa); - - $update_metadata = $sa->getUpdateMetadataFunc(); - $this->assertInternalType('callable', $update_metadata); - - $actual_metadata = call_user_func( - $update_metadata, - $metadata = array('foo' => 'bar'), - $authUri = 'https://example.com/service' - ); - $this->assertArrayHasKey( - CredentialsLoader::AUTH_METADATA_KEY, - $actual_metadata - ); - - $authorization = $actual_metadata[CredentialsLoader::AUTH_METADATA_KEY]; - $this->assertInternalType('array', $authorization); - - $bearer_token = current($authorization); - $this->assertInternalType('string', $bearer_token); - $this->assertEquals(0, strpos($bearer_token, 'Bearer ')); - $this->assertGreaterThan(30, strlen($bearer_token)); - - $actual_metadata2 = call_user_func( - $update_metadata, - $metadata = array('foo' => 'bar'), - $authUri = 'https://example.com/anotherService' - ); - $this->assertArrayHasKey( - CredentialsLoader::AUTH_METADATA_KEY, - $actual_metadata2 - ); - - $authorization2 = $actual_metadata2[CredentialsLoader::AUTH_METADATA_KEY]; - $this->assertInternalType('array', $authorization2); - - $bearer_token2 = current($authorization2); - $this->assertInternalType('string', $bearer_token2); - $this->assertEquals(0, strpos($bearer_token2, 'Bearer ')); - $this->assertGreaterThan(30, strlen($bearer_token2)); - $this->assertNotEquals($bearer_token2, $bearer_token); - } -} - -class SACJwtAccessComboTest extends TestCase -{ - private $privateKey; - - public function setUp() - { - $this->privateKey = - file_get_contents(__DIR__ . '/../fixtures' . '/private.pem'); - } - - private function createTestJson() - { - $testJson = createTestJson(); - $testJson['private_key'] = $this->privateKey; - - return $testJson; - } - - public function testNoScopeUseJwtAccess() - { - $testJson = $this->createTestJson(); - // no scope, jwt access should be used, no outbound - // call should be made - $scope = null; - $sa = new ServiceAccountCredentials( - $scope, - $testJson - ); - $this->assertNotNull($sa); - - $update_metadata = $sa->getUpdateMetadataFunc(); - $this->assertInternalType('callable', $update_metadata); - - $actual_metadata = call_user_func( - $update_metadata, - $metadata = array('foo' => 'bar'), - $authUri = 'https://example.com/service' - ); - $this->assertArrayHasKey( - CredentialsLoader::AUTH_METADATA_KEY, - $actual_metadata - ); - - $authorization = $actual_metadata[CredentialsLoader::AUTH_METADATA_KEY]; - $this->assertInternalType('array', $authorization); - - $bearer_token = current($authorization); - $this->assertInternalType('string', $bearer_token); - $this->assertEquals(0, strpos($bearer_token, 'Bearer ')); - $this->assertGreaterThan(30, strlen($bearer_token)); - } - - /** @runInSeparateProcess */ - public function testJwtAccessFromApplicationDefault() - { - $keyFile = __DIR__ . '/../fixtures3/service_account_credentials.json'; - putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); - $creds = ApplicationDefaultCredentials::getCredentials( - null, // $scope - null, // $httpHandler - null, // $cacheConfig - null, // $cache - null, // $quotaProject - 'a default scope' // $defaultScope - ); - $authUri = 'https://example.com/service'; - - $metadata = $creds->updateMetadata(['foo' => 'bar'], $authUri); - - $this->assertArrayHasKey('authorization', $metadata); - $token = str_replace('Bearer ', '', $metadata['authorization'][0]); - $key = file_get_contents(__DIR__ . '/../fixtures3/key.pub'); - - $class = 'JWT'; - if (class_exists('Firebase\JWT\JWT')) { - $class = 'Firebase\JWT\JWT'; - } - $jwt = new $class(); - $result = $jwt::decode($token, $key, ['RS256']); - - $this->assertEquals($authUri, $result->aud); - } - - public function testNoScopeAndNoAuthUri() - { - $testJson = $this->createTestJson(); - // no scope, jwt access should be used, no outbound - // call should be made - $scope = null; - $sa = new ServiceAccountCredentials( - $scope, - $testJson - ); - $this->assertNotNull($sa); - - $update_metadata = $sa->getUpdateMetadataFunc(); - $this->assertInternalType('callable', $update_metadata); - - $actual_metadata = call_user_func( - $update_metadata, - $metadata = array('foo' => 'bar'), - $authUri = null - ); - // no access_token is added to the metadata hash - // but also, no error should be thrown - $this->assertInternalType('array', $actual_metadata); - $this->assertArrayNotHasKey( - CredentialsLoader::AUTH_METADATA_KEY, - $actual_metadata - ); - } - - public function testUpdateMetadataJwtAccess() - { - $testJson = $this->createTestJson(); - // no scope, jwt access should be used, no outbound - // call should be made - $scope = null; - $sa = new ServiceAccountCredentials( - $scope, - $testJson - ); - $this->assertNotNull($sa); - $metadata = $sa->updateMetadata( - array('foo' => 'bar'), - 'https://example.com/service' - ); - $this->assertArrayHasKey( - CredentialsLoader::AUTH_METADATA_KEY, - $metadata - ); - - $authorization = $metadata[CredentialsLoader::AUTH_METADATA_KEY]; - $this->assertInternalType('array', $authorization); - - $bearerToken = current($authorization); - $this->assertInternalType('string', $bearerToken); - $this->assertEquals(0, strpos($bearerToken, 'Bearer ')); - $token = str_replace('Bearer ', '', $bearerToken); - - $lastReceivedToken = $sa->getLastReceivedToken(); - $this->assertArrayHasKey('access_token', $lastReceivedToken); - $this->assertEquals($token, $lastReceivedToken['access_token']); - } -} - -class SACJWTGetCacheKeyTest extends TestCase -{ - public function testShouldBeTheSameAsOAuth2WithTheSameScope() - { - $testJson = createTestJson(); - $scope = ['scope/1', 'scope/2']; - $sa = new ServiceAccountJwtAccessCredentials($testJson); - $this->assertNull($sa->getCacheKey()); - } -} - -class SACJWTGetClientNameTest extends TestCase -{ - public function testReturnsClientEmail() - { - $testJson = createTestJson(); - $sa = new ServiceAccountJwtAccessCredentials($testJson); - $this->assertEquals($testJson['client_email'], $sa->getClientName()); - } -} - -class SACJWTGetProjectIdTest extends TestCase -{ - public function testGetProjectId() - { - $testJson = createTestJson(); - $sa = new ServiceAccountJwtAccessCredentials($testJson); - $this->assertEquals($testJson['project_id'], $sa->getProjectId()); - } -} - -class SACJWTGetQuotaProjectTest extends TestCase -{ - public function testGetQuotaProject() - { - $keyFile = __DIR__ . '/../fixtures' . '/private.json'; - $sa = new ServiceAccountJwtAccessCredentials($keyFile); - $this->assertEquals('test_quota_project', $sa->getQuotaProject()); - } -} diff --git a/tests/Credentials/UserRefreshCredentialsTest.php b/tests/Credentials/UserRefreshCredentialsTest.php deleted file mode 100644 index 3aa3b2497..000000000 --- a/tests/Credentials/UserRefreshCredentialsTest.php +++ /dev/null @@ -1,280 +0,0 @@ - 'client123', - 'client_secret' => 'clientSecret123', - 'refresh_token' => 'refreshToken123', - 'type' => 'authorized_user', - ]; -} - -class URCGetCacheKeyTest extends TestCase -{ - public function testShouldBeTheSameAsOAuth2WithTheSameScope() - { - $testJson = createURCTestJson(); - $scope = ['scope/1', 'scope/2']; - $sa = new UserRefreshCredentials( - $scope, - $testJson - ); - $o = new OAuth2(['scope' => $scope]); - $this->assertSame( - $testJson['client_id'] . ':' . $o->getCacheKey(), - $sa->getCacheKey() - ); - } -} - -class URCConstructorTest extends TestCase -{ - /** - * @expectedException InvalidArgumentException - */ - public function testShouldFailIfScopeIsNotAValidType() - { - $testJson = createURCTestJson(); - $notAnArrayOrString = new \stdClass(); - $sa = new UserRefreshCredentials( - $notAnArrayOrString, - $testJson - ); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testShouldFailIfJsonDoesNotHaveClientSecret() - { - $testJson = createURCTestJson(); - unset($testJson['client_secret']); - $scope = ['scope/1', 'scope/2']; - $sa = new UserRefreshCredentials( - $scope, - $testJson - ); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testShouldFailIfJsonDoesNotHaveRefreshToken() - { - $testJson = createURCTestJson(); - unset($testJson['refresh_token']); - $scope = ['scope/1', 'scope/2']; - $sa = new UserRefreshCredentials( - $scope, - $testJson - ); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testShouldFailIfJsonDoesNotHaveClientId() - { - $testJson = createURCTestJson(); - unset($testJson['client_id']); - $scope = ['scope/1', 'scope/2']; - $sa = new UserRefreshCredentials( - $scope, - $testJson - ); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testFailsToInitalizeFromANonExistentFile() - { - $keyFile = __DIR__ . '/../fixtures/does-not-exist-private.json'; - new UserRefreshCredentials('scope/1', $keyFile); - } - - public function testInitalizeFromAFile() - { - $keyFile = __DIR__ . '/../fixtures2' . '/private.json'; - $this->assertNotNull( - new UserRefreshCredentials('scope/1', $keyFile) - ); - } - - /** - * @expectedException LogicException - */ - public function testFailsToInitializeFromInvalidJsonData() - { - $tmp = tmpfile(); - fwrite($tmp, '{'); - - $path = stream_get_meta_data($tmp)['uri']; - - try { - new UserRefreshCredentials('scope/1', $path); - } catch (\Exception $e) { - fclose($tmp); - throw $e; - } - } - - public function testValid3LOauthCreds() - { - $keyFile = __DIR__ . '/../fixtures2/valid_oauth_creds.json'; - $this->assertNotNull( - new UserRefreshCredentials('scope/1', $keyFile) - ); - } -} - -class URCFromEnvTest extends TestCase -{ - protected function tearDown() - { - putenv(UserRefreshCredentials::ENV_VAR); // removes it from - } - - public function testIsNullIfEnvVarIsNotSet() - { - $this->assertNull(UserRefreshCredentials::fromEnv('a scope')); - } - - /** - * @expectedException DomainException - */ - public function testFailsIfEnvSpecifiesNonExistentFile() - { - $keyFile = __DIR__ . '/../fixtures/does-not-exist-private.json'; - putenv(UserRefreshCredentials::ENV_VAR . '=' . $keyFile); - UserRefreshCredentials::fromEnv('a scope'); - } - - public function testSucceedIfFileExists() - { - $keyFile = __DIR__ . '/../fixtures2/private.json'; - putenv(UserRefreshCredentials::ENV_VAR . '=' . $keyFile); - $this->assertNotNull(ApplicationDefaultCredentials::getCredentials('a scope')); - } -} - -class URCFromWellKnownFileTest extends TestCase -{ - private $originalHome; - - protected function setUp() - { - $this->originalHome = getenv('HOME'); - } - - protected function tearDown() - { - if ($this->originalHome != getenv('HOME')) { - putenv('HOME=' . $this->originalHome); - } - } - - public function testIsNullIfFileDoesNotExist() - { - putenv('HOME=' . __DIR__ . '/../not_exist_fixtures'); - $this->assertNull( - UserRefreshCredentials::fromWellKnownFile('a scope') - ); - } - - public function testSucceedIfFileIsPresent() - { - putenv('HOME=' . __DIR__ . '/../fixtures2'); - $this->assertNotNull( - ApplicationDefaultCredentials::getCredentials('a scope') - ); - } -} - -class URCFetchAuthTokenTest extends TestCase -{ - /** - * @expectedException GuzzleHttp\Exception\ClientException - */ - public function testFailsOnClientErrors() - { - $testJson = createURCTestJson(); - $scope = ['scope/1', 'scope/2']; - $httpHandler = getHandler([ - buildResponse(400), - ]); - $sa = new UserRefreshCredentials( - $scope, - $testJson - ); - $sa->fetchAuthToken($httpHandler); - } - - /** - * @expectedException GuzzleHttp\Exception\ServerException - */ - public function testFailsOnServerErrors() - { - $testJson = createURCTestJson(); - $scope = ['scope/1', 'scope/2']; - $httpHandler = getHandler([ - buildResponse(500), - ]); - $sa = new UserRefreshCredentials( - $scope, - $testJson - ); - $sa->fetchAuthToken($httpHandler); - } - - public function testCanFetchCredsOK() - { - $testJson = createURCTestJson(); - $testJsonText = json_encode($testJson); - $scope = ['scope/1', 'scope/2']; - $httpHandler = getHandler([ - buildResponse(200, [], Psr7\stream_for($testJsonText)), - ]); - $sa = new UserRefreshCredentials( - $scope, - $testJson - ); - $tokens = $sa->fetchAuthToken($httpHandler); - $this->assertEquals($testJson, $tokens); - } -} - -class URCGetQuotaProjectTest extends TestCase -{ - public function testGetQuotaProject() - { - $keyFile = __DIR__ . '/../fixtures2' . '/private.json'; - $sa = new UserRefreshCredentials('a-scope', $keyFile); - $this->assertEquals('test_quota_project', $sa->getQuotaProject()); - } -} diff --git a/tests/CredentialsLoaderTest.php b/tests/CredentialsLoaderTest.php deleted file mode 100644 index 32de32070..000000000 --- a/tests/CredentialsLoaderTest.php +++ /dev/null @@ -1,50 +0,0 @@ -updateMetadata(['authentication' => 'foo']); - $this->assertArrayHasKey('authentication', $metadata); - $this->assertEquals('foo', $metadata['authentication']); - } -} - -class TestCredentialsLoader extends CredentialsLoader -{ - public function getCacheKey() - { - return 'test'; - } - - public function fetchAuthToken(callable $httpHandler = null) - { - return 'test'; - } - - public function getLastReceivedToken() - { - return null; - } -} diff --git a/tests/FetchAuthTokenCacheTest.php b/tests/FetchAuthTokenCacheTest.php deleted file mode 100644 index 3c4e5cbb5..000000000 --- a/tests/FetchAuthTokenCacheTest.php +++ /dev/null @@ -1,505 +0,0 @@ -mockFetcher = $this->prophesize(); - $this->mockFetcher->willImplement('Google\Auth\FetchAuthTokenInterface'); - $this->mockFetcher->willImplement('Google\Auth\UpdateMetadataInterface'); - $this->mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $this->mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - $this->mockSigner = $this->prophesize('Google\Auth\SignBlobInterface'); - } - - public function testUsesCachedAccessToken() - { - $cacheKey = 'myKey'; - $token = '2/abcdef1234567890'; - $cachedValue = ['access_token' => $token]; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($cacheKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->fetchAuthToken() - ->shouldNotBeCalled(); - $this->mockFetcher->getCacheKey() - ->shouldBeCalled() - ->willReturn($cacheKey); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - null, - $this->mockCache->reveal() - ); - $accessToken = $cachedFetcher->fetchAuthToken(); - $this->assertEquals($accessToken, ['access_token' => $token]); - } - - public function testUsesCachedIdToken() - { - $cacheKey = 'myKey'; - $token = '2/abcdef1234567890'; - $cachedValue = ['id_token' => $token]; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($cacheKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->fetchAuthToken() - ->shouldNotBeCalled(); - $this->mockFetcher->getCacheKey() - ->shouldBeCalled() - ->willReturn($cacheKey); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - null, - $this->mockCache->reveal() - ); - $idToken = $cachedFetcher->fetchAuthToken(); - $this->assertEquals($idToken, ['id_token' => $token]); - } - - public function testUpdateMetadataWithCache() - { - $cacheKey = 'myKey'; - $token = '2/abcdef1234567890'; - $cachedValue = ['access_token' => $token]; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($cacheKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->fetchAuthToken() - ->shouldNotBeCalled(); - $this->mockFetcher->getCacheKey() - ->shouldBeCalled() - ->willReturn($cacheKey); - $this->mockFetcher->updateMetadata(Argument::type('array'), null, null) - ->shouldBeCalled() - ->will(function ($args, $fetcher) { - return $args[0]; - }); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - null, - $this->mockCache->reveal() - ); - $headers = $cachedFetcher->updateMetadata(['foo' => 'bar']); - $this->assertArrayHasKey('authorization', $headers); - $this->assertEquals(["Bearer $token"], $headers['authorization']); - $this->assertArrayHasKey('foo', $headers); - $this->assertEquals('bar', $headers['foo']); - } - - public function testUpdateMetadataWithoutCache() - { - $cacheKey = 'myKey'; - $token = '2/abcdef1234567890'; - $value = ['access_token' => $token]; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCache->getItem($cacheKey) - ->shouldBeCalledTimes(2) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->getCacheKey() - ->shouldBeCalled() - ->willReturn($cacheKey); - $this->mockFetcher->getLastReceivedToken() - ->shouldBeCalled() - ->willReturn($value); - $this->mockCacheItem->set($value) - ->shouldBeCalledTimes(1); - $this->mockCacheItem->expiresAfter(1500) - ->shouldBeCalledTimes(1); - $this->mockCache->save($this->mockCacheItem) - ->shouldBeCalledTimes(1); - $this->mockFetcher->updateMetadata(Argument::type('array'), null, null) - ->shouldBeCalled() - ->will(function ($args, $fetcher) use ($token) { - $args[0]['authorization'] = ["Bearer $token"]; - return $args[0]; - }); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - null, - $this->mockCache->reveal() - ); - $headers = $cachedFetcher->updateMetadata(['foo' => 'bar']); - $this->assertArrayHasKey('authorization', $headers); - $this->assertEquals(["Bearer $token"], $headers['authorization']); - $this->assertArrayHasKey('foo', $headers); - $this->assertEquals('bar', $headers['foo']); - } - - public function testUpdateMetadataWithJwtAccess() - { - $privateKey = file_get_contents(__DIR__ . '/fixtures/private.pem'); - $testJson = [ - 'private_key' => $privateKey, - 'private_key_id' => 'key123', - 'client_email' => 'test@example.com', - 'client_id' => 'client123', - 'type' => 'service_account', - 'project_id' => 'example_project', - ]; - - $fetcher = new ServiceAccountCredentials(null, $testJson); - $cache = new MemoryCacheItemPool(); - - $cachedFetcher = new FetchAuthTokenCache( - $fetcher, - null, - $cache - ); - $metadata = $cachedFetcher->updateMetadata([], 'http://test-auth-uri'); - $this->assertArrayHasKey( - CredentialsLoader::AUTH_METADATA_KEY, - $metadata - ); - - $authorization = $metadata[CredentialsLoader::AUTH_METADATA_KEY]; - $this->assertInternalType('array', $authorization); - - $bearerToken = current($authorization); - $this->assertInternalType('string', $bearerToken); - $this->assertEquals(0, strpos($bearerToken, 'Bearer ')); - $token = str_replace('Bearer ', '', $bearerToken); - - $lastReceivedToken = $cachedFetcher->getLastReceivedToken(); - $this->assertArrayHasKey('access_token', $lastReceivedToken); - $this->assertEquals($token, $lastReceivedToken['access_token']); - - // Ensure token is cached - $metadata2 = $cachedFetcher->updateMetadata([], 'http://test-auth-uri'); - $this->assertEquals($metadata, $metadata2); - - // Ensure token for different URI is NOT cached - $metadata3 = $cachedFetcher->updateMetadata([], 'http://test-auth-uri-2'); - $this->assertNotEquals($metadata, $metadata3); - } - - /** - * @expectedException RuntimeException - * @expectedExceptionMessage Credentials fetcher does not implement Google\Auth\UpdateMetadataInterface - */ - public function testUpdateMetadataWithInvalidFetcher() - { - $mockFetcher = $this->prophesize('Google\Auth\FetchAuthTokenInterface'); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $mockFetcher->reveal(), - null, - $this->mockCache->reveal() - ); - $cachedFetcher->updateMetadata(['foo' => 'bar']); - } - - - public function testShouldReturnValueWhenNotExpired() - { - $cacheKey = 'myKey'; - $token = '2/abcdef1234567890'; - $expiresAt = time() + 10; - $cachedValue = [ - 'access_token' => $token, - 'expires_at' => $expiresAt, - ]; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($cacheKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->fetchAuthToken() - ->shouldNotBeCalled(); - $this->mockFetcher->getCacheKey() - ->shouldBeCalled() - ->willReturn($cacheKey); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - null, - $this->mockCache->reveal() - ); - $accessToken = $cachedFetcher->fetchAuthToken(); - $this->assertEquals($accessToken, [ - 'access_token' => $token, - 'expires_at' => $expiresAt - ]); - } - - public function testShouldNotReturnValueWhenExpired() - { - $cacheKey = 'myKey'; - $token = '2/abcdef1234567890'; - $expiresAt = time() - 10; - $cachedValue = [ - 'access_token' => $token, - 'expires_at' => $expiresAt, - ]; - $newToken = ['access_token' => '3/abcdef1234567890']; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCacheItem->set($newToken) - ->shouldBeCalledTimes(1); - $this->mockCacheItem->expiresAfter(1500) - ->shouldBeCalledTimes(1); - $this->mockCache->getItem($cacheKey) - ->shouldBeCalledTimes(2) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->fetchAuthToken(null) - ->shouldBeCalledTimes(1) - ->willReturn($newToken); - $this->mockFetcher->getCacheKey() - ->shouldBeCalled() - ->willReturn($cacheKey); - $this->mockCache->save($this->mockCacheItem) - ->shouldBeCalledTimes(1); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - null, - $this->mockCache->reveal() - ); - $accessToken = $cachedFetcher->fetchAuthToken(); - $this->assertEquals($newToken, $accessToken); - } - - public function testGetsCachedAuthTokenUsingCachePrefix() - { - $prefix = 'test_prefix_'; - $cacheKey = 'myKey'; - $token = '2/abcdef1234567890'; - $cachedValue = ['access_token' => $token]; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($prefix . $cacheKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->fetchAuthToken() - ->shouldNotBeCalled(); - $this->mockFetcher->getCacheKey() - ->shouldBeCalled() - ->willReturn($cacheKey); - - // Run the test - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - ['prefix' => $prefix], - $this->mockCache->reveal() - ); - $accessToken = $cachedFetcher->fetchAuthToken(); - $this->assertEquals($accessToken, ['access_token' => $token]); - } - - public function testShouldSaveValueInCacheWithCacheOptions() - { - $prefix = 'test_prefix_'; - $lifetime = '70707'; - $cacheKey = 'myKey'; - $token = '1/abcdef1234567890'; - $cachedValue = ['access_token' => $token]; - $this->mockCacheItem->get(Argument::any()) - ->willReturn(null); - $this->mockCacheItem->isHit() - ->willReturn(false); - $this->mockCacheItem->set($cachedValue) - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCacheItem->expiresAfter($lifetime) - ->shouldBeCalledTimes(1); - $this->mockCache->getItem($prefix . $cacheKey) - ->shouldBeCalledTimes(2) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->shouldBeCalled(); - $this->mockFetcher->getCacheKey() - ->willReturn($cacheKey); - $this->mockFetcher->fetchAuthToken(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - - // Run the test - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - ['prefix' => $prefix, 'lifetime' => $lifetime], - $this->mockCache->reveal() - ); - $accessToken = $cachedFetcher->fetchAuthToken(); - $this->assertEquals($accessToken, ['access_token' => $token]); - } - - public function testGetLastReceivedToken() - { - $token = 'foo'; - - $mockFetcher = $this->prophesize('Google\Auth\FetchAuthTokenInterface'); - $mockFetcher->getLastReceivedToken() - ->shouldBeCalled() - ->willReturn([ - 'access_token' => $token - ]); - - $fetcher = new FetchAuthTokenCache( - $mockFetcher->reveal(), - [], - $this->mockCache->reveal() - ); - - $this->assertEquals($token, $fetcher->getLastReceivedToken()['access_token']); - } - - public function testGetClientName() - { - $name = 'test@example.com'; - - $this->mockSigner->getClientName(null) - ->shouldBeCalled() - ->willReturn($name); - - $fetcher = new FetchAuthTokenCache( - $this->mockSigner->reveal(), - [], - $this->mockCache->reveal() - ); - - $this->assertEquals($name, $fetcher->getClientName()); - } - - public function testSignBlob() - { - $stringToSign = 'foobar'; - $signature = 'helloworld'; - - $this->mockSigner->willImplement('Google\Auth\FetchAuthTokenInterface'); - $this->mockSigner->signBlob($stringToSign, true) - ->shouldBeCalled() - ->willReturn($signature); - - $fetcher = new FetchAuthTokenCache( - $this->mockSigner->reveal(), - [], - $this->mockCache->reveal() - ); - - $this->assertEquals($signature, $fetcher->signBlob($stringToSign, true)); - } - - /** - * @expectedException RuntimeException - */ - public function testSignBlobInvalidFetcher() - { - $this->mockFetcher->signBlob('test') - ->shouldNotbeCalled(); - - $fetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - [], - $this->mockCache - ); - - $fetcher->signBlob('test'); - } - - public function testGetProjectId() - { - $projectId = 'foobar'; - - $mockFetcher = $this->prophesize('Google\Auth\ProjectIdProviderInterface'); - $mockFetcher->willImplement('Google\Auth\FetchAuthTokenInterface'); - $mockFetcher->getProjectId(null) - ->shouldBeCalled() - ->willReturn($projectId); - - $fetcher = new FetchAuthTokenCache( - $mockFetcher->reveal(), - [], - $this->mockCache->reveal() - ); - - $this->assertEquals($projectId, $fetcher->getProjectId()); - } - - /** - * @expectedException RuntimeException - */ - public function testGetProjectIdInvalidFetcher() - { - $mockFetcher = $this->prophesize('Google\Auth\FetchAuthTokenInterface'); - $mockFetcher->getProjectId() - ->shouldNotbeCalled(); - - $fetcher = new FetchAuthTokenCache( - $mockFetcher->reveal(), - [], - $this->mockCache - ); - - $fetcher->getProjectId(); - } -} diff --git a/tests/FetchAuthTokenTest.php b/tests/FetchAuthTokenTest.php deleted file mode 100644 index 244452a26..000000000 --- a/tests/FetchAuthTokenTest.php +++ /dev/null @@ -1,237 +0,0 @@ -prophesize($fetcherClass); - - $httpHandlerCalled = false; - $httpHandler = function () use (&$httpHandlerCalled) { - $httpHandlerCalled = true; - return ['access_token' => 'xyz']; - }; - - if (in_array( - 'Google\Auth\GetQuotaProjectInterface', - class_implements($fetcherClass) - )) { - $mockFetcher->getQuotaProject()->shouldBeCalledTimes(1); - } - $mockFetcher->fetchAuthToken(Argument::any()) - ->shouldBeCalledTimes(1) - ->will($httpHandler); - $mockFetcher->getCacheKey()->willReturn(''); - - $tokenCallbackCalled = false; - $tokenCallback = function ($cacheKey, $accessToken) use (&$tokenCallbackCalled) { - $tokenCallbackCalled = true; - $this->assertEquals('xyz', $accessToken); - }; - - if ($this->getGuzzleMajorVersion() === 5) { - $clientOptions = [ - 'base_url' => 'https://www.googleapis.com/books/v1/', - 'defaults' => ['exceptions' => false], - ]; - } else { - $clientOptions = [ - 'base_uri' => 'https://www.googleapis.com/books/v1/', - 'http_errors' => false, - ]; - } - - $client = CredentialsLoader::makeHttpClient( - $mockFetcher->reveal(), - $clientOptions, - $httpHandler, - $tokenCallback - ); - - $response = $client->get( - 'volumes?q=Henry+David+Thoreau&country=US' - ); - - $this->assertEquals(401, $response->getStatusCode()); - $this->assertTrue($httpHandlerCalled); - $this->assertTrue($tokenCallbackCalled); - } - - public function provideMakeHttpClient() - { - return [ - ['Google\Auth\Credentials\AppIdentityCredentials'], - ['Google\Auth\Credentials\GCECredentials'], - ['Google\Auth\Credentials\ServiceAccountCredentials'], - ['Google\Auth\Credentials\ServiceAccountJwtAccessCredentials'], - ['Google\Auth\Credentials\UserRefreshCredentials'], - ['Google\Auth\OAuth2'], - ]; - } - - public function testAppIdentityCredentialsGetLastReceivedToken() - { - $class = new \ReflectionClass( - 'Google\Auth\Credentials\AppIdentityCredentials' - ); - $property = $class->getProperty('lastReceivedToken'); - $property->setAccessible(true); - - $credentials = new AppIdentityCredentials(); - $property->setValue($credentials, [ - 'access_token' => 'xyz', - 'expiration_time' => strtotime('2001'), - ]); - - $this->assertGetLastReceivedToken($credentials); - } - - public function testGCECredentialsGetLastReceivedToken() - { - $class = new \ReflectionClass( - 'Google\Auth\Credentials\GCECredentials' - ); - $property = $class->getProperty('lastReceivedToken'); - $property->setAccessible(true); - - $credentials = new GCECredentials(); - $property->setValue($credentials, [ - 'access_token' => 'xyz', - 'expires_at' => strtotime('2001'), - ]); - - $this->assertGetLastReceivedToken($credentials); - } - - public function testServiceAccountCredentialsGetLastReceivedToken() - { - $jsonPath = sprintf( - '%s/fixtures/.config/%s', - __DIR__, - CredentialsLoader::WELL_KNOWN_PATH - ); - - $class = new \ReflectionClass( - 'Google\Auth\Credentials\ServiceAccountCredentials' - ); - $property = $class->getProperty('auth'); - $property->setAccessible(true); - - $oauth2Mock = $this->getOAuth2Mock(); - $oauth2Mock->getScope() - ->willReturn($this->scopes); - - $credentials = new ServiceAccountCredentials($this->scopes, $jsonPath); - $property->setValue($credentials, $oauth2Mock->reveal()); - - $this->assertGetLastReceivedToken($credentials); - } - - public function testServiceAccountJwtAccessCredentialsGetLastReceivedToken() - { - $jsonPath = sprintf( - '%s/fixtures/.config/%s', - __DIR__, - CredentialsLoader::WELL_KNOWN_PATH - ); - - $class = new \ReflectionClass( - 'Google\Auth\Credentials\ServiceAccountJwtAccessCredentials' - ); - $property = $class->getProperty('auth'); - $property->setAccessible(true); - - $credentials = new ServiceAccountJwtAccessCredentials($jsonPath); - $property->setValue($credentials, $this->getOAuth2Mock()->reveal()); - - $this->assertGetLastReceivedToken($credentials); - } - - public function testUserRefreshCredentialsGetLastReceivedToken() - { - $jsonPath = sprintf( - '%s/fixtures2/.config/%s', - __DIR__, - CredentialsLoader::WELL_KNOWN_PATH - ); - - $class = new \ReflectionClass( - 'Google\Auth\Credentials\UserRefreshCredentials' - ); - $property = $class->getProperty('auth'); - $property->setAccessible(true); - - $credentials = new UserRefreshCredentials($this->scopes, $jsonPath); - $property->setValue($credentials, $this->getOAuth2Mock()->reveal()); - - $this->assertGetLastReceivedToken($credentials); - } - - private function getOAuth2() - { - $oauth = new OAuth2([ - 'access_token' => 'xyz', - 'expires_at' => strtotime('2001'), - ]); - - $this->assertGetLastReceivedToken($oauth); - } - - private function getOAuth2Mock() - { - $mock = $this->prophesize('Google\Auth\OAuth2'); - - $mock->getLastReceivedToken() - ->shouldBeCalledTimes(1) - ->willReturn([ - 'access_token' => 'xyz', - 'expires_at' => strtotime('2001'), - ]); - - return $mock; - } - - private function assertGetLastReceivedToken(FetchAuthTokenInterface $fetcher) - { - $accessToken = $fetcher->getLastReceivedToken(); - - $this->assertNotNull($accessToken); - $this->assertArrayHasKey('access_token', $accessToken); - $this->assertArrayHasKey('expires_at', $accessToken); - - $this->assertEquals('xyz', $accessToken['access_token']); - $this->assertEquals(strtotime('2001'), $accessToken['expires_at']); - } -} diff --git a/tests/GCECacheTest.php b/tests/GCECacheTest.php deleted file mode 100644 index 086609708..000000000 --- a/tests/GCECacheTest.php +++ /dev/null @@ -1,161 +0,0 @@ -mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $this->mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - } - - public function testCachedOnGceTrueValue() - { - $cachedValue = true; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem(GCECache::GCE_CACHE_KEY) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - - // Run the test. - $gceCache = new GCECache( - null, - $this->mockCache->reveal() - ); - $this->assertTrue($gceCache->onGce()); - } - - public function testCachedOnGceFalseValue() - { - $cachedValue = false; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem(GCECache::GCE_CACHE_KEY) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - - // Run the test. - $gceCache = new GCECache( - null, - $this->mockCache->reveal() - ); - $this->assertFalse($gceCache->onGce()); - } - - public function testUncached() - { - $gceIsCalled = false; - $dummyHandler = function ($request) use (&$gceIsCalled) { - $gceIsCalled = true; - return new Psr7\Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']); - }; - - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCacheItem->set(true) - ->shouldBeCalledTimes(1); - $this->mockCacheItem->expiresAfter(1500) - ->shouldBeCalledTimes(1); - $this->mockCache->getItem(GCECache::GCE_CACHE_KEY) - ->shouldBeCalledTimes(2) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save($this->mockCacheItem->reveal()) - ->shouldBeCalledTimes(1); - - // Run the test. - $gceCache = new GCECache( - null, - $this->mockCache->reveal() - ); - - $this->assertTrue($gceCache->onGce($dummyHandler)); - $this->assertTrue($gceIsCalled); - } - - public function testShouldFetchFromCacheWithCacheOptions() - { - $prefix = 'test_prefix_'; - $lifetime = '70707'; - $cachedValue = true; - - $this->mockCacheItem->isHit() - ->willReturn(true); - $this->mockCacheItem->get() - ->willReturn($cachedValue); - $this->mockCache->getItem($prefix . GCECache::GCE_CACHE_KEY) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - - // Run the test - $gceCache = new GCECache( - ['prefix' => $prefix, 'lifetime' => $lifetime], - $this->mockCache->reveal() - ); - $this->assertTrue($gceCache->onGce()); - } - - public function testShouldSaveValueInCacheWithCacheOptions() - { - $prefix = 'test_prefix_'; - $lifetime = '70707'; - $gceIsCalled = false; - $dummyHandler = function ($request) use (&$gceIsCalled) { - $gceIsCalled = true; - return new Psr7\Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']); - }; - $this->mockCacheItem->isHit() - ->willReturn(false); - $this->mockCacheItem->set(true) - ->shouldBeCalledTimes(1); - $this->mockCacheItem->expiresAfter($lifetime) - ->shouldBeCalledTimes(1); - $this->mockCache->getItem($prefix . GCECache::GCE_CACHE_KEY) - ->shouldBeCalledTimes(2) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save($this->mockCacheItem->reveal()) - ->shouldBeCalled(); - - // Run the test - $gceCache = new GCECache( - ['prefix' => $prefix, 'lifetime' => $lifetime], - $this->mockCache->reveal() - ); - $onGce = $gceCache->onGce($dummyHandler); - $this->assertTrue($onGce); - $this->assertTrue($gceIsCalled); - } -} diff --git a/tests/HttpHandler/Guzzle5HttpHandlerTest.php b/tests/HttpHandler/Guzzle5HttpHandlerTest.php deleted file mode 100644 index f2e0e4d59..000000000 --- a/tests/HttpHandler/Guzzle5HttpHandlerTest.php +++ /dev/null @@ -1,240 +0,0 @@ -onlyGuzzle5(); - - $uri = $this->prophesize('Psr\Http\Message\UriInterface'); - $body = $this->prophesize('Psr\Http\Message\StreamInterface'); - - $this->mockPsr7Request = $this->prophesize('Psr\Http\Message\RequestInterface'); - $this->mockPsr7Request->getMethod()->willReturn('GET'); - $this->mockPsr7Request->getUri()->willReturn($uri->reveal()); - $this->mockPsr7Request->getHeaders()->willReturn([]); - $this->mockPsr7Request->getBody()->willReturn($body->reveal()); - - $this->mockRequest = $this->prophesize('GuzzleHttp\Message\RequestInterface'); - $this->mockClient = $this->prophesize('GuzzleHttp\Client'); - $this->mockFuture = $this->prophesize('GuzzleHttp\Ring\Future\FutureInterface'); - } - - public function testSuccessfullySendsRealRequest() - { - $request = new \GuzzleHttp\Psr7\Request('get', 'https://httpbin.org/get'); - $client = new \GuzzleHttp\Client(); - $handler = new Guzzle5HttpHandler($client); - $response = $handler($request); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); - $this->assertEquals(200, $response->getStatusCode()); - $json = json_decode((string) $response->getBody(), true); - $this->assertArrayHasKey('url', $json); - $this->assertEquals((string) $request->getUri(), $json['url']); - } - - public function testSuccessfullySendsMockRequest() - { - $response = new Response( - 200, - [], - Stream::factory('Body Text') - ); - $this->mockClient->send(Argument::type('GuzzleHttp\Message\RequestInterface')) - ->willReturn($response); - $this->mockClient->createRequest( - 'GET', - Argument::type('Psr\Http\Message\UriInterface'), - Argument::type('array') - )->willReturn($this->mockRequest->reveal()); - - $handler = new Guzzle5HttpHandler($this->mockClient->reveal()); - $response = $handler($this->mockPsr7Request->reveal()); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals('Body Text', (string) $response->getBody()); - } - - public function testAsyncWithoutGuzzlePromiseThrowsException() - { - // Pretend the promise library doesn't exist - foreach (spl_autoload_functions() as $function) { - if ($function[0] instanceof ClassLoader) { - $newAutoloader = clone $function[0]; - $newAutoloader->setPsr4('GuzzleHttp\\Promise\\', '/tmp'); - spl_autoload_register($newAutoloadFunc = [$newAutoloader, 'loadClass']); - spl_autoload_unregister($previousAutoloadFunc = $function); - } - } - - $this->mockClient->send(Argument::type('GuzzleHttp\Message\RequestInterface')) - ->willReturn(new FutureResponse($this->mockFuture->reveal())); - $this->mockClient->createRequest('GET', Argument::type('Psr\Http\Message\UriInterface'), Argument::allOf( - Argument::withEntry('headers', []), - Argument::withEntry('future', true), - Argument::that(function ($arg) { - return $arg['body'] instanceof StreamInterface; - }) - ))->willReturn($this->mockRequest->reveal()); - - $handler = new Guzzle5HttpHandler($this->mockClient->reveal()); - $errorThrown = false; - try { - $handler->async($this->mockPsr7Request->reveal()); - } catch (Exception $e) { - $this->assertEquals( - 'Install guzzlehttp/promises to use async with Guzzle 5', - $e->getMessage() - ); - $errorThrown = true; - } - - // Restore autoloader before assertion (in case it fails) - spl_autoload_register($previousAutoloadFunc); - spl_autoload_unregister($newAutoloadFunc); - - $this->assertTrue($errorThrown); - } - - public function testSuccessfullySendsRequestAsync() - { - $response = new Response( - 200, - [], - Stream::factory('Body Text') - ); - $this->mockClient->send(Argument::type('GuzzleHttp\Message\RequestInterface')) - ->willReturn(new FutureResponse( - new CompletedFutureValue($response) - )); - $this->mockClient->createRequest('GET', Argument::type('Psr\Http\Message\UriInterface'), Argument::allOf( - Argument::withEntry('headers', []), - Argument::withEntry('future', true), - Argument::that(function ($arg) { - return $arg['body'] instanceof StreamInterface; - }) - ))->willReturn($this->mockRequest->reveal()); - - $handler = new Guzzle5HttpHandler($this->mockClient->reveal()); - $promise = $handler->async($this->mockPsr7Request->reveal()); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $promise->wait()); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals('Body Text', (string) $response->getBody()); - } - - /** - * @expectedException Exception - * @expectedExceptionMessage This is a test rejection message - */ - public function testPromiseHandlesException() - { - $this->mockClient->send(Argument::type('GuzzleHttp\Message\RequestInterface')) - ->willReturn(new FutureResponse( - (new CompletedFutureValue(new Response(200)))->then(function () { - throw new Exception('This is a test rejection message'); - }) - )); - $this->mockClient->createRequest('GET', Argument::type('Psr\Http\Message\UriInterface'), Argument::allOf( - Argument::withEntry('headers', []), - Argument::withEntry('future', true), - Argument::that(function ($arg) { - return $arg['body'] instanceof StreamInterface; - }) - ))->willReturn($this->mockRequest->reveal()); - - $handler = new Guzzle5HttpHandler($this->mockClient->reveal()); - $promise = $handler->async($this->mockPsr7Request->reveal()); - $promise->wait(); - } - - public function testCreateGuzzle5Request() - { - $requestHeaders = [ - 'header1' => 'value1', - 'header2' => 'value2', - ]; - $this->mockPsr7Request->getHeaders() - ->shouldBeCalledTimes(1) - ->willReturn($requestHeaders); - $mockBody = $this->prophesize('Psr\Http\Message\StreamInterface'); - $this->mockPsr7Request->getBody() - ->shouldBeCalledTimes(1) - ->willReturn($mockBody->reveal()); - - $mockGuzzleRequest = $this->prophesize('GuzzleHttp\Message\RequestInterface'); - $this->mockClient->createRequest( - 'GET', - Argument::type('Psr\Http\Message\UriInterface'), - [ - 'headers' => $requestHeaders + ['header3' => 'value3'], - 'body' => $mockBody->reveal(), - ] - )->shouldBeCalledTimes(1)->willReturn( - $mockGuzzleRequest->reveal() - ); - - $this->mockClient->send(Argument::type('GuzzleHttp\Message\RequestInterface')) - ->shouldBeCalledTimes(1) - ->willReturn($this->getGuzzle5ResponseMock()->reveal()); - - $handler = new Guzzle5HttpHandler($this->mockClient->reveal()); - $handler($this->mockPsr7Request->reveal(), [ - 'headers' => [ - 'header3' => 'value3' - ] - ]); - } - - private function getGuzzle5ResponseMock() - { - $responseMock = $this->prophesize('GuzzleHttp\Message\ResponseInterface'); - $responseMock->getStatusCode()->willReturn(200); - $responseMock->getHeaders()->willReturn([]); - $responseMock->getProtocolVersion()->willReturn(''); - $responseMock->getReasonPhrase()->willReturn(''); - - $res = $this->prophesize('GuzzleHttp\Stream\StreamInterface'); - $res->__toString()->willReturn(''); - $responseMock->getBody()->willReturn( - $res->reveal() - ); - - return $responseMock; - } -} diff --git a/tests/HttpHandler/Guzzle6HttpHandlerTest.php b/tests/HttpHandler/Guzzle6HttpHandlerTest.php deleted file mode 100644 index b86d94d68..000000000 --- a/tests/HttpHandler/Guzzle6HttpHandlerTest.php +++ /dev/null @@ -1,68 +0,0 @@ -onlyGuzzle6(); - - $this->client = $this->prophesize('GuzzleHttp\ClientInterface'); - $this->handler = new Guzzle6HttpHandler($this->client->reveal()); - } - - public function testSuccessfullySendsRequest() - { - $request = new Request('GET', 'https://domain.tld'); - $options = ['key' => 'value']; - $response = new Response(200); - - $this->client->send($request, $options)->willReturn($response); - - $handler = $this->handler; - - $this->assertSame($response, $handler($request, $options)); - } - - public function testSuccessfullySendsRequestAsync() - { - $request = new Request('GET', 'https://domain.tld'); - $options = ['key' => 'value']; - $response = new Response(200); - $promise = new FulfilledPromise($response); - - $this->client->sendAsync($request, $options)->willReturn($promise); - - $handler = $this->handler; - - $this->assertSame($response, $handler->async($request, $options)->wait()); - } -} diff --git a/tests/HttpHandler/Guzzle7HttpHandlerTest.php b/tests/HttpHandler/Guzzle7HttpHandlerTest.php deleted file mode 100644 index f6eaa6360..000000000 --- a/tests/HttpHandler/Guzzle7HttpHandlerTest.php +++ /dev/null @@ -1,34 +0,0 @@ -onlyGuzzle7(); - - $this->client = $this->prophesize('GuzzleHttp\ClientInterface'); - $this->handler = new Guzzle7HttpHandler($this->client->reveal()); - } -} diff --git a/tests/HttpHandler/HttpHandlerFactoryTest.php b/tests/HttpHandler/HttpHandlerFactoryTest.php deleted file mode 100644 index f0673f819..000000000 --- a/tests/HttpHandler/HttpHandlerFactoryTest.php +++ /dev/null @@ -1,52 +0,0 @@ -onlyGuzzle5(); - - HttpClientCache::setHttpClient(null); - $handler = HttpHandlerFactory::build(); - $this->assertInstanceOf('Google\Auth\HttpHandler\Guzzle5HttpHandler', $handler); - } - - public function testBuildsGuzzle6Handler() - { - $this->onlyGuzzle6(); - - HttpClientCache::setHttpClient(null); - $handler = HttpHandlerFactory::build(); - $this->assertInstanceOf('Google\Auth\HttpHandler\Guzzle6HttpHandler', $handler); - } - - public function testBuildsGuzzle7Handler() - { - $this->onlyGuzzle7(); - - HttpClientCache::setHttpClient(null); - $handler = HttpHandlerFactory::build(); - $this->assertInstanceOf('Google\Auth\HttpHandler\Guzzle7HttpHandler', $handler); - } -} diff --git a/tests/Middleware/AuthTokenMiddlewareTest.php b/tests/Middleware/AuthTokenMiddlewareTest.php deleted file mode 100644 index 8b6c24069..000000000 --- a/tests/Middleware/AuthTokenMiddlewareTest.php +++ /dev/null @@ -1,350 +0,0 @@ -onlyGuzzle6And7(); - - $this->mockFetcher = $this->prophesize('Google\Auth\FetchAuthTokenInterface'); - $this->mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $this->mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - $this->mockRequest = $this->prophesize('GuzzleHttp\Psr7\Request'); - } - - public function testOnlyTouchesWhenAuthConfigScoped() - { - $this->mockFetcher->fetchAuthToken(Argument::any()) - ->willReturn([]); - $this->mockRequest->withHeader()->shouldNotBeCalled(); - - $middleware = new AuthTokenMiddleware($this->mockFetcher->reveal()); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'not_google_auth']); - } - - public function testAddsTheTokenAsAnAuthorizationHeader() - { - $authResult = ['access_token' => '1/abcdef1234567890']; - $this->mockFetcher->fetchAuthToken(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($authResult); - $this->mockRequest->withHeader('authorization', 'Bearer ' . $authResult['access_token']) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockRequest->reveal()); - - // Run the test. - $middleware = new AuthTokenMiddleware($this->mockFetcher->reveal()); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'google_auth']); - } - - public function testDoesNotAddAnAuthorizationHeaderOnNoAccessToken() - { - $authResult = ['not_access_token' => '1/abcdef1234567890']; - $this->mockFetcher->fetchAuthToken(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($authResult); - $this->mockRequest->withHeader('authorization', 'Bearer ') - ->willReturn($this->mockRequest->reveal()); - - // Run the test. - $middleware = new AuthTokenMiddleware($this->mockFetcher->reveal()); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'google_auth']); - } - - public function testUsesIdTokenWhenAccessTokenDoesNotExist() - { - $token = 'idtoken12345'; - $authResult = ['id_token' => $token]; - $this->mockFetcher->fetchAuthToken(Argument::any()) - ->willReturn($authResult); - $this->mockRequest->withHeader('authorization', 'Bearer ' . $token) - ->willReturn($this->mockRequest); - - $middleware = new AuthTokenMiddleware($this->mockFetcher->reveal()); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'google_auth']); - } - - public function testUsesCachedAccessToken() - { - $cacheKey = 'myKey'; - $accessToken = '2/abcdef1234567890'; - $cachedValue = ['access_token' => $accessToken]; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($cacheKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->fetchAuthToken() - ->shouldNotBeCalled(); - $this->mockFetcher->getCacheKey() - ->shouldBeCalled() - ->willReturn($cacheKey); - $this->mockRequest->withHeader('authorization', 'Bearer ' . $accessToken) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockRequest->reveal()); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - null, - $this->mockCache->reveal() - ); - $middleware = new AuthTokenMiddleware($cachedFetcher); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'google_auth']); - } - - public function testUsesCachedIdToken() - { - $cacheKey = 'myKey'; - $idToken = '2/abcdef1234567890'; - $cachedValue = ['id_token' => $idToken]; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($cacheKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->fetchAuthToken() - ->shouldNotBeCalled(); - $this->mockFetcher->getCacheKey() - ->shouldBeCalled() - ->willReturn($cacheKey); - $this->mockRequest->withHeader('authorization', 'Bearer ' . $idToken) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockRequest->reveal()); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - null, - $this->mockCache->reveal() - ); - $middleware = new AuthTokenMiddleware($cachedFetcher); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'google_auth']); - } - - public function testGetsCachedAuthTokenUsingCacheOptions() - { - $prefix = 'test_prefix_'; - $cacheKey = 'myKey'; - $token = '2/abcdef1234567890'; - $cachedValue = ['access_token' => $token]; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($prefix . $cacheKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->fetchAuthToken() - ->shouldNotBeCalled(); - $this->mockFetcher->getCacheKey() - ->shouldBeCalled() - ->willReturn($cacheKey); - $this->mockRequest->withHeader('authorization', 'Bearer ' . $token) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockRequest->reveal()); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - ['prefix' => $prefix], - $this->mockCache->reveal() - ); - $middleware = new AuthTokenMiddleware($cachedFetcher); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'google_auth']); - } - - public function testShouldSaveValueInCacheWithSpecifiedPrefix() - { - $prefix = 'test_prefix_'; - $lifetime = '70707'; - $cacheKey = 'myKey'; - $token = '1/abcdef1234567890'; - $cachedValue = ['access_token' => $token]; - $this->mockCacheItem->get() - ->willReturn(null); - $this->mockCacheItem->isHit() - ->willReturn(false); - $this->mockCacheItem->set($cachedValue) - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCacheItem->expiresAfter($lifetime) - ->shouldBeCalledTimes(1); - $this->mockCache->getItem($prefix . $cacheKey) - ->shouldBeCalled() - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->shouldBeCalled(); - $this->mockFetcher->getCacheKey() - ->shouldBeCalled() - ->willReturn($cacheKey); - $this->mockFetcher->fetchAuthToken(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockRequest->withHeader('authorization', 'Bearer ' . $token) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockRequest->reveal()); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - ['prefix' => $prefix, 'lifetime' => $lifetime], - $this->mockCache->reveal() - ); - $middleware = new AuthTokenMiddleware($cachedFetcher); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'google_auth']); - } - - /** - * @dataProvider provideShouldNotifyTokenCallback - */ - public function testShouldNotifyTokenCallback(callable $tokenCallback) - { - $prefix = 'test_prefix_'; - $cacheKey = 'myKey'; - $token = '1/abcdef1234567890'; - $cachedValue = ['access_token' => $token]; - $this->mockCacheItem->get() - ->willReturn(null); - $this->mockCacheItem->isHit() - ->willReturn(false); - $this->mockCacheItem->set($cachedValue) - ->shouldBeCalled(); - $this->mockCacheItem->expiresAfter(Argument::any()) - ->shouldBeCalled(); - $this->mockCache->getItem($prefix . $cacheKey) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->shouldBeCalled(); - $this->mockFetcher->getCacheKey() - ->willReturn($cacheKey); - $this->mockFetcher->fetchAuthToken(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockRequest->withHeader(Argument::any(), Argument::any()) - ->willReturn($this->mockRequest->reveal()); - - MiddlewareCallback::$expectedKey = $this->getValidKeyName($prefix . $cacheKey); - MiddlewareCallback::$expectedValue = $token; - MiddlewareCallback::$called = false; - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - ['prefix' => $prefix], - $this->mockCache->reveal() - ); - $middleware = new AuthTokenMiddleware( - $cachedFetcher, - null, - $tokenCallback - ); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'google_auth']); - $this->assertTrue(MiddlewareCallback::$called); - } - - public function provideShouldNotifyTokenCallback() - { - MiddlewareCallback::$phpunit = $this; - $anonymousFunc = function ($key, $value) { - MiddlewareCallback::staticInvoke($key, $value); - }; - return [ - ['Google\Auth\Tests\Middleware\MiddlewareCallbackFunction'], - ['Google\Auth\Tests\Middleware\MiddlewareCallback::staticInvoke'], - [['Google\Auth\Tests\Middleware\MiddlewareCallback', 'staticInvoke']], - [$anonymousFunc], - [[new MiddlewareCallback, 'staticInvoke']], - [[new MiddlewareCallback, 'methodInvoke']], - [new MiddlewareCallback], - ]; - } -} - -class MiddlewareCallback -{ - public static $phpunit; - public static $expectedKey; - public static $expectedValue; - public static $called = false; - - public function __invoke($key, $value) - { - self::$phpunit->assertEquals(self::$expectedKey, $key); - self::$phpunit->assertEquals(self::$expectedValue, $value); - self::$called = true; - } - - public function methodInvoke($key, $value) - { - return $this($key, $value); - } - - public static function staticInvoke($key, $value) - { - $instance = new self(); - return $instance($key, $value); - } -} - -function MiddlewareCallbackFunction($key, $value) -{ - return MiddlewareCallback::staticInvoke($key, $value); -} diff --git a/tests/Middleware/ScopedAccessTokenMiddlewareTest.php b/tests/Middleware/ScopedAccessTokenMiddlewareTest.php deleted file mode 100644 index 174efbf11..000000000 --- a/tests/Middleware/ScopedAccessTokenMiddlewareTest.php +++ /dev/null @@ -1,221 +0,0 @@ -onlyGuzzle6And7(); - - $this->mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $this->mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - $this->mockRequest = $this->prophesize('GuzzleHttp\Psr7\Request'); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testRequiresScopeAsAStringOrArray() - { - $fakeAuthFunc = function ($unused_scopes) { - return '1/abcdef1234567890'; - }; - new ScopedAccessTokenMiddleware($fakeAuthFunc, new \stdClass()); - } - - public function testAddsTheTokenAsAnAuthorizationHeader() - { - $token = '1/abcdef1234567890'; - $fakeAuthFunc = function ($unused_scopes) use ($token) { - return $token; - }; - $this->mockRequest->withHeader('authorization', 'Bearer ' . $token) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockRequest->reveal()); - - // Run the test - $middleware = new ScopedAccessTokenMiddleware($fakeAuthFunc, self::TEST_SCOPE); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'scoped']); - } - - public function testUsesCachedAuthToken() - { - $cachedValue = '2/abcdef1234567890'; - $fakeAuthFunc = function ($unused_scopes) { - return ''; - }; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($this->getValidKeyName(self::TEST_SCOPE)) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockRequest->withHeader('authorization', 'Bearer ' . $cachedValue) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockRequest->reveal()); - - // Run the test - $middleware = new ScopedAccessTokenMiddleware( - $fakeAuthFunc, - self::TEST_SCOPE, - [], - $this->mockCache->reveal() - ); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'scoped']); - } - - public function testGetsCachedAuthTokenUsingCachePrefix() - { - $prefix = 'test_prefix_'; - $cachedValue = '2/abcdef1234567890'; - $fakeAuthFunc = function ($unused_scopes) { - return ''; - }; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($prefix . $this->getValidKeyName(self::TEST_SCOPE)) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockRequest->withHeader('authorization', 'Bearer ' . $cachedValue) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockRequest->reveal()); - - // Run the test - $middleware = new ScopedAccessTokenMiddleware( - $fakeAuthFunc, - self::TEST_SCOPE, - ['prefix' => $prefix], - $this->mockCache->reveal() - ); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'scoped']); - } - - public function testShouldSaveValueInCache() - { - $token = '2/abcdef1234567890'; - $fakeAuthFunc = function ($unused_scopes) use ($token) { - return $token; - }; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCacheItem->set($token) - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCacheItem->expiresAfter(Argument::any()) - ->shouldBeCalledTimes(1); - $this->mockCache->getItem($this->getValidKeyName(self::TEST_SCOPE)) - ->shouldBeCalledTimes(2) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->shouldBeCalled() - ->willReturn(true); - $this->mockRequest->withHeader('authorization', 'Bearer ' . $token) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockRequest->reveal()); - - // Run the test - $middleware = new ScopedAccessTokenMiddleware( - $fakeAuthFunc, - self::TEST_SCOPE, - [], - $this->mockCache->reveal() - ); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'scoped']); - } - - public function testShouldSaveValueInCacheWithCacheOptions() - { - $token = '2/abcdef1234567890'; - $prefix = 'test_prefix_'; - $lifetime = '70707'; - $fakeAuthFunc = function ($unused_scopes) use ($token) { - return $token; - }; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCacheItem->set($token) - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCacheItem->expiresAfter($lifetime) - ->shouldBeCalledTimes(1); - $this->mockCache->getItem($prefix . $this->getValidKeyName(self::TEST_SCOPE)) - ->shouldBeCalledTimes(2) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->shouldBeCalled() - ->willReturn(true); - $this->mockRequest->withHeader('authorization', 'Bearer ' . $token) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockRequest->reveal()); - - // Run the test - $middleware = new ScopedAccessTokenMiddleware( - $fakeAuthFunc, - self::TEST_SCOPE, - ['prefix' => $prefix, 'lifetime' => $lifetime], - $this->mockCache->reveal() - ); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'scoped']); - } - - public function testOnlyTouchesWhenAuthConfigScoped() - { - $fakeAuthFunc = function ($unused_scopes) { - return '1/abcdef1234567890'; - }; - $this->mockRequest->withHeader()->shouldNotBeCalled(); - - // Run the test - $middleware = new ScopedAccessTokenMiddleware($fakeAuthFunc, self::TEST_SCOPE); - $mock = new MockHandler([new Response(200)]); - $callable = $middleware($mock); - $callable($this->mockRequest->reveal(), ['auth' => 'not_scoped']); - } -} diff --git a/tests/Middleware/SimpleMiddlewareTest.php b/tests/Middleware/SimpleMiddlewareTest.php deleted file mode 100644 index ab34ff739..000000000 --- a/tests/Middleware/SimpleMiddlewareTest.php +++ /dev/null @@ -1,39 +0,0 @@ -onlyGuzzle6And7(); - - $this->mockRequest = $this->prophesize('GuzzleHttp\Psr7\Request'); - } - - public function testTest() - { - } -} diff --git a/tests/Subscriber/AuthTokenSubscriberTest.php b/tests/Subscriber/AuthTokenSubscriberTest.php deleted file mode 100644 index ace685960..000000000 --- a/tests/Subscriber/AuthTokenSubscriberTest.php +++ /dev/null @@ -1,335 +0,0 @@ -onlyGuzzle5(); - - $this->mockFetcher = $this->prophesize('Google\Auth\FetchAuthTokenInterface'); - $this->mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $this->mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - } - - public function testSubscribesToEvents() - { - $a = new AuthTokenSubscriber($this->mockFetcher->reveal()); - $this->assertArrayHasKey('before', $a->getEvents()); - } - - public function testOnlyTouchesWhenAuthConfigScoped() - { - $s = new AuthTokenSubscriber($this->mockFetcher->reveal()); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'not_google_auth'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $s->onBefore($before); - $this->assertSame($request->getHeader('authorization'), ''); - } - - public function testAddsTheTokenAsAnAuthorizationHeader() - { - $authResult = ['access_token' => '1/abcdef1234567890']; - $this->mockFetcher->fetchAuthToken(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($authResult); - - // Run the test. - $a = new AuthTokenSubscriber($this->mockFetcher->reveal()); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'google_auth'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $a->onBefore($before); - $this->assertSame( - $request->getHeader('authorization'), - 'Bearer 1/abcdef1234567890' - ); - } - - public function testDoesNotAddAnAuthorizationHeaderOnNoAccessToken() - { - $authResult = ['not_access_token' => '1/abcdef1234567890']; - $this->mockFetcher->fetchAuthToken(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($authResult); - - // Run the test. - $a = new AuthTokenSubscriber($this->mockFetcher->reveal()); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'google_auth'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $a->onBefore($before); - $this->assertSame($request->getHeader('authorization'), ''); - } - - public function testUsesCachedAuthToken() - { - $cacheKey = 'myKey'; - $token = '2/abcdef1234567890'; - $cachedValue = ['access_token' => $token]; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($cacheKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->fetchAuthToken() - ->shouldNotBeCalled(); - $this->mockFetcher->getCacheKey() - ->willReturn($cacheKey); - - // Run the test. - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - null, - $this->mockCache->reveal() - ); - $a = new AuthTokenSubscriber($cachedFetcher); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'google_auth'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $a->onBefore($before); - $this->assertSame( - $request->getHeader('authorization'), - 'Bearer ' . $token - ); - } - - public function testGetsCachedAuthTokenUsingCachePrefix() - { - $prefix = 'test_prefix_'; - $cacheKey = 'myKey'; - $token = '2/abcdef1234567890'; - $cachedValue = ['access_token' => $token]; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($prefix . $cacheKey) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockFetcher->fetchAuthToken() - ->shouldNotBeCalled(); - $this->mockFetcher->getCacheKey() - ->willReturn($cacheKey); - - // Run the test - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - ['prefix' => $prefix], - $this->mockCache->reveal() - ); - $a = new AuthTokenSubscriber($cachedFetcher); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'google_auth'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $a->onBefore($before); - $this->assertSame( - $request->getHeader('authorization'), - 'Bearer ' . $token - ); - } - - public function testShouldSaveValueInCacheWithCacheOptions() - { - $prefix = 'test_prefix_'; - $lifetime = '70707'; - $cacheKey = 'myKey'; - $token = '2/abcdef1234567890'; - $cachedValue = ['access_token' => $token]; - $this->mockCacheItem->get() - ->willReturn(null); - $this->mockCacheItem->set($cachedValue) - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCacheItem->isHit() - ->willReturn(false); - $this->mockCacheItem->expiresAfter($lifetime) - ->shouldBeCalledTimes(1); - $this->mockCache->getItem($prefix . $cacheKey) - ->shouldBeCalledTimes(2) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->willReturn(null); - $this->mockFetcher->getCacheKey() - ->willReturn($cacheKey); - $this->mockFetcher->fetchAuthToken(Argument::any()) - ->willReturn($cachedValue); - - // Run the test - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - ['prefix' => $prefix, 'lifetime' => $lifetime], - $this->mockCache->reveal() - ); - $a = new AuthTokenSubscriber($cachedFetcher); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'google_auth'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $a->onBefore($before); - $this->assertSame( - $request->getHeader('authorization'), - 'Bearer ' . $token - ); - } - - /** - * @dataProvider provideShouldNotifyTokenCallback - */ - public function testShouldNotifyTokenCallback(callable $tokenCallback) - { - $prefix = 'test_prefix_'; - $cacheKey = 'myKey'; - $token = '1/abcdef1234567890'; - $cachedValue = ['access_token' => $token]; - $this->mockCacheItem->get() - ->willReturn(null); - $this->mockCacheItem->isHit() - ->willReturn(false); - $this->mockCacheItem->set($cachedValue) - ->willReturn(false); - $this->mockCacheItem->expiresAfter(Argument::any()) - ->willReturn(null); - $this->mockCache->getItem($prefix . $cacheKey) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->willReturn(null); - $this->mockFetcher->getCacheKey() - ->willReturn($cacheKey); - $this->mockFetcher->fetchAuthToken(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - - SubscriberCallback::$expectedKey = $this->getValidKeyName($prefix . $cacheKey); - SubscriberCallback::$expectedValue = $token; - SubscriberCallback::$called = false; - - // Run the test - $cachedFetcher = new FetchAuthTokenCache( - $this->mockFetcher->reveal(), - ['prefix' => $prefix], - $this->mockCache->reveal() - ); - $a = new AuthTokenSubscriber( - $cachedFetcher, - null, - $tokenCallback - ); - - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'google_auth'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $a->onBefore($before); - $this->assertTrue(SubscriberCallback::$called); - } - - public function provideShouldNotifyTokenCallback() - { - SubscriberCallback::$phpunit = $this; - $anonymousFunc = function ($key, $value) { - SubscriberCallback::staticInvoke($key, $value); - }; - return [ - ['Google\Auth\Tests\Subscriber\SubscriberCallbackFunction'], - ['Google\Auth\Tests\Subscriber\SubscriberCallback::staticInvoke'], - [['Google\Auth\Tests\Subscriber\SubscriberCallback', 'staticInvoke']], - [$anonymousFunc], - [[new SubscriberCallback, 'staticInvoke']], - [[new SubscriberCallback, 'methodInvoke']], - [new SubscriberCallback], - ]; - } -} - -class SubscriberCallback -{ - public static $phpunit; - public static $expectedKey; - public static $expectedValue; - public static $called = false; - - public function __invoke($key, $value) - { - self::$phpunit->assertEquals(self::$expectedKey, $key); - self::$phpunit->assertEquals(self::$expectedValue, $value); - self::$called = true; - } - - public function methodInvoke($key, $value) - { - return $this($key, $value); - } - - public static function staticInvoke($key, $value) - { - $instance = new self(); - return $instance($key, $value); - } -} - -function SubscriberCallbackFunction($key, $value) -{ - return SubscriberCallback::staticInvoke($key, $value); -} diff --git a/tests/Subscriber/ScopedAccessTokenSubscriberTest.php b/tests/Subscriber/ScopedAccessTokenSubscriberTest.php deleted file mode 100644 index e64e22508..000000000 --- a/tests/Subscriber/ScopedAccessTokenSubscriberTest.php +++ /dev/null @@ -1,256 +0,0 @@ -onlyGuzzle5(); - - $this->mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $this->mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - $this->mockRequest = $this->prophesize('GuzzleHttp\Psr7\Request'); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testRequiresScopeAsAStringOrArray() - { - $fakeAuthFunc = function ($unused_scopes) { - return '1/abcdef1234567890'; - }; - new ScopedAccessTokenSubscriber($fakeAuthFunc, new \stdClass(), array()); - } - - public function testSubscribesToEvents() - { - $fakeAuthFunc = function ($unused_scopes) { - return '1/abcdef1234567890'; - }; - $s = new ScopedAccessTokenSubscriber($fakeAuthFunc, self::TEST_SCOPE, array()); - $this->assertArrayHasKey('before', $s->getEvents()); - } - - public function testAddsTheTokenAsAnAuthorizationHeader() - { - $fakeAuthFunc = function ($unused_scopes) { - return '1/abcdef1234567890'; - }; - $s = new ScopedAccessTokenSubscriber($fakeAuthFunc, self::TEST_SCOPE, array()); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'scoped'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $s->onBefore($before); - $this->assertSame( - 'Bearer 1/abcdef1234567890', - $request->getHeader('authorization') - ); - } - - public function testUsesCachedAuthToken() - { - $cachedValue = '2/abcdef1234567890'; - $fakeAuthFunc = function ($unused_scopes) { - return ''; - }; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($this->getValidKeyName(self::TEST_SCOPE)) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - - // Run the test - $s = new ScopedAccessTokenSubscriber( - $fakeAuthFunc, - self::TEST_SCOPE, - [], - $this->mockCache->reveal() - ); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'scoped'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $s->onBefore($before); - $this->assertSame( - 'Bearer 2/abcdef1234567890', - $request->getHeader('authorization') - ); - } - - public function testGetsCachedAuthTokenUsingCachePrefix() - { - $prefix = 'test_prefix_'; - $cachedValue = '2/abcdef1234567890'; - $fakeAuthFunc = function ($unused_scopes) { - return ''; - }; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(true); - $this->mockCacheItem->get() - ->shouldBeCalledTimes(1) - ->willReturn($cachedValue); - $this->mockCache->getItem($prefix . $this->getValidKeyName(self::TEST_SCOPE)) - ->shouldBeCalledTimes(1) - ->willReturn($this->mockCacheItem->reveal()); - - // Run the test - $s = new ScopedAccessTokenSubscriber( - $fakeAuthFunc, - self::TEST_SCOPE, - ['prefix' => $prefix], - $this->mockCache->reveal() - ); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'scoped'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $s->onBefore($before); - $this->assertSame( - 'Bearer 2/abcdef1234567890', - $request->getHeader('authorization') - ); - } - - public function testShouldSaveValueInCache() - { - $token = '2/abcdef1234567890'; - $fakeAuthFunc = function ($unused_scopes) { - return '2/abcdef1234567890'; - }; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCacheItem->set($token) - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCacheItem->expiresAfter(Argument::any()) - ->shouldBeCalledTimes(1); - $this->mockCache->getItem($this->getValidKeyName(self::TEST_SCOPE)) - ->shouldBeCalledTimes(2) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->shouldBeCalledTimes(1); - - $s = new ScopedAccessTokenSubscriber( - $fakeAuthFunc, - self::TEST_SCOPE, - [], - $this->mockCache->reveal() - ); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'scoped'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $s->onBefore($before); - $this->assertSame( - 'Bearer 2/abcdef1234567890', - $request->getHeader('authorization') - ); - } - - public function testShouldSaveValueInCacheWithCacheOptions() - { - $token = '2/abcdef1234567890'; - $prefix = 'test_prefix_'; - $lifetime = '70707'; - $fakeAuthFunc = function ($unused_scopes) { - return '2/abcdef1234567890'; - }; - $this->mockCacheItem->isHit() - ->shouldBeCalledTimes(1) - ->willReturn(false); - $this->mockCacheItem->set($token) - ->shouldBeCalledTimes(1); - $this->mockCacheItem->expiresAfter($lifetime) - ->shouldBeCalledTimes(1); - $this->mockCache->getItem($prefix . $this->getValidKeyName(self::TEST_SCOPE)) - ->willReturn($this->mockCacheItem->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) - ->shouldBeCalledTimes(1); - - // Run the test - $s = new ScopedAccessTokenSubscriber( - $fakeAuthFunc, - self::TEST_SCOPE, - ['prefix' => $prefix, 'lifetime' => $lifetime], - $this->mockCache->reveal() - ); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'scoped'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $s->onBefore($before); - $this->assertSame( - 'Bearer 2/abcdef1234567890', - $request->getHeader('authorization') - ); - } - - public function testOnlyTouchesWhenAuthConfigScoped() - { - $fakeAuthFunc = function ($unused_scopes) { - return '1/abcdef1234567890'; - }; - $s = new ScopedAccessTokenSubscriber($fakeAuthFunc, self::TEST_SCOPE, []); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'notscoped'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $s->onBefore($before); - $this->assertSame('', $request->getHeader('authorization')); - } -} diff --git a/tests/Subscriber/SimpleSubscriberTest.php b/tests/Subscriber/SimpleSubscriberTest.php deleted file mode 100644 index 2cb7abf7e..000000000 --- a/tests/Subscriber/SimpleSubscriberTest.php +++ /dev/null @@ -1,76 +0,0 @@ -onlyGuzzle5(); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testRequiresADeveloperKey() - { - new SimpleSubscriber(['not_key' => 'a test key']); - } - - public function testSubscribesToEvents() - { - $events = (new SimpleSubscriber(['key' => 'a test key']))->getEvents(); - $this->assertArrayHasKey('before', $events); - } - - public function testAddsTheKeyToTheQuery() - { - $s = new SimpleSubscriber(['key' => 'test_key']); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'simple'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $s->onBefore($before); - $this->assertCount(1, $request->getQuery()); - $this->assertTrue($request->getQuery()->hasKey('key')); - $this->assertSame($request->getQuery()->get('key'), 'test_key'); - } - - public function testOnlyTouchesWhenAuthConfigIsSimple() - { - $s = new SimpleSubscriber(['key' => 'test_key']); - $client = new Client(); - $request = $client->createRequest( - 'GET', - 'http://testing.org', - ['auth' => 'notsimple'] - ); - $before = new BeforeEvent(new Transaction($client, $request)); - $s->onBefore($before); - $this->assertCount(0, $request->getQuery()); - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d2e9b26d0..079a3475a 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -19,34 +19,46 @@ require dirname(__DIR__) . '/vendor/autoload.php'; date_default_timezone_set('UTC'); -function buildResponse($code, array $headers = [], $body = null) +use Google\Http\PromiseInterface; +use Google\Http\ClientInterface; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +function httpClientWithResponses(array $mockResponses = []) { - if (class_exists('GuzzleHttp\HandlerStack')) { - return new \GuzzleHttp\Psr7\Response($code, $headers, $body); - } - - return new \GuzzleHttp\Message\Response( - $code, - $headers, - \GuzzleHttp\Stream\Stream::factory((string)$body) - ); + $mock = new \GuzzleHttp\Handler\MockHandler($mockResponses); + + $handler = \GuzzleHttp\HandlerStack::create($mock); + $client = new \GuzzleHttp\Client(['handler' => $handler]); + + return new \Google\Http\Client\GuzzleClient($client); } -function getHandler(array $mockResponses = []) +function httpClientFromCallable(callable $httpHandler): ClientInterface { - if (class_exists('GuzzleHttp\HandlerStack')) { - $mock = new \GuzzleHttp\Handler\MockHandler($mockResponses); - - $handler = \GuzzleHttp\HandlerStack::create($mock); - $client = new \GuzzleHttp\Client(['handler' => $handler]); + return new class($httpHandler) implements ClientInterface { + private $httpHandler; - return new \Google\Auth\HttpHandler\Guzzle6HttpHandler($client); - } + public function __construct(callable $httpHandler) + { + $this->httpHandler = $httpHandler; + } - $client = new \GuzzleHttp\Client(); - $client->getEmitter()->attach( - new \GuzzleHttp\Subscriber\Mock($mockResponses) - ); + public function send( + RequestInterface $request, + array $options = [] + ) : ResponseInterface + { + $httpHandler = $this->httpHandler; + return $httpHandler($request); + } - return new \Google\Auth\HttpHandler\Guzzle5HttpHandler($client); + public function sendAsync( + RequestInterface $request, + array $options = [] + ) : PromiseInterface + { + // no op + } + }; } diff --git a/tests/fixtures2/gcloud.json b/tests/fixtures2/gcloud.json deleted file mode 100644 index 8f210b4a4..000000000 --- a/tests/fixtures2/gcloud.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com", - "client_secret": "dummy_client_secret", - "refresh_token": "dummy_refresh_token", - "type": "authorized_user" -} diff --git a/tests/fixtures2/valid_oauth_creds.json b/tests/fixtures2/valid_oauth_creds.json deleted file mode 100644 index 338c645da..000000000 --- a/tests/fixtures2/valid_oauth_creds.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "valid.apps.googleusercontent.com", - "client_secret": "dummy_client_secret", - "refresh_token": "dummy_refresh_token", - "type": "authorized_user" -} diff --git a/tests/fixtures3/key.pub b/tests/fixtures3/key.pub deleted file mode 100644 index 745ae9e09..000000000 --- a/tests/fixtures3/key.pub +++ /dev/null @@ -1,6 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGhw1WMos5gp2YjV7+fNwXN1tI4/ -DFXKzwY6TDWsPxkbyfjHgunX/sijlnJt3Qs1gBxiwEEjzFFlp39O3/gEbIoYWHR/ -4sZdqNRFzbhJcTpnUvRlZDBLE5h8f5uu4aL4D32WyiELF/vpr533lZCBwWsnN3zI -YJxThgRF9i/R7F8tAgMBAAE= ------END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/fixtures3/service_account_credentials.json b/tests/fixtures3/service_account_credentials.json deleted file mode 100644 index 30499df62..000000000 --- a/tests/fixtures3/service_account_credentials.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "service_account", - "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", - "client_email": "testing@example.com" -} \ No newline at end of file diff --git a/tests/mocks/AppIdentityService.php b/tests/mocks/AppIdentityService.php deleted file mode 100644 index 8d73238be..000000000 --- a/tests/mocks/AppIdentityService.php +++ /dev/null @@ -1,38 +0,0 @@ - 'xyz', - 'expiration_time' => '2147483646', - ]; - public static $serviceAccountName; - public static $applicationId; - - public static function getAccessToken($scope) - { - self::$scope = $scope; - - return self::$accessToken; - } - - public static function signForApp($stringToSign) - { - return [ - 'signature' => 'Signed: ' . $stringToSign - ]; - } - - public static function getServiceAccountName() - { - return self::$serviceAccountName; - } - - public static function getApplicationId() - { - return self::$applicationId; - } -}