Differences Between: [Versions 401 and 403] [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 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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body