oont-contents/plugins/wp-mail-smtp/vendor_prefixed/google/auth/src/AccessToken.php
2025-02-08 15:10:23 +01:00

414 lines
19 KiB
PHP

<?php
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace WPMailSMTP\Vendor\Google\Auth;
use DateTime;
use Exception;
use WPMailSMTP\Vendor\Firebase\JWT\ExpiredException;
use WPMailSMTP\Vendor\Firebase\JWT\JWT;
use WPMailSMTP\Vendor\Firebase\JWT\Key;
use WPMailSMTP\Vendor\Firebase\JWT\SignatureInvalidException;
use WPMailSMTP\Vendor\Google\Auth\Cache\MemoryCacheItemPool;
use WPMailSMTP\Vendor\Google\Auth\HttpHandler\HttpClientCache;
use WPMailSMTP\Vendor\Google\Auth\HttpHandler\HttpHandlerFactory;
use WPMailSMTP\Vendor\GuzzleHttp\Psr7\Request;
use WPMailSMTP\Vendor\GuzzleHttp\Psr7\Utils;
use InvalidArgumentException;
use WPMailSMTP\Vendor\phpseclib\Crypt\RSA;
use WPMailSMTP\Vendor\phpseclib\Math\BigInteger as BigInteger2;
use WPMailSMTP\Vendor\phpseclib3\Crypt\PublicKeyLoader;
use WPMailSMTP\Vendor\phpseclib3\Math\BigInteger as BigInteger3;
use WPMailSMTP\Vendor\Psr\Cache\CacheItemPoolInterface;
use RuntimeException;
use WPMailSMTP\Vendor\SimpleJWT\InvalidTokenException;
use WPMailSMTP\Vendor\SimpleJWT\JWT as SimpleJWT;
use WPMailSMTP\Vendor\SimpleJWT\Keys\KeyFactory;
use WPMailSMTP\Vendor\SimpleJWT\Keys\KeySet;
use UnexpectedValueException;
/**
* Wrapper around Google Access Tokens which provides convenience functions.
*
* @experimental
*/
class AccessToken
{
const FEDERATED_SIGNON_CERT_URL = 'https://www.googleapis.com/oauth2/v3/certs';
const IAP_CERT_URL = 'https://www.gstatic.com/iap/verify/public_key-jwk';
const IAP_ISSUER = 'https://cloud.google.com/iap';
const OAUTH2_ISSUER = 'accounts.google.com';
const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com';
const OAUTH2_REVOKE_URI = 'https://oauth2.googleapis.com/revoke';
/**
* @var callable
*/
private $httpHandler;
/**
* @var CacheItemPoolInterface
*/
private $cache;
/**
* @param callable $httpHandler [optional] An HTTP Handler to deliver PSR-7 requests.
* @param CacheItemPoolInterface $cache [optional] A PSR-6 compatible cache implementation.
*/
public function __construct(callable $httpHandler = null, \WPMailSMTP\Vendor\Psr\Cache\CacheItemPoolInterface $cache = null)
{
$this->httpHandler = $httpHandler ?: \WPMailSMTP\Vendor\Google\Auth\HttpHandler\HttpHandlerFactory::build(\WPMailSMTP\Vendor\Google\Auth\HttpHandler\HttpClientCache::getHttpClient());
$this->cache = $cache ?: new \WPMailSMTP\Vendor\Google\Auth\Cache\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<mixed> $options [optional] {
* Configuration options.
* @type string $audience The indended recipient of the token.
* @type string $issuer The intended issuer of the token.
* @type string $cacheKey The cache key of the cached certs. Defaults to
* the sha1 of $certsLocation if provided, otherwise is set to
* "federated_signon_certs_v3".
* @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.
* @type bool $throwException Whether the function should throw an
* exception if the verification fails. This is useful for
* determining the reason verification failed.
* }
* @return array<mixed>|false 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 (\WPMailSMTP\Vendor\Firebase\JWT\ExpiredException $e) {
// firebase/php-jwt 5+
} catch (\WPMailSMTP\Vendor\Firebase\JWT\SignatureInvalidException $e) {
// firebase/php-jwt 5+
} catch (\WPMailSMTP\Vendor\SimpleJWT\InvalidTokenException $e) {
// simplejwt
} 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<mixed> $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<mixed> $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<mixed> the token payload, if successful, or false if not.
*/
private function verifyEs256($token, array $certs, $audience = null, $issuer = null)
{
$this->checkSimpleJwt();
$jwkset = new \WPMailSMTP\Vendor\SimpleJWT\Keys\KeySet();
foreach ($certs as $cert) {
$jwkset->add(\WPMailSMTP\Vendor\SimpleJWT\Keys\KeyFactory::create($cert, 'php'));
}
// Validate the signature using the key set and ES256 algorithm.
$jwt = $this->callSimpleJwtDecode([$token, $jwkset, 'ES256']);
$payload = $jwt->getClaims();
if ($audience) {
if (!isset($payload['aud']) || $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<mixed> $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<mixed> 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');
}
$publicKey = $this->loadPhpsecPublicKey($cert['n'], $cert['e']);
// create an array of key IDs to certs for the JWT library
$keys[$cert['kid']] = new \WPMailSMTP\Vendor\Firebase\JWT\Key($publicKey, 'RS256');
}
$payload = $this->callJwtStatic('decode', [$token, $keys]);
if ($audience) {
if (!\property_exists($payload, 'aud') || $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<mixed> $token The token (access token or a refresh token) that should be revoked.
* @param array<mixed> $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 = \WPMailSMTP\Vendor\GuzzleHttp\Psr7\Utils::streamFor(\http_build_query(['token' => $token]));
$request = new \WPMailSMTP\Vendor\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 string $cacheKey The key under which to cache the retrieved certs.
* @param array<mixed> $options [optional] Configuration options.
* @return array<mixed>
* @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 string $url location
* @param array<mixed> $options [optional] Configuration options.
* @return array<mixed> 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((string) \file_get_contents($url), \true);
}
$httpHandler = $this->httpHandler;
$response = $httpHandler(new \WPMailSMTP\Vendor\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());
}
/**
* @return void
*/
private function checkAndInitializePhpsec()
{
if (!$this->checkAndInitializePhpsec2() && !$this->checkPhpsec3()) {
throw new \RuntimeException('Please require phpseclib/phpseclib v2 or v3 to use this utility.');
}
}
private function loadPhpsecPublicKey(string $modulus, string $exponent) : string
{
if (\class_exists(\WPMailSMTP\Vendor\phpseclib\Crypt\RSA::class) && \class_exists(\WPMailSMTP\Vendor\phpseclib\Math\BigInteger::class)) {
$key = new \WPMailSMTP\Vendor\phpseclib\Crypt\RSA();
$key->loadKey(['n' => new \WPMailSMTP\Vendor\phpseclib\Math\BigInteger($this->callJwtStatic('urlsafeB64Decode', [$modulus]), 256), 'e' => new \WPMailSMTP\Vendor\phpseclib\Math\BigInteger($this->callJwtStatic('urlsafeB64Decode', [$exponent]), 256)]);
return $key->getPublicKey();
}
$key = \WPMailSMTP\Vendor\phpseclib3\Crypt\PublicKeyLoader::load(['n' => new \WPMailSMTP\Vendor\phpseclib3\Math\BigInteger($this->callJwtStatic('urlsafeB64Decode', [$modulus]), 256), 'e' => new \WPMailSMTP\Vendor\phpseclib3\Math\BigInteger($this->callJwtStatic('urlsafeB64Decode', [$exponent]), 256)]);
return $key->toString('PKCS1');
}
/**
* @return bool
*/
private function checkAndInitializePhpsec2() : bool
{
if (!\class_exists('WPMailSMTP\\Vendor\\phpseclib\\Crypt\\RSA')) {
return \false;
}
/**
* 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
*/
if (\filter_var(\getenv('GAE_VM'), \FILTER_VALIDATE_BOOLEAN)) {
if (!\defined('WPMailSMTP\\Vendor\\MATH_BIGINTEGER_OPENSSL_ENABLED')) {
\define('WPMailSMTP\\Vendor\\MATH_BIGINTEGER_OPENSSL_ENABLED', \true);
}
if (!\defined('WPMailSMTP\\Vendor\\CRYPT_RSA_MODE')) {
\define('WPMailSMTP\\Vendor\\CRYPT_RSA_MODE', \WPMailSMTP\Vendor\phpseclib\Crypt\RSA::MODE_OPENSSL);
}
}
return \true;
}
/**
* @return bool
*/
private function checkPhpsec3() : bool
{
return \class_exists('WPMailSMTP\\Vendor\\phpseclib3\\Crypt\\RSA');
}
/**
* @return void
*/
private function checkSimpleJwt()
{
// @codeCoverageIgnoreStart
if (!\class_exists(\WPMailSMTP\Vendor\SimpleJWT\JWT::class)) {
throw new \RuntimeException('Please require kelvinmo/simplejwt ^0.2 to use this utility.');
}
// @codeCoverageIgnoreEnd
}
/**
* Provide a hook to mock calls to the JWT static methods.
*
* @param string $method
* @param array<mixed> $args
* @return mixed
*/
protected function callJwtStatic($method, array $args = [])
{
return \call_user_func_array([\WPMailSMTP\Vendor\Firebase\JWT\JWT::class, $method], $args);
// @phpstan-ignore-line
}
/**
* Provide a hook to mock calls to the JWT static methods.
*
* @param array<mixed> $args
* @return mixed
*/
protected function callSimpleJwtDecode(array $args = [])
{
return \call_user_func_array([\WPMailSMTP\Vendor\SimpleJWT\JWT::class, '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;
}
}