ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
CachedKeySet.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Firebase\JWT;
4 
5 use ArrayAccess;
15 
19 class 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(),
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 }
static parseKey(array $jwk, string $defaultAlg=null)
Parse a JWK key.
Definition: JWK.php:96
CacheItemInterface $cacheItem
RequestFactoryInterface $httpFactory
string $kid
Key ID.
Definition: System.php:88
formatJwksForCache(string $jwks)
keyIdExists(string $keyId)
__construct(string $jwksUri, ClientInterface $httpClient, RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, int $expiresAfter=null, bool $rateLimit=false, string $defaultAlg=null)
$keys
Definition: metadata.php:204
offsetSet($offset, $value)
string $key
Consumer key/client ID value.
Definition: System.php:193
CacheItemPoolInterface $cache
ClientInterface $httpClient