PluginProbe
WP-Stateless – Google Cloud Storage / trunk
WP-Stateless – Google Cloud Storage vtrunk
4.4.3 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.3.0 2.3.1 2.3.2 3.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 All 62 releases
wp-stateless / vendor / firebase / php-jwt / src / CachedKeySet.php

CachedKeySet.php in WP-Stateless – Google Cloud Storage trunk, at vendor/firebase/php-jwt/src/CachedKeySet.php

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