PluginProbe ʕ •ᴥ•ʔ
AI Agent by SiteGround / 1.1.7
AI Agent by SiteGround v1.1.7
1.2.2 1.2.1 1.2.0 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 trunk 1.1.3
sg-ai-studio / vendor / firebase / php-jwt / src / CachedKeySet.php
sg-ai-studio / vendor / firebase / php-jwt / src Last commit date
BeforeValidException.php 1 month ago CachedKeySet.php 1 month ago ExpiredException.php 1 month ago JWK.php 1 month ago JWT.php 1 month ago JWTExceptionWithPayloadInterface.php 1 month ago Key.php 1 month ago SignatureInvalidException.php 1 month ago
CachedKeySet.php
269 lines
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 if (!$cacheItem->isHit()) {
216 $cacheItem->expiresAfter(1); // # of calls are cached each minute
217 }
218
219 $callsPerMinute = (int) $cacheItem->get();
220 if (++$callsPerMinute > $this->maxCallsPerMinute) {
221 return true;
222 }
223 $cacheItem->set($callsPerMinute);
224 $this->cache->save($cacheItem);
225 return false;
226 }
227
228 private function getCacheItem(): CacheItemInterface
229 {
230 if (\is_null($this->cacheItem)) {
231 $this->cacheItem = $this->cache->getItem($this->cacheKey);
232 }
233
234 return $this->cacheItem;
235 }
236
237 private function setCacheKeys(): void
238 {
239 if (empty($this->jwksUri)) {
240 throw new RuntimeException('JWKS URI is empty');
241 }
242
243 // ensure we do not have illegal characters
244 $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
245
246 // add prefix
247 $key = $this->cacheKeyPrefix . $key;
248
249 // Hash keys if they exceed $maxKeyLength of 64
250 if (\strlen($key) > $this->maxKeyLength) {
251 $key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
252 }
253
254 $this->cacheKey = $key;
255
256 if ($this->rateLimit) {
257 // add prefix
258 $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
259
260 // Hash keys if they exceed $maxKeyLength of 64
261 if (\strlen($rateLimitKey) > $this->maxKeyLength) {
262 $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
263 }
264
265 $this->rateLimitCacheKey = $rateLimitKey;
266 }
267 }
268 }
269