PluginProbe
Authorizer / 3.14.2
Authorizer v3.14.2
3.15.3 3.15.2 3.15.1 3.15.0 3.14.3 3.14.4 3.14.2 3.14.1 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.9.0 2.9.1 2.9.10 2.9.11 2.9.12 2.9.13 2.9.2 2.9.3 2.9.6 All 126 releases
authorizer / vendor / firebase / php-jwt / src / CachedKeySet.php

CachedKeySet.php in Authorizer 3.14.2, at vendor/firebase/php-jwt/src/CachedKeySet.php

276 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 if ($jwksResponse->getStatusCode() !== 200) {
182 throw new UnexpectedValueException(
183 \sprintf(
184 'HTTP Error: %d %s for URI "%s"',
185 $jwksResponse->getStatusCode(),
186 $jwksResponse->getReasonPhrase(),
187 $this->jwksUri,
188 ),
189 $jwksResponse->getStatusCode()
190 );
191 }
192 $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
193
194 if (!isset($this->keySet[$keyId])) {
195 return false;
196 }
197
198 $item = $this->getCacheItem();
199 $item->set($this->keySet);
200 if ($this->expiresAfter) {
201 $item->expiresAfter($this->expiresAfter);
202 }
203 $this->cache->save($item);
204 }
205
206 return true;
207 }
208
209 private function rateLimitExceeded(): bool
210 {
211 if (!$this->rateLimit) {
212 return false;
213 }
214
215 $cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
216
217 $cacheItemData = [];
218 if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) {
219 $cacheItemData = $data;
220 }
221
222 $callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0;
223 $expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC'));
224
225 if (++$callsPerMinute > $this->maxCallsPerMinute) {
226 return true;
227 }
228
229 $cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]);
230 $cacheItem->expiresAt($expiry);
231 $this->cache->save($cacheItem);
232 return false;
233 }
234
235 private function getCacheItem(): CacheItemInterface
236 {
237 if (\is_null($this->cacheItem)) {
238 $this->cacheItem = $this->cache->getItem($this->cacheKey);
239 }
240
241 return $this->cacheItem;
242 }
243
244 private function setCacheKeys(): void
245 {
246 if (empty($this->jwksUri)) {
247 throw new RuntimeException('JWKS URI is empty');
248 }
249
250 // ensure we do not have illegal characters
251 $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
252
253 // add prefix
254 $key = $this->cacheKeyPrefix . $key;
255
256 // Hash keys if they exceed $maxKeyLength of 64
257 if (\strlen($key) > $this->maxKeyLength) {
258 $key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
259 }
260
261 $this->cacheKey = $key;
262
263 if ($this->rateLimit) {
264 // add prefix
265 $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
266
267 // Hash keys if they exceed $maxKeyLength of 64
268 if (\strlen($rateLimitKey) > $this->maxKeyLength) {
269 $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
270 }
271
272 $this->rateLimitCacheKey = $rateLimitKey;
273 }
274 }
275 }
276