Skip to content

Commit b57c063

Browse files
authored
Merge pull request #434 from ONLYOFFICE/develop
Release/8.2.1
2 parents 2afca07 + fd47e10 commit b57c063

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2024
-248
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
2+
23
namespace Firebase\JWT;
34

45
class BeforeValidException extends \UnexpectedValueException
56
{
6-
77
}

3rdparty/jwt/CachedKeySet.php

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
<?php
2+
3+
namespace Firebase\JWT;
4+
5+
use ArrayAccess;
6+
use InvalidArgumentException;
7+
use LogicException;
8+
use OutOfBoundsException;
9+
use Psr\Cache\CacheItemInterface;
10+
use Psr\Cache\CacheItemPoolInterface;
11+
use Psr\Http\Client\ClientInterface;
12+
use Psr\Http\Message\RequestFactoryInterface;
13+
use RuntimeException;
14+
use UnexpectedValueException;
15+
16+
/**
17+
* @implements ArrayAccess<string, Key>
18+
*/
19+
class CachedKeySet implements ArrayAccess
20+
{
21+
/**
22+
* @var string
23+
*/
24+
private $jwksUri;
25+
/**
26+
* @var ClientInterface
27+
*/
28+
private $httpClient;
29+
/**
30+
* @var RequestFactoryInterface
31+
*/
32+
private $httpFactory;
33+
/**
34+
* @var CacheItemPoolInterface
35+
*/
36+
private $cache;
37+
/**
38+
* @var ?int
39+
*/
40+
private $expiresAfter;
41+
/**
42+
* @var ?CacheItemInterface
43+
*/
44+
private $cacheItem;
45+
/**
46+
* @var array<string, array<mixed>>
47+
*/
48+
private $keySet;
49+
/**
50+
* @var string
51+
*/
52+
private $cacheKey;
53+
/**
54+
* @var string
55+
*/
56+
private $cacheKeyPrefix = 'jwks';
57+
/**
58+
* @var int
59+
*/
60+
private $maxKeyLength = 64;
61+
/**
62+
* @var bool
63+
*/
64+
private $rateLimit;
65+
/**
66+
* @var string
67+
*/
68+
private $rateLimitCacheKey;
69+
/**
70+
* @var int
71+
*/
72+
private $maxCallsPerMinute = 10;
73+
/**
74+
* @var string|null
75+
*/
76+
private $defaultAlg;
77+
78+
public function __construct(
79+
string $jwksUri,
80+
ClientInterface $httpClient,
81+
RequestFactoryInterface $httpFactory,
82+
CacheItemPoolInterface $cache,
83+
int $expiresAfter = null,
84+
bool $rateLimit = false,
85+
string $defaultAlg = null
86+
) {
87+
$this->jwksUri = $jwksUri;
88+
$this->httpClient = $httpClient;
89+
$this->httpFactory = $httpFactory;
90+
$this->cache = $cache;
91+
$this->expiresAfter = $expiresAfter;
92+
$this->rateLimit = $rateLimit;
93+
$this->defaultAlg = $defaultAlg;
94+
$this->setCacheKeys();
95+
}
96+
97+
/**
98+
* @param string $keyId
99+
* @return Key
100+
*/
101+
public function offsetGet($keyId): Key
102+
{
103+
if (!$this->keyIdExists($keyId)) {
104+
throw new OutOfBoundsException('Key ID not found');
105+
}
106+
return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg);
107+
}
108+
109+
/**
110+
* @param string $keyId
111+
* @return bool
112+
*/
113+
public function offsetExists($keyId): bool
114+
{
115+
return $this->keyIdExists($keyId);
116+
}
117+
118+
/**
119+
* @param string $offset
120+
* @param Key $value
121+
*/
122+
public function offsetSet($offset, $value): void
123+
{
124+
throw new LogicException('Method not implemented');
125+
}
126+
127+
/**
128+
* @param string $offset
129+
*/
130+
public function offsetUnset($offset): void
131+
{
132+
throw new LogicException('Method not implemented');
133+
}
134+
135+
/**
136+
* @return array<mixed>
137+
*/
138+
private function formatJwksForCache(string $jwks): array
139+
{
140+
$jwks = json_decode($jwks, true);
141+
142+
if (!isset($jwks['keys'])) {
143+
throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
144+
}
145+
146+
if (empty($jwks['keys'])) {
147+
throw new InvalidArgumentException('JWK Set did not contain any keys');
148+
}
149+
150+
$keys = [];
151+
foreach ($jwks['keys'] as $k => $v) {
152+
$kid = isset($v['kid']) ? $v['kid'] : $k;
153+
$keys[(string) $kid] = $v;
154+
}
155+
156+
return $keys;
157+
}
158+
159+
private function keyIdExists(string $keyId): bool
160+
{
161+
if (null === $this->keySet) {
162+
$item = $this->getCacheItem();
163+
// Try to load keys from cache
164+
if ($item->isHit()) {
165+
// item found! retrieve it
166+
$this->keySet = $item->get();
167+
// If the cached item is a string, the JWKS response was cached (previous behavior).
168+
// Parse this into expected format array<kid, jwk> instead.
169+
if (\is_string($this->keySet)) {
170+
$this->keySet = $this->formatJwksForCache($this->keySet);
171+
}
172+
}
173+
}
174+
175+
if (!isset($this->keySet[$keyId])) {
176+
if ($this->rateLimitExceeded()) {
177+
return false;
178+
}
179+
$request = $this->httpFactory->createRequest('GET', $this->jwksUri);
180+
$jwksResponse = $this->httpClient->sendRequest($request);
181+
$this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
182+
183+
if (!isset($this->keySet[$keyId])) {
184+
return false;
185+
}
186+
187+
$item = $this->getCacheItem();
188+
$item->set($this->keySet);
189+
if ($this->expiresAfter) {
190+
$item->expiresAfter($this->expiresAfter);
191+
}
192+
$this->cache->save($item);
193+
}
194+
195+
return true;
196+
}
197+
198+
private function rateLimitExceeded(): bool
199+
{
200+
if (!$this->rateLimit) {
201+
return false;
202+
}
203+
204+
$cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
205+
if (!$cacheItem->isHit()) {
206+
$cacheItem->expiresAfter(1); // # of calls are cached each minute
207+
}
208+
209+
$callsPerMinute = (int) $cacheItem->get();
210+
if (++$callsPerMinute > $this->maxCallsPerMinute) {
211+
return true;
212+
}
213+
$cacheItem->set($callsPerMinute);
214+
$this->cache->save($cacheItem);
215+
return false;
216+
}
217+
218+
private function getCacheItem(): CacheItemInterface
219+
{
220+
if (\is_null($this->cacheItem)) {
221+
$this->cacheItem = $this->cache->getItem($this->cacheKey);
222+
}
223+
224+
return $this->cacheItem;
225+
}
226+
227+
private function setCacheKeys(): void
228+
{
229+
if (empty($this->jwksUri)) {
230+
throw new RuntimeException('JWKS URI is empty');
231+
}
232+
233+
// ensure we do not have illegal characters
234+
$key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
235+
236+
// add prefix
237+
$key = $this->cacheKeyPrefix . $key;
238+
239+
// Hash keys if they exceed $maxKeyLength of 64
240+
if (\strlen($key) > $this->maxKeyLength) {
241+
$key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
242+
}
243+
244+
$this->cacheKey = $key;
245+
246+
if ($this->rateLimit) {
247+
// add prefix
248+
$rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
249+
250+
// Hash keys if they exceed $maxKeyLength of 64
251+
if (\strlen($rateLimitKey) > $this->maxKeyLength) {
252+
$rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
253+
}
254+
255+
$this->rateLimitCacheKey = $rateLimitKey;
256+
}
257+
}
258+
}

3rdparty/jwt/ExpiredException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
2+
23
namespace Firebase\JWT;
34

45
class ExpiredException extends \UnexpectedValueException
56
{
6-
77
}

0 commit comments

Comments
 (0)