Skip to content

Commit

Permalink
Merge pull request #860 from ONLYOFFICE/feature/nc-27
Browse files Browse the repository at this point in the history
Release/8.2.0
  • Loading branch information
LinneyS authored Aug 14, 2023
2 parents 2ea8bec + 3cf98ff commit 04fc9bd
Show file tree
Hide file tree
Showing 56 changed files with 2,371 additions and 303 deletions.
2 changes: 1 addition & 1 deletion 3rdparty/jwt/BeforeValidException.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php

namespace Firebase\JWT;

class BeforeValidException extends \UnexpectedValueException
{

}
258 changes: 258 additions & 0 deletions 3rdparty/jwt/CachedKeySet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
<?php

namespace Firebase\JWT;

use ArrayAccess;
use InvalidArgumentException;
use LogicException;
use OutOfBoundsException;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Client\ClientInterface;
use 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,
ClientInterface $httpClient,
RequestFactoryInterface $httpFactory,
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): Key
{
if (!$this->keyIdExists($keyId)) {
throw new OutOfBoundsException('Key ID not found');
}
return 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);
$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(): 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;
}
}
}
2 changes: 1 addition & 1 deletion 3rdparty/jwt/ExpiredException.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php

namespace Firebase\JWT;

class ExpiredException extends \UnexpectedValueException
{

}
Loading

0 comments on commit 04fc9bd

Please sign in to comment.