226 lines
7.1 KiB
PHP
226 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace Google\Site_Kit_Dependencies\Firebase\JWT;
|
|
|
|
use ArrayAccess;
|
|
use InvalidArgumentException;
|
|
use LogicException;
|
|
use OutOfBoundsException;
|
|
use Google\Site_Kit_Dependencies\Psr\Cache\CacheItemInterface;
|
|
use Google\Site_Kit_Dependencies\Psr\Cache\CacheItemPoolInterface;
|
|
use Google\Site_Kit_Dependencies\Psr\Http\Client\ClientInterface;
|
|
use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestFactoryInterface;
|
|
use RuntimeException;
|
|
use UnexpectedValueException;
|
|
/**
|
|
* @implements ArrayAccess<string, Key>
|
|
*/
|
|
class CachedKeySet implements \ArrayAccess
|
|
{
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $jwksUri;
|
|
/**
|
|
* @var ClientInterface
|
|
*/
|
|
private $httpClient;
|
|
/**
|
|
* @var RequestFactoryInterface
|
|
*/
|
|
private $httpFactory;
|
|
/**
|
|
* @var CacheItemPoolInterface
|
|
*/
|
|
private $cache;
|
|
/**
|
|
* @var ?int
|
|
*/
|
|
private $expiresAfter;
|
|
/**
|
|
* @var ?CacheItemInterface
|
|
*/
|
|
private $cacheItem;
|
|
/**
|
|
* @var array<string, array<mixed>>
|
|
*/
|
|
private $keySet;
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $cacheKey;
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $cacheKeyPrefix = 'jwks';
|
|
/**
|
|
* @var int
|
|
*/
|
|
private $maxKeyLength = 64;
|
|
/**
|
|
* @var bool
|
|
*/
|
|
private $rateLimit;
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $rateLimitCacheKey;
|
|
/**
|
|
* @var int
|
|
*/
|
|
private $maxCallsPerMinute = 10;
|
|
/**
|
|
* @var string|null
|
|
*/
|
|
private $defaultAlg;
|
|
public function __construct(string $jwksUri, \Google\Site_Kit_Dependencies\Psr\Http\Client\ClientInterface $httpClient, \Google\Site_Kit_Dependencies\Psr\Http\Message\RequestFactoryInterface $httpFactory, \Google\Site_Kit_Dependencies\Psr\Cache\CacheItemPoolInterface $cache, int $expiresAfter = null, bool $rateLimit = \false, string $defaultAlg = null)
|
|
{
|
|
$this->jwksUri = $jwksUri;
|
|
$this->httpClient = $httpClient;
|
|
$this->httpFactory = $httpFactory;
|
|
$this->cache = $cache;
|
|
$this->expiresAfter = $expiresAfter;
|
|
$this->rateLimit = $rateLimit;
|
|
$this->defaultAlg = $defaultAlg;
|
|
$this->setCacheKeys();
|
|
}
|
|
/**
|
|
* @param string $keyId
|
|
* @return Key
|
|
*/
|
|
public function offsetGet($keyId) : \Google\Site_Kit_Dependencies\Firebase\JWT\Key
|
|
{
|
|
if (!$this->keyIdExists($keyId)) {
|
|
throw new \OutOfBoundsException('Key ID not found');
|
|
}
|
|
return \Google\Site_Kit_Dependencies\Firebase\JWT\JWK::parseKey($this->keySet[$keyId], $this->defaultAlg);
|
|
}
|
|
/**
|
|
* @param string $keyId
|
|
* @return bool
|
|
*/
|
|
public function offsetExists($keyId) : bool
|
|
{
|
|
return $this->keyIdExists($keyId);
|
|
}
|
|
/**
|
|
* @param string $offset
|
|
* @param Key $value
|
|
*/
|
|
public function offsetSet($offset, $value) : void
|
|
{
|
|
throw new \LogicException('Method not implemented');
|
|
}
|
|
/**
|
|
* @param string $offset
|
|
*/
|
|
public function offsetUnset($offset) : void
|
|
{
|
|
throw new \LogicException('Method not implemented');
|
|
}
|
|
/**
|
|
* @return array<mixed>
|
|
*/
|
|
private function formatJwksForCache(string $jwks) : array
|
|
{
|
|
$jwks = \json_decode($jwks, \true);
|
|
if (!isset($jwks['keys'])) {
|
|
throw new \UnexpectedValueException('"keys" member must exist in the JWK Set');
|
|
}
|
|
if (empty($jwks['keys'])) {
|
|
throw new \InvalidArgumentException('JWK Set did not contain any keys');
|
|
}
|
|
$keys = [];
|
|
foreach ($jwks['keys'] as $k => $v) {
|
|
$kid = isset($v['kid']) ? $v['kid'] : $k;
|
|
$keys[(string) $kid] = $v;
|
|
}
|
|
return $keys;
|
|
}
|
|
private function keyIdExists(string $keyId) : bool
|
|
{
|
|
if (null === $this->keySet) {
|
|
$item = $this->getCacheItem();
|
|
// Try to load keys from cache
|
|
if ($item->isHit()) {
|
|
// item found! retrieve it
|
|
$this->keySet = $item->get();
|
|
// If the cached item is a string, the JWKS response was cached (previous behavior).
|
|
// Parse this into expected format array<kid, jwk> instead.
|
|
if (\is_string($this->keySet)) {
|
|
$this->keySet = $this->formatJwksForCache($this->keySet);
|
|
}
|
|
}
|
|
}
|
|
if (!isset($this->keySet[$keyId])) {
|
|
if ($this->rateLimitExceeded()) {
|
|
return \false;
|
|
}
|
|
$request = $this->httpFactory->createRequest('GET', $this->jwksUri);
|
|
$jwksResponse = $this->httpClient->sendRequest($request);
|
|
if ($jwksResponse->getStatusCode() !== 200) {
|
|
throw new \UnexpectedValueException(\sprintf('HTTP Error: %d %s for URI "%s"', $jwksResponse->getStatusCode(), $jwksResponse->getReasonPhrase(), $this->jwksUri), $jwksResponse->getStatusCode());
|
|
}
|
|
$this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
|
|
if (!isset($this->keySet[$keyId])) {
|
|
return \false;
|
|
}
|
|
$item = $this->getCacheItem();
|
|
$item->set($this->keySet);
|
|
if ($this->expiresAfter) {
|
|
$item->expiresAfter($this->expiresAfter);
|
|
}
|
|
$this->cache->save($item);
|
|
}
|
|
return \true;
|
|
}
|
|
private function rateLimitExceeded() : bool
|
|
{
|
|
if (!$this->rateLimit) {
|
|
return \false;
|
|
}
|
|
$cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
|
|
if (!$cacheItem->isHit()) {
|
|
$cacheItem->expiresAfter(1);
|
|
// # of calls are cached each minute
|
|
}
|
|
$callsPerMinute = (int) $cacheItem->get();
|
|
if (++$callsPerMinute > $this->maxCallsPerMinute) {
|
|
return \true;
|
|
}
|
|
$cacheItem->set($callsPerMinute);
|
|
$this->cache->save($cacheItem);
|
|
return \false;
|
|
}
|
|
private function getCacheItem() : \Google\Site_Kit_Dependencies\Psr\Cache\CacheItemInterface
|
|
{
|
|
if (\is_null($this->cacheItem)) {
|
|
$this->cacheItem = $this->cache->getItem($this->cacheKey);
|
|
}
|
|
return $this->cacheItem;
|
|
}
|
|
private function setCacheKeys() : void
|
|
{
|
|
if (empty($this->jwksUri)) {
|
|
throw new \RuntimeException('JWKS URI is empty');
|
|
}
|
|
// ensure we do not have illegal characters
|
|
$key = \preg_replace('|[^a-zA-Z0-9_\\.!]|', '', $this->jwksUri);
|
|
// add prefix
|
|
$key = $this->cacheKeyPrefix . $key;
|
|
// Hash keys if they exceed $maxKeyLength of 64
|
|
if (\strlen($key) > $this->maxKeyLength) {
|
|
$key = \substr(\hash('sha256', $key), 0, $this->maxKeyLength);
|
|
}
|
|
$this->cacheKey = $key;
|
|
if ($this->rateLimit) {
|
|
// add prefix
|
|
$rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
|
|
// Hash keys if they exceed $maxKeyLength of 64
|
|
if (\strlen($rateLimitKey) > $this->maxKeyLength) {
|
|
$rateLimitKey = \substr(\hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
|
|
}
|
|
$this->rateLimitCacheKey = $rateLimitKey;
|
|
}
|
|
}
|
|
}
|