httpHandler = $httpHandler ?: \Google\Site_Kit_Dependencies\Google\Auth\HttpHandler\HttpHandlerFactory::build(\Google\Site_Kit_Dependencies\Google\Auth\HttpHandler\HttpClientCache::getHttpClient()); $this->cache = $cache ?: new \Google\Site_Kit_Dependencies\Google\Auth\Cache\MemoryCacheItemPool(); $this->configureJwtService(); // set phpseclib constants if applicable $this->setPhpsecConstants(); } /** * 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. * * @type string $audience The indended recipient of the token. * @type string $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. * } * @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 \RuntimeException If certs could not be retrieved from a remote location. */ public function verify($token, array $options = []) { $audience = isset($options['audience']) ? $options['audience'] : null; $certsLocation = isset($options['certsLocation']) ? $options['certsLocation'] : self::FEDERATED_SIGNON_CERT_URL; unset($options['audience'], $options['certsLocation']); // Check signature against each available cert. // allow the loop to complete unless a known bad result is encountered. $certs = $this->getFederatedSignOnCerts($certsLocation, $options); foreach ($certs as $cert) { $rsa = new \Google\Site_Kit_Dependencies\phpseclib\Crypt\RSA(); $rsa->loadKey(['n' => new \Google\Site_Kit_Dependencies\phpseclib\Math\BigInteger($this->callJwtStatic('urlsafeB64Decode', [$cert['n']]), 256), 'e' => new \Google\Site_Kit_Dependencies\phpseclib\Math\BigInteger($this->callJwtStatic('urlsafeB64Decode', [$cert['e']]), 256)]); try { $pubkey = $rsa->getPublicKey(); $payload = $this->callJwtStatic('decode', [$token, $pubkey, ['RS256']]); 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 (\Google\Site_Kit_Dependencies\Firebase\JWT\ExpiredException $e) { return \false; } catch (\Google\Site_Kit_Dependencies\ExpiredException $e) { // (firebase/php-jwt 2) return \false; } catch (\Google\Site_Kit_Dependencies\Firebase\JWT\SignatureInvalidException $e) { // continue } catch (\Google\Site_Kit_Dependencies\SignatureInvalidException $e) { // continue (firebase/php-jwt 2) } catch (\DomainException $e) { // continue } } return \false; } /** * 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 boolean 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 = \Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\stream_for(\http_build_query(['token' => $token])); $request = new \Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\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 array $options [optional] Configuration options. * @return array * @throws \InvalidArgumentException If received certs are in an invalid format. */ private function getFederatedSignOnCerts($location, array $options = []) { $cacheItem = $this->cache->getItem('federated_signon_certs_v3'); $certs = $cacheItem ? $cacheItem->get() : null; $gotNewCerts = \false; if (!$certs) { $certs = $this->retrieveCertsFromLocation($location, $options); $gotNewCerts = \true; } if (!isset($certs['keys'])) { throw new \InvalidArgumentException('federated sign-on 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. * @throws \RuntimeException * @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 \Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\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()); } /** * Set required defaults for JWT. */ private function configureJwtService() { $class = \class_exists('Google\\Site_Kit_Dependencies\\Firebase\\JWT\\JWT') ? 'Firebase\\JWT\\JWT' : '\\JWT'; if (\property_exists($class, 'leeway') && $class::$leeway < 1) { // Ensures JWT leeway is at least 1 // @see https://github.com/google/google-api-php-client/issues/827 $class::$leeway = 1; } } /** * 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('Google\\Site_Kit_Dependencies\\MATH_BIGINTEGER_OPENSSL_ENABLED')) { \define('Google\\Site_Kit_Dependencies\\MATH_BIGINTEGER_OPENSSL_ENABLED', \true); } if (!\defined('Google\\Site_Kit_Dependencies\\CRYPT_RSA_MODE')) { \define('Google\\Site_Kit_Dependencies\\CRYPT_RSA_MODE', \Google\Site_Kit_Dependencies\phpseclib\Crypt\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('Google\\Site_Kit_Dependencies\\Firebase\\JWT\\JWT') ? 'Firebase\\JWT\\JWT' : 'JWT'; return \call_user_func_array([$class, $method], $args); } }