ILIAS  release_8 Revision v8.24
CachedKeySet.php
Go to the documentation of this file.
1<?php
2
3namespace Firebase\JWT;
4
5use ArrayAccess;
6use InvalidArgumentException;
7use LogicException;
8use OutOfBoundsException;
9use Psr\Cache\CacheItemInterface;
10use Psr\Cache\CacheItemPoolInterface;
11use Psr\Http\Client\ClientInterface;
12use Psr\Http\Message\RequestFactoryInterface;
13use RuntimeException;
14use UnexpectedValueException;
15
19class CachedKeySet implements ArrayAccess
20{
24 private string $jwksUri;
28 private ClientInterface $httpClient;
32 private RequestFactoryInterface $httpFactory;
36 private CacheItemPoolInterface $cache;
40 private ?int $expiresAfter;
44 private ?CacheItemInterface $cacheItem;
48 private array $keySet;
52 private string $cacheKey;
56 private string $cacheKeyPrefix = 'jwks';
60 private int $maxKeyLength = 64;
64 private bool $rateLimit;
68 private string $rateLimitCacheKey;
72 private int $maxCallsPerMinute = 10;
76 private ?string $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
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
113 public function offsetExists($keyId): bool
114 {
115 return $this->keyIdExists($keyId);
116 }
117
122 public function offsetSet($offset, $value): void
123 {
124 throw new LogicException('Method not implemented');
125 }
126
130 public function offsetUnset($offset): void
131 {
132 throw new LogicException('Method not implemented');
133 }
134
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 if (!$cacheItem->isHit()) {
217 $cacheItem->expiresAfter(1); // # of calls are cached each minute
218 }
219
220 $callsPerMinute = (int) $cacheItem->get();
221 if (++$callsPerMinute > $this->maxCallsPerMinute) {
222 return true;
223 }
224 $cacheItem->set($callsPerMinute);
225 $this->cache->save($cacheItem);
226 return false;
227 }
228
229 private function getCacheItem(): CacheItemInterface
230 {
231 if (\is_null($this->cacheItem)) {
232 $this->cacheItem = $this->cache->getItem($this->cacheKey);
233 }
234
235 return $this->cacheItem;
236 }
237
238 private function setCacheKeys(): void
239 {
240 if (empty($this->jwksUri)) {
241 throw new RuntimeException('JWKS URI is empty');
242 }
243
244 // ensure we do not have illegal characters
245 $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
246
247 // add prefix
248 $key = $this->cacheKeyPrefix . $key;
249
250 // Hash keys if they exceed $maxKeyLength of 64
251 if (\strlen($key) > $this->maxKeyLength) {
252 $key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
253 }
254
255 $this->cacheKey = $key;
256
257 if ($this->rateLimit) {
258 // add prefix
259 $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
260
261 // Hash keys if they exceed $maxKeyLength of 64
262 if (\strlen($rateLimitKey) > $this->maxKeyLength) {
263 $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
264 }
265
266 $this->rateLimitCacheKey = $rateLimitKey;
267 }
268 }
269}
ClientInterface $httpClient
keyIdExists(string $keyId)
offsetSet($offset, $value)
CacheItemInterface $cacheItem
__construct(string $jwksUri, ClientInterface $httpClient, RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, int $expiresAfter=null, bool $rateLimit=false, string $defaultAlg=null)
CacheItemPoolInterface $cache
RequestFactoryInterface $httpFactory
formatJwksForCache(string $jwks)
static parseKey(array $jwk, string $defaultAlg=null)
Parse a JWK key.
Definition: JWK.php:96
$keys
Definition: metadata.php:204
string $kid
Key ID.
Definition: System.php:88
string $key
Consumer key/client ID value.
Definition: System.php:193