See Release Notes
Long Term Support Release
Differences Between: [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 3 namespace Firebase\JWT; 4 5 use ArrayAccess; 6 use LogicException; 7 use OutOfBoundsException; 8 use Psr\Cache\CacheItemInterface; 9 use Psr\Cache\CacheItemPoolInterface; 10 use Psr\Http\Client\ClientInterface; 11 use Psr\Http\Message\RequestFactoryInterface; 12 use RuntimeException; 13 14 /** 15 * @implements ArrayAccess<string, Key> 16 */ 17 class CachedKeySet implements ArrayAccess 18 { 19 /** 20 * @var string 21 */ 22 private $jwksUri; 23 /** 24 * @var ClientInterface 25 */ 26 private $httpClient; 27 /** 28 * @var RequestFactoryInterface 29 */ 30 private $httpFactory; 31 /** 32 * @var CacheItemPoolInterface 33 */ 34 private $cache; 35 /** 36 * @var ?int 37 */ 38 private $expiresAfter; 39 /** 40 * @var ?CacheItemInterface 41 */ 42 private $cacheItem; 43 /** 44 * @var array<string, Key> 45 */ 46 private $keySet; 47 /** 48 * @var string 49 */ 50 private $cacheKey; 51 /** 52 * @var string 53 */ 54 private $cacheKeyPrefix = 'jwks'; 55 /** 56 * @var int 57 */ 58 private $maxKeyLength = 64; 59 /** 60 * @var bool 61 */ 62 private $rateLimit; 63 /** 64 * @var string 65 */ 66 private $rateLimitCacheKey; 67 /** 68 * @var int 69 */ 70 private $maxCallsPerMinute = 10; 71 /** 72 * @var string|null 73 */ 74 private $defaultAlg; 75 76 public function __construct( 77 string $jwksUri, 78 ClientInterface $httpClient, 79 RequestFactoryInterface $httpFactory, 80 CacheItemPoolInterface $cache, 81 int $expiresAfter = null, 82 bool $rateLimit = false, 83 string $defaultAlg = null 84 ) { 85 $this->jwksUri = $jwksUri; 86 $this->httpClient = $httpClient; 87 $this->httpFactory = $httpFactory; 88 $this->cache = $cache; 89 $this->expiresAfter = $expiresAfter; 90 $this->rateLimit = $rateLimit; 91 $this->defaultAlg = $defaultAlg; 92 $this->setCacheKeys(); 93 } 94 95 /** 96 * @param string $keyId 97 * @return Key 98 */ 99 public function offsetGet($keyId): Key 100 { 101 if (!$this->keyIdExists($keyId)) { 102 throw new OutOfBoundsException('Key ID not found'); 103 } 104 return $this->keySet[$keyId]; 105 } 106 107 /** 108 * @param string $keyId 109 * @return bool 110 */ 111 public function offsetExists($keyId): bool 112 { 113 return $this->keyIdExists($keyId); 114 } 115 116 /** 117 * @param string $offset 118 * @param Key $value 119 */ 120 public function offsetSet($offset, $value): void 121 { 122 throw new LogicException('Method not implemented'); 123 } 124 125 /** 126 * @param string $offset 127 */ 128 public function offsetUnset($offset): void 129 { 130 throw new LogicException('Method not implemented'); 131 } 132 133 private function keyIdExists(string $keyId): bool 134 { 135 if (null === $this->keySet) { 136 $item = $this->getCacheItem(); 137 // Try to load keys from cache 138 if ($item->isHit()) { 139 // item found! Return it 140 $jwks = $item->get(); 141 $this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg); 142 } 143 } 144 145 if (!isset($this->keySet[$keyId])) { 146 if ($this->rateLimitExceeded()) { 147 return false; 148 } 149 $request = $this->httpFactory->createRequest('get', $this->jwksUri); 150 $jwksResponse = $this->httpClient->sendRequest($request); 151 $jwks = (string) $jwksResponse->getBody(); 152 $this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg); 153 154 if (!isset($this->keySet[$keyId])) { 155 return false; 156 } 157 158 $item = $this->getCacheItem(); 159 $item->set($jwks); 160 if ($this->expiresAfter) { 161 $item->expiresAfter($this->expiresAfter); 162 } 163 $this->cache->save($item); 164 } 165 166 return true; 167 } 168 169 private function rateLimitExceeded(): bool 170 { 171 if (!$this->rateLimit) { 172 return false; 173 } 174 175 $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); 176 if (!$cacheItem->isHit()) { 177 $cacheItem->expiresAfter(1); // # of calls are cached each minute 178 } 179 180 $callsPerMinute = (int) $cacheItem->get(); 181 if (++$callsPerMinute > $this->maxCallsPerMinute) { 182 return true; 183 } 184 $cacheItem->set($callsPerMinute); 185 $this->cache->save($cacheItem); 186 return false; 187 } 188 189 private function getCacheItem(): CacheItemInterface 190 { 191 if (\is_null($this->cacheItem)) { 192 $this->cacheItem = $this->cache->getItem($this->cacheKey); 193 } 194 195 return $this->cacheItem; 196 } 197 198 private function setCacheKeys(): void 199 { 200 if (empty($this->jwksUri)) { 201 throw new RuntimeException('JWKS URI is empty'); 202 } 203 204 // ensure we do not have illegal characters 205 $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri); 206 207 // add prefix 208 $key = $this->cacheKeyPrefix . $key; 209 210 // Hash keys if they exceed $maxKeyLength of 64 211 if (\strlen($key) > $this->maxKeyLength) { 212 $key = substr(hash('sha256', $key), 0, $this->maxKeyLength); 213 } 214 215 $this->cacheKey = $key; 216 217 if ($this->rateLimit) { 218 // add prefix 219 $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key; 220 221 // Hash keys if they exceed $maxKeyLength of 64 222 if (\strlen($rateLimitKey) > $this->maxKeyLength) { 223 $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength); 224 } 225 226 $this->rateLimitCacheKey = $rateLimitKey; 227 } 228 } 229 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body