From ecc069d757170ee2dab288e048b0c8299b7bd8eb Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 11 Feb 2021 10:40:23 -0800 Subject: [PATCH] adds jwt abstraction --- .gitignore | 1 + UPGRADING.md | 21 ++ src/Auth/GoogleAuth.php | 132 +++++++----- src/Auth/Jwt/FirebaseJwtClient.php | 54 +++++ .../Jwt/JwtClientInterface.php} | 23 +- src/Auth/OAuth2.php | 199 ++---------------- src/HttpHandler/Guzzle6HttpHandler.php | 62 ------ src/HttpHandler/HttpHandlerFactory.php | 53 ----- tests/GoogleAuthTest.php | 74 ++++--- tests/bootstrap.php | 47 ++--- 10 files changed, 252 insertions(+), 414 deletions(-) create mode 100644 src/Auth/Jwt/FirebaseJwtClient.php rename src/{HttpHandler/Guzzle7HttpHandler.php => Auth/Jwt/JwtClientInterface.php} (56%) delete mode 100644 src/HttpHandler/Guzzle6HttpHandler.php delete mode 100644 src/HttpHandler/HttpHandlerFactory.php 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/UPGRADING.md b/UPGRADING.md index 333c0c340..02a75ab67 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -64,6 +64,27 @@ $httpClient = new Google\Http\Client\GuzzleClient($guzzle); $auth = new GoogleAuth(['httpClient' => $httpClient]); ``` +#### Improved JWT handling + +* Provides an abstraction from `firebase/jwt`, `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**: Provide documentation on how to use a different library +* Adds `JwtClientInterface` and `FirebaseJwtClient` + +**Example** + +```php +$jwt = new class implements Google\Auth\Jwt\JwtClientInterface { + public function encode($payload, $signingKey, $signingAlg, $keyId) { + // encode method + } + + // ... other JWT hander interface methods go here ... +}; +$googleAuth = new GoogleAuth(['jwtClient' => $jwt]); +$googleAuth->verify($someJwt); +``` + #### New `GoogleAuth` class `GoogleAuth` replaces `ApplicationDefaultCredentials`, and provides a diff --git a/src/Auth/GoogleAuth.php b/src/Auth/GoogleAuth.php index 421b1294c..037570876 100644 --- a/src/Auth/GoogleAuth.php +++ b/src/Auth/GoogleAuth.php @@ -20,17 +20,22 @@ namespace Google\Auth; use DomainException; +use Firebase\JWT\JWT; +use Firebase\JWT\JWK; use Google\Auth\Credentials\ComputeCredentials; use Google\Auth\Credentials\ServiceAccountCredentials; use Google\Auth\Credentials\ServiceAccountJwtAccessCredentials; use Google\Auth\Credentials\CredentialsInterface; use Google\Auth\Credentials\UserRefreshCredentials; use Google\Auth\Http\ClientFactory; +use Google\Auth\Jwt\FirebaseJwtClient; +use Google\Auth\Jwt\JwtClientInterface; use Google\Cache\MemoryCacheItemPool; use GuzzleHttp\Psr7\Request; use InvalidArgumentException; use Psr\Cache\CacheItemPoolInterface; use RuntimeException; +use UnexpectedValueException; /** * GoogleAuth obtains the default credentials for @@ -73,7 +78,7 @@ class GoogleAuth { private const TOKEN_REVOKE_URI = 'https://oauth2.googleapis.com/revoke'; private const OIDC_CERT_URI = 'https://www.googleapis.com/oauth2/v3/certs'; - private const OIDC_ISSUERS = ['accounts.google.com', 'https://accounts.google.com']; + private const OIDC_ISSUERS = ['http://accounts.google.com', 'https://accounts.google.com']; private const IAP_JWK_URI = 'https://www.gstatic.com/iap/verify/public_key-jwk'; private const IAP_ISSUERS = ['https://cloud.google.com/iap']; @@ -81,12 +86,11 @@ class GoogleAuth private const WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json'; private const NON_WINDOWS_WELL_KNOWN_PATH_BASE = '.config'; - private const ON_COMPUTE_CACHE_KEY = 'google_auth_on_gce_cache'; - - private $httpClient; private $cache; private $cacheLifetime; private $cachePrefix; + private $httpClient; + private $jwtClient; /** * Obtains an AuthTokenMiddleware which will fetch an access token to use in @@ -98,6 +102,7 @@ class GoogleAuth * * @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 @@ -107,15 +112,18 @@ class GoogleAuth public function __construct(array $options = []) { $options += [ - 'httpClient' => null, 'cache' => null, 'cacheLifetime' => 1500, 'cachePrefix' => '', + 'httpClient' => null, + 'jwtClient' => null, ]; - $this->httpClient = $options['httpClient'] ?: ClientFactory::build(); $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()); } /** @@ -233,13 +241,15 @@ public function makeCredentials(array $options = []): CredentialsInterface * Determines if this a GCE instance, by accessing the expected metadata * host. * + * @param array $options [optional] Configuration options. + * @param string $options.cacheKey cache key used for caching the result + * * @return bool */ - public function onCompute(): bool + public function onCompute(?array $options): bool { - $cacheItem = $this->cache->getItem( - $this->cachePrefix . self::ON_COMPUTE_CACHE_KEY - ); + $cacheKey = $options['cacheKey'] ?? 'google_auth_on_gce_cache'; + $cacheItem = $this->cache->getItem($this->cachePrefix . $cacheKey); if ($cacheItem->isHit()) { return $cacheItem->get(); @@ -257,23 +267,34 @@ public function onCompute(): bool * @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 $certsLocation URI for JSON certificate array conforming to + * @param array $options.issuers The intended issuers 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). */ - public function verify(string $token, array $options = []): array + public function verify(string $token, ?array $options): bool { - $location = isset($options['certsLocation']) - ? $options['certsLocation'] - : self::OIDC_CERT_URI; + $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); + $issuers = $options['issuers'] ?? + ['RS256' => self::OIDC_ISSUERS, 'ES256' => self::IAP_ISSUERS][$alg]; + $aud = $options['audience'] ?? null; - $cacheKey = isset($options['cacheKey']) - ? $options['cacheKey'] - : $this->getCacheKeyFromCertLocation($location); + $keys = $this->jwtClient->parseKeySet($certs); + $payload = $this->jwtClient->decode($token, $keys, [$alg]); - $certs = $this->getCerts($location, $cacheKey); - $oauth = new OAuth2(); - return $oauth->verify($token, $certs, $options); + if (empty($payload['iss']) || !in_array($payload['iss'], $issuers)) { + throw new UnexpectedValueException('Issuer does not match'); + } + + if ($aud && isset($payload['aud']) && $payload['aud'] != $aud) { + throw new UnexpectedValueException('Audience does not match'); + } + + return true; } /** @@ -286,8 +307,9 @@ public function verify(string $token, array $options = []): array * @return array * @throws InvalidArgumentException If received certs are in an invalid format. */ - private function getCerts(string $location, string $cacheKey): array { - $cacheItem = $this->cache->getItem($cacheKey); + private function getCerts(string $location, string $cacheKey): array + { + $cacheItem = $this->cache->getItem($this->cachePrefix . $cacheKey); $certs = $cacheItem ? $cacheItem->get() : null; $gotNewCerts = false; @@ -298,11 +320,6 @@ private function getCerts(string $location, string $cacheKey): array { } if (!isset($certs['keys'])) { - if ($location !== self::IAP_JWK_URI) { - throw new InvalidArgumentException( - 'federated sign-on certs expects "keys" to be set' - ); - } throw new InvalidArgumentException( 'certs expects "keys" to be set' ); @@ -316,7 +333,40 @@ private function getCerts(string $location, string $cacheKey): array { $this->cache->save($cacheItem); } - return $certs['keys']; + 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; } /** @@ -353,22 +403,6 @@ private function retrieveCertsFromLocation(string $url): array ), $response->getStatusCode()); } - /** - * 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::OIDC_CERT_URI - ? 'federated_signon_certs_v3' - : sha1($certsLocation); - - return 'google_auth_certs_cache|' . $key; - } - /** * Revoke an OAuth2 access token or refresh token. This method will revoke the current access * token, if a token isn't provided. @@ -378,11 +412,11 @@ private function getCacheKeyFromCertLocation($certsLocation) */ public function revoke($token): bool { - $oauth = new OAuth2([ + $oauth2 = new OAuth2([ 'tokenRevokeUri' => self::TOKEN_REVOKE_URI, + 'httpClient' => $this->httpClient, ]); - - return $oauth->revoke($token); + return $oauth2->revoke($token); } /** diff --git a/src/Auth/Jwt/FirebaseJwtClient.php b/src/Auth/Jwt/FirebaseJwtClient.php new file mode 100644 index 000000000..898af6102 --- /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); + } +} \ No newline at end of file diff --git a/src/HttpHandler/Guzzle7HttpHandler.php b/src/Auth/Jwt/JwtClientInterface.php similarity index 56% rename from src/HttpHandler/Guzzle7HttpHandler.php rename to src/Auth/Jwt/JwtClientInterface.php index e84f6603b..0d0728238 100644 --- a/src/HttpHandler/Guzzle7HttpHandler.php +++ b/src/Auth/Jwt/JwtClientInterface.php @@ -1,12 +1,12 @@ null, 'httpClient' => null, + 'jwtClient' => null, 'expiry' => self::DEFAULT_EXPIRY_SECONDS, 'extensionParams' => [], 'authorizationUri' => null, @@ -369,6 +378,8 @@ public function __construct(array $config = []) } $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']); @@ -430,7 +441,7 @@ public function toJwt(array $config = []) } $assertion += $this->getAdditionalClaims(); - return JWT::encode( + return $this->jwtClient->encode( $assertion, $this->getSigningKey(), $this->getSigningAlgorithm(), @@ -625,192 +636,6 @@ public function setAuthToken(array $authToken) } } - /** - * 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 $certs Certificate array according to the JWK spec (see - * https://tools.ietf.org/html/rfc7517). - * @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. - * @return array the token payload. - * @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( - string $token, - array $certs, - array $options = [] - ): array { - $audience = isset($options['audience']) - ? $options['audience'] - : null; - $issuer = isset($options['issuer']) - ? $options['issuer'] - : null; - - // Check signature against each available cert. - $alg = $this->determineAlg($certs); - if (!in_array($alg, ['RS256', 'ES256'])) { - throw new InvalidArgumentException( - 'unrecognized "alg" in certs, expected ES256 or RS256' - ); - } - - if ($alg == 'RS256') { - return $this->verifyRs256($token, $certs, $audience, $issuer); - } - - return $this->verifyEs256($token, $certs, $audience, $issuer); - } - - /** - * 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 the token payload. - */ - private function verifyEs256( - string $token, - array $certs, - $audience = null, - $issuer = null - ): array { - $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 the token payload. - */ - private function verifyRs256( - string $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; - } - - /** - * 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 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; - } - /** * Revoke an OAuth2 access token or refresh token. This method will revoke the current access * token, if a token isn't provided. 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/HttpHandlerFactory.php b/src/HttpHandler/HttpHandlerFactory.php deleted file mode 100644 index 41ccde878..000000000 --- a/src/HttpHandler/HttpHandlerFactory.php +++ /dev/null @@ -1,53 +0,0 @@ -mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $this->mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $this->mockCacheItem = $this->prophesize(CacheItemInterface::class); + $this->mockCache = $this->prophesize(CacheItemPoolInterface::class); } public function testCachedOnComputeTrueValue() @@ -380,7 +383,7 @@ public function testComputeCredentialsDefaultScopeArray() // ]); // $cacheOptions = []; - // $cachePool = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + // $cachePool = $this->prophesize(CacheItemPoolInterface::class); // $middleware = GoogleAuth::getMiddleware( // 'a scope', @@ -418,14 +421,14 @@ public function testComputeCredentialsDefaultScopeArray() // { // putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); - // $mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + // $mockCacheItem = $this->prophesize(CacheItemInterface::class); // $mockCacheItem->isHit() // ->willReturn(true); // $mockCacheItem->get() // ->shouldBeCalledTimes(1) // ->willReturn(false); - // $mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + // $mockCache = $this->prophesize(CacheItemPoolInterface::class); // $mockCache->getItem('google_auth_on_gce_cache') // ->shouldBeCalledTimes(1) // ->willReturn($mockCacheItem->reveal()); @@ -447,7 +450,7 @@ public function testComputeCredentialsDefaultScopeArray() // $gceIsCalled = true; // return new Response(200, ['Metadata-Flavor' => 'Google']); // }; - // $mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + // $mockCacheItem = $this->prophesize(CacheItemInterface::class); // $mockCacheItem->isHit() // ->willReturn(false); // $mockCacheItem->set(true) @@ -455,7 +458,7 @@ public function testComputeCredentialsDefaultScopeArray() // $mockCacheItem->expiresAfter(1500) // ->shouldBeCalledTimes(1); - // $mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + // $mockCache = $this->prophesize(CacheItemPoolInterface::class); // $mockCache->getItem('google_auth_on_gce_cache') // ->shouldBeCalledTimes(1) // ->willReturn($mockCacheItem->reveal()); @@ -484,7 +487,7 @@ public function testComputeCredentialsDefaultScopeArray() // $gceIsCalled = true; // return new Response(200, ['Metadata-Flavor' => 'Google']); // }; - // $mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + // $mockCacheItem = $this->prophesize(CacheItemInterface::class); // $mockCacheItem->isHit() // ->willReturn(false); // $mockCacheItem->set(true) @@ -492,7 +495,7 @@ public function testComputeCredentialsDefaultScopeArray() // $mockCacheItem->expiresAfter($lifetime) // ->shouldBeCalledTimes(1); - // $mockCache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + // $mockCache = $this->prophesize(CacheItemPoolInterface::class); // $mockCache->getItem($prefix . 'google_auth_on_gce_cache') // ->shouldBeCalledTimes(1) // ->willReturn($mockCacheItem->reveal()); @@ -562,7 +565,7 @@ public function testComputeCredentialsDefaultScopeArray() // ]); // $cacheOptions = []; - // $cachePool = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + // $cachePool = $this->prophesize(CacheItemPoolInterface::class); // $credentials = GoogleAuth::getIdTokenCredentials( // self::TEST_TARGET_AUDIENCE, @@ -639,7 +642,7 @@ public function testWithFetchAuthTokenCacheAndExplicitQuotaProject() new Response(200), ]); - $cachePool = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cachePool = $this->prophesize(CacheItemPoolInterface::class); $googleAuth = new GoogleAuth([ 'cache' => $cachePool->reveal(), @@ -752,8 +755,9 @@ public function testRetrieveCertsFromLocationLocalFile() { $certsLocation = __DIR__ . '/fixtures/federated-certs.json'; $certsData = json_decode(file_get_contents($certsLocation), true); + $parsedCertsData = []; - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); + $item = $this->prophesize(CacheItemInterface::class); $item->get() ->shouldBeCalledTimes(1) ->willReturn(null); @@ -766,26 +770,30 @@ public function testRetrieveCertsFromLocationLocalFile() ->shouldBeCalledTimes(1) ->willReturn($item->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) + $this->mockCache->save(Argument::type(CacheItemInterface::class)) ->shouldBeCalledTimes(1); - $googleAuth = new GoogleAuth(['cache' => $this->mockCache->reveal()]); - - $googleAuth->mocks['decode'] = function ($googleAuth, $publicKey, $allowedAlgs) { - $this->assertEquals(self::TEST_TOKEN, $googleAuth); - $this->assertEquals(['RS256'], $allowedAlgs); - - return (object) [ + $jwt = $this->prophesize(JwtClientInterface::class); + $jwt->parseKeySet($certsData) + ->shouldBeCalledTimes(1) + ->willReturn($parsedCertsData); + $jwt->decode(self::TEST_TOKEN, $parsedCertsData, ['RS256']) + ->shouldBeCalledTimes(1) + ->willReturn([ 'iat' => time(), 'exp' => time() + 30, 'name' => 'foo', - 'iss' => AccessToken::OAUTH2_ISSUER_HTTPS - ];; - }; + 'iss' => 'https://accounts.google.com' + ]); - $googleAuth->verify(self::TEST_TOKEN, [ - 'certsLocation' => $certsLocation + $googleAuth = new GoogleAuth([ + 'cache' => $this->mockCache->reveal(), + 'jwtClient' => $jwt->reveal(), ]); + + $this->assertTrue($googleAuth->verify(self::TEST_TOKEN, [ + 'certsLocation' => $certsLocation + ])); } public function testRetrieveCertsFromLocationLocalFileInvalidFilePath() @@ -795,7 +803,7 @@ public function testRetrieveCertsFromLocationLocalFileInvalidFilePath() $certsLocation = __DIR__ . '/fixtures/federated-certs-does-not-exist.json'; - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); + $item = $this->prophesize(CacheItemInterface::class); $item->get() ->shouldBeCalledTimes(1) ->willReturn(null); @@ -814,9 +822,9 @@ public function testRetrieveCertsFromLocationLocalFileInvalidFilePath() public function testRetrieveCertsInvalidData() { $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage('federated sign-on certs expects "keys" to be set'); + $this->expectExceptionMessage('certs expects "keys" to be set'); - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); + $item = $this->prophesize(CacheItemInterface::class); $item->get() ->shouldBeCalledTimes(1) ->willReturn('{}'); @@ -833,12 +841,12 @@ public function testRetrieveCertsInvalidData() public function testRetrieveCertsFromLocationLocalFileInvalidFileData() { $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage('federated sign-on certs expects "keys" to be set'); + $this->expectExceptionMessage('certs expects "keys" to be set'); $temp = tmpfile(); fwrite($temp, '{}'); $certsLocation = stream_get_meta_data($temp)['uri']; - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); + $item = $this->prophesize(CacheItemInterface::class); $item->get() ->shouldBeCalledTimes(1) ->willReturn(null); @@ -872,7 +880,7 @@ function ($request) use ($certsJson) { } ); - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); + $item = $this->prophesize(CacheItemInterface::class); $item->get() ->shouldBeCalledTimes(1) ->willReturn(null); @@ -885,7 +893,7 @@ function ($request) use ($certsJson) { ->shouldBeCalledTimes(1) ->willReturn($item->reveal()); - $this->mockCache->save(Argument::type('Psr\Cache\CacheItemInterface')) + $this->mockCache->save(Argument::type(CacheItemInterface::class)) ->shouldBeCalledTimes(1); $googleAuth = new GoogleAuth([ @@ -919,7 +927,7 @@ public function testRetrieveCertsFromLocationRemoteBadRequest() new Response(500, [], $badBody), ]); - $item = $this->prophesize('Psr\Cache\CacheItemInterface'); + $item = $this->prophesize(CacheItemInterface::class); $item->get() ->shouldBeCalledTimes(1) ->willReturn(null); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2af59d0ea..079a3475a 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -36,32 +36,29 @@ function httpClientWithResponses(array $mockResponses = []) function httpClientFromCallable(callable $httpHandler): ClientInterface { - return new HttpClientImpl($httpHandler); -} - -class HttpClientImpl implements ClientInterface -{ - private $httpHandler; + return new class($httpHandler) implements ClientInterface { + private $httpHandler; - public function __construct(callable $httpHandler) - { - $this->httpHandler = $httpHandler; - } + public function __construct(callable $httpHandler) + { + $this->httpHandler = $httpHandler; + } - public function send( - RequestInterface $request, - array $options = [] - ) : ResponseInterface - { - $httpHandler = $this->httpHandler; - return $httpHandler($request); - } + public function send( + RequestInterface $request, + array $options = [] + ) : ResponseInterface + { + $httpHandler = $this->httpHandler; + return $httpHandler($request); + } - public function sendAsync( - RequestInterface $request, - array $options = [] - ) : PromiseInterface - { - // no op - } + public function sendAsync( + RequestInterface $request, + array $options = [] + ) : PromiseInterface + { + // no op + } + }; }