Differences Between: [Versions 401 and 402] [Versions 402 and 403]
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 $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody()); 182 183 if (!isset($this->keySet[$keyId])) { 184 return false; 185 } 186 187 $item = $this->getCacheItem(); 188 $item->set($this->keySet); 189 if ($this->expiresAfter) { 190 $item->expiresAfter($this->expiresAfter); 191 } 192 $this->cache->save($item); 193 } 194 195 return true; 196 } 197 198 private function rateLimitExceeded(): bool 199 { 200 if (!$this->rateLimit) { 201 return false; 202 } 203 204 $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); 205 if (!$cacheItem->isHit()) { 206 $cacheItem->expiresAfter(1); // # of calls are cached each minute 207 } 208 209 $callsPerMinute = (int) $cacheItem->get(); 210 if (++$callsPerMinute > $this->maxCallsPerMinute) { 211 return true; 212 } 213 $cacheItem->set($callsPerMinute); 214 $this->cache->save($cacheItem); 215 return false; 216 } 217 218 private function getCacheItem(): CacheItemInterface 219 { 220 if (\is_null($this->cacheItem)) { 221 $this->cacheItem = $this->cache->getItem($this->cacheKey); 222 } 223 224 return $this->cacheItem; 225 } 226 227 private function setCacheKeys(): void 228 { 229 if (empty($this->jwksUri)) { 230 throw new RuntimeException('JWKS URI is empty'); 231 } 232 233 // ensure we do not have illegal characters 234 $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri); 235 236 // add prefix 237 $key = $this->cacheKeyPrefix . $key; 238 239 // Hash keys if they exceed $maxKeyLength of 64 240 if (\strlen($key) > $this->maxKeyLength) { 241 $key = substr(hash('sha256', $key), 0, $this->maxKeyLength); 242 } 243 244 $this->cacheKey = $key; 245 246 if ($this->rateLimit) { 247 // add prefix 248 $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key; 249 250 // Hash keys if they exceed $maxKeyLength of 64 251 if (\strlen($rateLimitKey) > $this->maxKeyLength) { 252 $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength); 253 } 254 255 $this->rateLimitCacheKey = $rateLimitKey; 256 } 257 } 258 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body