diff --git a/composer.json b/composer.json index abacbe84b..53659b698 100644 --- a/composer.json +++ b/composer.json @@ -9,9 +9,8 @@ "php": "^8.0", "google/auth": "^1.37", "google/apiclient-services": "~0.350", - "firebase/php-jwt": "^6.0", + "firebase/php-jwt": "^6.3", "monolog/monolog": "^2.9||^3.0", - "phpseclib/phpseclib": "^3.0.36", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.6" }, diff --git a/src/AccessToken/Verify.php b/src/AccessToken/Verify.php index d957908ba..da6bf85fa 100644 --- a/src/AccessToken/Verify.php +++ b/src/AccessToken/Verify.php @@ -18,24 +18,20 @@ namespace Google\AccessToken; -use DateTime; use DomainException; use Exception; -use ExpiredException; -use Firebase\JWT\ExpiredException as ExpiredExceptionV3; +use Firebase\JWT\CachedKeySet; +use Firebase\JWT\ExpiredException; use Firebase\JWT\JWT; -use Firebase\JWT\Key; use Firebase\JWT\SignatureInvalidException; use Google\Auth\Cache\MemoryCacheItemPool; -use Google\Exception as GoogleException; use GuzzleHttp\Client; -use GuzzleHttp\ClientInterface; +use GuzzleHttp\ClientInterface as GuzzleClientInterface; +use GuzzleHttp\Psr7\HttpFactory; use InvalidArgumentException; use LogicException; -use phpseclib3\Crypt\AES; -use phpseclib3\Crypt\PublicKeyLoader; -use phpseclib3\Math\BigInteger; use Psr\Cache\CacheItemPoolInterface; +use Psr\Http\Client\ClientInterface; /** * Wrapper around Google Access Tokens which provides convenience functions @@ -48,26 +44,21 @@ class Verify const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com'; /** - * @var ClientInterface The http client - */ - private $http; + * @var \Firebase\JWT\JWT + */ + public JWT $jwt; /** - * @var CacheItemPoolInterface cache class + * @var \Firebase\JWT\CachedKeySet */ - private $cache; - - /** - * @var \Firebase\JWT\JWT - */ - public $jwt; + private CachedKeySet $keySet; /** * Instantiates the class, but does not initiate the login flow, leaving it * to the discretion of the caller. */ public function __construct( - ClientInterface $http = null, + GuzzleClientInterface $http = null, CacheItemPoolInterface $cache = null, $jwt = null ) { @@ -79,9 +70,17 @@ public function __construct( $cache = new MemoryCacheItemPool(); } - $this->http = $http; - $this->cache = $cache; + if (!$http instanceof ClientInterface) { + throw new InvalidArgumentException('http client must implement ' . ClientInterface::class); + } + $this->jwt = $jwt ?: $this->getJwtService(); + $this->keySet = new CachedKeySet( + self::FEDERATED_SIGNON_CERT_URL, + $http, + new HttpFactory(), + $cache + ); } /** @@ -100,123 +99,27 @@ public function verifyIdToken($idToken, $audience = null) throw new LogicException('id_token cannot be null'); } - // set phpseclib constants if applicable - $this->setPhpsecConstants(); - // Check signature - $certs = $this->getFederatedSignOnCerts(); - foreach ($certs as $cert) { - try { - $args = [$idToken]; - $publicKey = $this->getPublicKey($cert); - if (class_exists(Key::class)) { - $args[] = new Key($publicKey, 'RS256'); - } else { - $args[] = $publicKey; - $args[] = ['RS256']; - } - $payload = \call_user_func_array([$this->jwt, 'decode'], $args); - - if (property_exists($payload, 'aud')) { - if ($audience && $payload->aud != $audience) { - return false; - } - } - - // support HTTP and HTTPS issuers - // @see https://developers.google.com/identity/sign-in/web/backend-auth - $issuers = [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS]; - if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) { - return false; - } - - return (array) $payload; - } catch (ExpiredException $e) { // @phpstan-ignore-line - return false; - } catch (ExpiredExceptionV3 $e) { - return false; - } catch (SignatureInvalidException $e) { - // continue - } catch (DomainException $e) { - // continue - } + try { + $payload = ($this->jwt)->decode($idToken, $this->keySet); + } catch (ExpiredException | SignatureInvalidException | DomainException) { + return false; } - return false; - } - - private function getCache() - { - return $this->cache; - } - - /** - * Retrieve and cache a certificates file. - * - * @param string $url location - * @throws \Google\Exception - * @return array certificates - */ - private function retrieveCertsFromLocation($url) - { - // If we're retrieving a local file, just grab it. - if (0 !== strpos($url, 'http')) { - if (!$file = file_get_contents($url)) { - throw new GoogleException( - "Failed to retrieve verification certificates: '" . - $url . "'." - ); - } - - return json_decode($file, true); - } - - // @phpstan-ignore-next-line - $response = $this->http->get($url); - - if ($response->getStatusCode() == 200) { - return json_decode((string) $response->getBody(), true); - } - throw new GoogleException( - sprintf( - 'Failed to retrieve verification certificates: "%s".', - $response->getBody()->getContents() - ), - $response->getStatusCode() - ); - } - - // 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. - private function getFederatedSignOnCerts() - { - $certs = null; - if ($cache = $this->getCache()) { - $cacheItem = $cache->getItem('federated_signon_certs_v3'); - $certs = $cacheItem->get(); - } - - - if (!$certs) { - $certs = $this->retrieveCertsFromLocation( - self::FEDERATED_SIGNON_CERT_URL - ); - - if ($cache) { - $cacheItem->expiresAt(new DateTime('+1 hour')); - $cacheItem->set($certs); - $cache->save($cacheItem); + if (property_exists($payload, 'aud')) { + if ($audience && $payload->aud != $audience) { + return false; } } - if (!isset($certs['keys'])) { - throw new InvalidArgumentException( - 'federated sign-on certs expects "keys" to be set' - ); + // support HTTP and HTTPS issuers + // @see https://developers.google.com/identity/sign-in/web/backend-auth + $issuers = [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS]; + if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) { + return false; } - return $certs['keys']; + return (array) $payload; } private function getJwtService() @@ -230,35 +133,4 @@ private function getJwtService() return $jwt; } - - private function getPublicKey($cert) - { - $modulus = new BigInteger($this->jwt->urlsafeB64Decode($cert['n']), 256); - $exponent = new BigInteger($this->jwt->urlsafeB64Decode($cert['e']), 256); - $component = ['n' => $modulus, 'e' => $exponent]; - - $loader = PublicKeyLoader::load($component); - - return $loader->toString('PKCS8'); - } - - /** - * 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 - */ - 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', AES::ENGINE_OPENSSL); - } - } - } } diff --git a/tests/BaseTest.php b/tests/BaseTest.php index 144c7d789..75b0bc5ec 100644 --- a/tests/BaseTest.php +++ b/tests/BaseTest.php @@ -32,7 +32,7 @@ class BaseTest extends TestCase use ProphecyTrait; private $key; - private $client; + protected $client; public function getClient() { @@ -45,14 +45,14 @@ public function getClient() public function getCache($path = null) { - $path = $path ?: sys_get_temp_dir().'/google-api-php-client-tests/'; + $path = sys_get_temp_dir() . '/google-api-php-client-tests/' . ($path ?: ''); $filesystemAdapter = new Local($path); $filesystem = new Filesystem($filesystemAdapter); return new FilesystemCachePool($filesystem); } - private function createClient() + protected function createClient(array $scopes = null) { $options = [ 'auth' => 'google_auth', @@ -69,14 +69,14 @@ private function createClient() $client = new Client(); $client->setApplicationName('google-api-php-client-tests'); $client->setHttpClient($httpClient); - $client->setScopes( - [ - "https://www.googleapis.com/auth/tasks", - "https://www.googleapis.com/auth/adsense", - "https://www.googleapis.com/auth/youtube", - "https://www.googleapis.com/auth/drive", - ] - ); + + $scopes = $scopes ?? [ + 'https://www.googleapis.com/auth/tasks', + 'https://www.googleapis.com/auth/adsense', + 'https://www.googleapis.com/auth/youtube', + 'https://www.googleapis.com/auth/drive', + ]; + $client->setScopes($scopes); if ($this->key) { $client->setDeveloperKey($this->key); @@ -85,9 +85,7 @@ private function createClient() list($clientId, $clientSecret) = $this->getClientIdAndSecret(); $client->setClientId($clientId); $client->setClientSecret($clientSecret); - if (version_compare(PHP_VERSION, '5.5', '>=')) { - $client->setCache($this->getCache()); - } + $client->setCache($this->getCache(sha1(implode('', $scopes)))); return $client; } diff --git a/tests/Google/AccessToken/VerifyTest.php b/tests/Google/AccessToken/VerifyTest.php index 7d37209e4..69ed0c4f2 100644 --- a/tests/Google/AccessToken/VerifyTest.php +++ b/tests/Google/AccessToken/VerifyTest.php @@ -24,38 +24,9 @@ use Firebase\JWT\JWT; use Google\AccessToken\Verify; use Google\Tests\BaseTest; -use ReflectionMethod; -use phpseclib3\Crypt\AES; class VerifyTest extends BaseTest { - /** - * This test needs to run before the other verify tests, - * to ensure the constants are not defined. - */ - public function testPhpsecConstants() - { - $client = $this->getClient(); - $verify = new Verify($client->getHttpClient()); - - // set these to values that will be changed - if (defined('MATH_BIGINTEGER_OPENSSL_ENABLED') || defined('CRYPT_RSA_MODE')) { - $this->markTestSkipped('Cannot run test - constants already defined'); - } - - // Pretend we are on App Engine VMs - putenv('GAE_VM=1'); - - $verify->verifyIdToken('a.b.c'); - - putenv('GAE_VM=0'); - - $openSslEnable = constant('MATH_BIGINTEGER_OPENSSL_ENABLED'); - $rsaMode = constant('CRYPT_RSA_MODE'); - $this->assertTrue($openSslEnable); - $this->assertEquals(AES::ENGINE_OPENSSL, $rsaMode); - } - /** * Most of the logic for ID token validation is in AuthTest - * this is just a general check to ensure we verify a valid @@ -78,6 +49,7 @@ public function testValidateIdToken() $data = json_decode($jwt->urlSafeB64Decode($segments[1])); $verify = new Verify($http); $payload = $verify->verifyIdToken($token['id_token'], $data->aud); + $this->assertIsArray($payload); $this->assertArrayHasKey('sub', $payload); $this->assertGreaterThan(0, strlen($payload['sub'])); @@ -107,6 +79,7 @@ public function testLeewayIsUnchangedWhenPassingInJwt() $jwt::$leeway = $leeway = 1.5; $client = $this->getClient(); $token = $client->getAccessToken(); + if ($client->isAccessTokenExpired()) { $token = $client->fetchAccessTokenWithRefreshToken(); } @@ -120,19 +93,11 @@ public function testLeewayIsUnchangedWhenPassingInJwt() $this->assertEquals($leeway, $jwt::$leeway); } - public function testRetrieveCertsFromLocation() + public function getClient() { - $client = $this->getClient(); - $verify = new Verify($client->getHttpClient()); - - // make this method public for testing purposes - $method = new ReflectionMethod($verify, 'retrieveCertsFromLocation'); - $method->setAccessible(true); - $certs = $method->invoke($verify, Verify::FEDERATED_SIGNON_CERT_URL); - - $this->assertArrayHasKey('keys', $certs); - $this->assertGreaterThan(1, count($certs['keys'])); - $this->assertArrayHasKey('alg', $certs['keys'][0]); - $this->assertEquals('RS256', $certs['keys'][0]['alg']); + if (!$this->client) { + $this->client = $this->createClient(['profile']); + } + return $this->client; } } diff --git a/tests/Google/CacheTest.php b/tests/Google/CacheTest.php index 153b2669c..b2c2e6d5c 100644 --- a/tests/Google/CacheTest.php +++ b/tests/Google/CacheTest.php @@ -56,7 +56,7 @@ public function testFileCache() $client->useApplicationDefaultCredentials(); $client->setScopes(['https://www.googleapis.com/auth/drive.readonly']); // filecache with new cache dir - $cache = $this->getCache(sys_get_temp_dir() . '/cloud-samples-tests-php-cache-test/'); + $cache = $this->getCache('cloud-samples-tests-php-cache-test/'); $client->setCache($cache); $token1 = null; diff --git a/tests/Google/Service/ResourceTest.php b/tests/Google/Service/ResourceTest.php index ccf29a7f7..b8d3d8959 100644 --- a/tests/Google/Service/ResourceTest.php +++ b/tests/Google/Service/ResourceTest.php @@ -48,7 +48,6 @@ public function __construct(Client $client, $rootUrl = null) class ResourceTest extends BaseTest { - private $client; private $service; public function setUp(): void diff --git a/tests/Google/Task/RunnerTest.php b/tests/Google/Task/RunnerTest.php index 650ef5c75..6e45b41c8 100644 --- a/tests/Google/Task/RunnerTest.php +++ b/tests/Google/Task/RunnerTest.php @@ -32,8 +32,6 @@ class RunnerTest extends BaseTest { - private $client; - private $mockedCallsCount = 0; private $currentMockedCall = 0; private $mockedCalls = [];