From 63ae30a0271a54d0ff3e9e26102c01c9ba7bb2a6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 8 Feb 2022 09:58:11 -0600 Subject: [PATCH 01/36] WIP: add CachedKeySet --- src/CachedKeySet.php | 121 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/CachedKeySet.php diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php new file mode 100644 index 00000000..5983fda8 --- /dev/null +++ b/src/CachedKeySet.php @@ -0,0 +1,121 @@ +jwkUri = $jwkUri; + $this->cacheKey = $this->getCacheKey($jwkUri); + $this->http = $http; + $this->cache = $cache; + $this->expiresIn = $expiresIn; + } + + public function offsetGet($keyId) + { + if (!$this->keyIdExists($kid)) { + throw new OutOfBoundsException('Key ID not found'); + } + return $this->keySet[$keyId]; + } + + public function offsetExists($keyId) + { + return $this->keyIdExists($keyId); + } + + public function offsetSet($offset, $value) + { + throw new LogicException('Method not implemented'); + } + + public function offsetUnset($offset) + { + throw new LogicException('Method not implemented'); + } + + private function fetchFromUrl() + { + // fetch the keys and save them to the cache + $jwks = $this->http->get($jwkUri); + $keySet = static::parseKeySet($jwks); + + if ($cache) { + $item->set($keySet); + $item->expiresAfter($expiresAfter); + $cache->save($item); + } + + return $keySet; + } + + private function keyIdExists($keyId) + { + $keySetToCache = null; + if (null === $this->keySet) { + $item = $this->cache->getItem($this->cacheKey); + // Try to load keys from cache + if ($item->isHit()) { + // item found! Return it + $this->keySet = $item->get(); + } + } + + if (!isset($this->keySet[$keyId])) { + $jwk = $this->http->get($this->jwtUri); + $this->keySet = $keySetToCache = JWK::parseKeySet($jwk); + + if (!isset($this->keySet[$keyId])) { + return false; + } + } + + if ($keySetToCache) { + $item->set($keySetToCache); + $this->cache->save($item); + } + + return true; + } + + private function getCacheKey($jwkUri) + { + if (is_null($jwkUri)) { + throw new RuntimeException('JWK URI is empty'); + } + + // ensure we do not have illegal characters + $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $jwkUri); + + // add prefix + $key = $this->cacheKeyPrefix . $key; + + // Hash keys if they exceed $maxKeyLength of 64 + if (strlen($key) > $this->maxKeyLength) { + $key = substr(hash('sha256', $key), 0, $this->maxKeyLength); + } + + return $key; + } +} \ No newline at end of file From 6fa256e09ea2bc6656ca54074361a978230e6a93 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 8 Feb 2022 17:52:26 -0600 Subject: [PATCH 02/36] add tests --- composer.json | 5 +++- src/CachedKeySet.php | 55 +++++++++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/composer.json b/composer.json index 6146e2dc..8d7e4397 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,9 @@ } }, "require-dev": { - "phpunit/phpunit": ">=4.8 <=9" + "phpunit/phpunit": ">=4.8 <=9", + "psr/cache": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" } } diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index 5983fda8..b562cf3a 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -4,6 +4,8 @@ use ArrayAccess; use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\RequestInterface; use Psr\Cache\CacheItemPoolInterface; use LogicException; use OutOfBoundsException; @@ -12,29 +14,34 @@ class CachedKeySet implements ArrayAccess { private $jwkUri; + private $httpClient; + private $httpFactory; private $cache; - private $http; - private $expiresIn; + private $expiresAfter; + + private $cacheItem; private $keySet; private $cacheKeyPrefix = 'jwk'; private $maxKeyLength = 64; public function __construct( $jwkUri, - ClientInterface $http, + ClientInterface $httpClient, + RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, - $expiresIn = null + $expiresAfter = null ) { $this->jwkUri = $jwkUri; $this->cacheKey = $this->getCacheKey($jwkUri); - $this->http = $http; + $this->httpClient = $httpClient; + $this->httpFactory = $httpFactory; $this->cache = $cache; - $this->expiresIn = $expiresIn; + $this->expiresAfter = $expiresAfter; } public function offsetGet($keyId) { - if (!$this->keyIdExists($kid)) { + if (!$this->keyIdExists($keyId)) { throw new OutOfBoundsException('Key ID not found'); } return $this->keySet[$keyId]; @@ -55,26 +62,11 @@ public function offsetUnset($offset) throw new LogicException('Method not implemented'); } - private function fetchFromUrl() - { - // fetch the keys and save them to the cache - $jwks = $this->http->get($jwkUri); - $keySet = static::parseKeySet($jwks); - - if ($cache) { - $item->set($keySet); - $item->expiresAfter($expiresAfter); - $cache->save($item); - } - - return $keySet; - } - private function keyIdExists($keyId) { $keySetToCache = null; if (null === $this->keySet) { - $item = $this->cache->getItem($this->cacheKey); + $item = $this->getCacheItem(); // Try to load keys from cache if ($item->isHit()) { // item found! Return it @@ -83,7 +75,9 @@ private function keyIdExists($keyId) } if (!isset($this->keySet[$keyId])) { - $jwk = $this->http->get($this->jwtUri); + $request = $this->httpFactory->createRequest('get', $this->jwkUri); + $jwkResponse = $this->httpClient->sendRequest($request); + $jwk = json_decode((string) $jwkResponse->getBody(), true); $this->keySet = $keySetToCache = JWK::parseKeySet($jwk); if (!isset($this->keySet[$keyId])) { @@ -92,13 +86,26 @@ private function keyIdExists($keyId) } if ($keySetToCache) { + $item = $this->getCacheItem(); $item->set($keySetToCache); + if ($this->expiresAfter) { + $item->expiresAfter($this->expiresAfter); + } $this->cache->save($item); } return true; } + private function getCacheItem() + { + if ($this->cacheItem) { + return $this->cacheItem; + } + + return $this->cacheItem = $this->cache->getItem($this->cacheKey); + } + private function getCacheKey($jwkUri) { if (is_null($jwkUri)) { From 66881d7a715930bc8f4e76df1b2703e95939b5d3 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 8 Feb 2022 17:53:46 -0600 Subject: [PATCH 03/36] fix cs --- src/CachedKeySet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index b562cf3a..e16fa893 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -125,4 +125,4 @@ private function getCacheKey($jwkUri) return $key; } -} \ No newline at end of file +} From ff7339f0314812436396d9937d4feb22a27feaa5 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 9 Feb 2022 10:27:14 -0600 Subject: [PATCH 04/36] remove require-dev deps --- composer.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 8d7e4397..6146e2dc 100644 --- a/composer.json +++ b/composer.json @@ -31,9 +31,6 @@ } }, "require-dev": { - "phpunit/phpunit": ">=4.8 <=9", - "psr/cache": "^1.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0" + "phpunit/phpunit": ">=4.8 <=9" } } From dc6e704975b95d0e867ef86e94e7ac84df7b9b0d Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 9 Feb 2022 11:30:50 -0600 Subject: [PATCH 05/36] add integration test and integration test config --- .github/workflows/tests.yml | 21 +++ phpunit-system.xml.dist | 18 +++ phpunit.xml.dist | 1 + tests/CachedKeySetTest.php | 255 ++++++++++++++++++++++++++++++++++++ tests/bootstrap-system.php | 172 ++++++++++++++++++++++++ 5 files changed, 467 insertions(+) create mode 100644 phpunit-system.xml.dist create mode 100644 tests/CachedKeySetTest.php create mode 100644 tests/bootstrap-system.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 50d8a5f0..e929eea9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,27 @@ jobs: - name: Run Script run: vendor/bin/phpunit + test_system: + runs-on: ubuntu-latest + strategy: + matrix: + php: [ "8.0", "8.1" ] + name: PHP ${{matrix.php }} Unit Test + steps: + - uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - name: Install Dependencies + uses: nick-invision/retry@v1 + with: + timeout_minutes: 10 + max_attempts: 3 + command: composer require guzzlehttp/guzzle psr/cache + - name: Run Script + run: vendor/bin/phpunit -c phpunit-system.xml.dist + # use dockerfiles for old versions of php (setup-php times out for those). test_php55: name: "PHP 5.5 Unit Test" diff --git a/phpunit-system.xml.dist b/phpunit-system.xml.dist new file mode 100644 index 00000000..f5874edd --- /dev/null +++ b/phpunit-system.xml.dist @@ -0,0 +1,18 @@ + + + + + + ./tests/CachedKeySetTest.php + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 092a662c..430f6299 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -13,6 +13,7 @@ ./tests + ./tests/CachedKeySetTest.php diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php new file mode 100644 index 00000000..418c5a31 --- /dev/null +++ b/tests/CachedKeySetTest.php @@ -0,0 +1,255 @@ +setExpectedException( + 'LogicException', + 'Method not implemented' + ); + + $cachedKeySet = new CachedKeySet( + $this->testJwkUri, + $this->prophesize('Psr\Http\Client\ClientInterface')->reveal(), + $this->prophesize('Psr\Http\Message\RequestFactoryInterface')->reveal(), + $this->prophesize('Psr\Cache\CacheItemPoolInterface')->reveal() + ); + + $cachedKeySet['foo'] = 'bar'; + } + + public function testOffsetUnsetThrowsException() + { + $this->setExpectedException( + 'LogicException', + 'Method not implemented' + ); + + $cachedKeySet = new CachedKeySet( + $this->testJwkUri, + $this->prophesize('Psr\Http\Client\ClientInterface')->reveal(), + $this->prophesize('Psr\Http\Message\RequestFactoryInterface')->reveal(), + $this->prophesize('Psr\Cache\CacheItemPoolInterface')->reveal() + ); + + unset($cachedKeySet['foo']); + } + + public function testOutOfBoundsThrowsException() + { + $this->setExpectedException( + 'OutOfBoundsException', + 'Key ID not found' + ); + + $cachedKeySet = new CachedKeySet( + $this->testJwkUri, + $this->getMockHttpClient($this->testJwk1), + $this->getMockHttpFactory(), + $this->getMockEmptyCache() + ); + + // keyID doesn't exist + $cachedKeySet['bar']; + } + + public function testWithExistingKeyId() + { + $cachedKeySet = new CachedKeySet( + $this->testJwkUri, + $this->getMockHttpClient($this->testJwk1), + $this->getMockHttpFactory(), + $this->getMockEmptyCache() + ); + $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['foo']); + $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + } + + public function testKeyIdIsCached() + { + $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + $cacheItem->isHit() + ->willReturn(true); + $cacheItem->get() + ->willReturn(JWK::parseKeySet(json_decode($this->testJwk1, true))); + + $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cache->getItem($this->testJwkUriKey) + ->willReturn($cacheItem->reveal()); + $cache->save(Argument::any()) + ->willReturn(true); + + $cachedKeySet = new CachedKeySet( + $this->testJwkUri, + $this->prophesize('Psr\Http\Client\ClientInterface')->reveal(), + $this->prophesize('Psr\Http\Message\RequestFactoryInterface')->reveal(), + $cache->reveal() + ); + $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['foo']); + $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + } + + public function testCachedKeyIdRefresh() + { + $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + $cacheItem->isHit() + ->shouldBeCalledOnce() + ->willReturn(true); + $cacheItem->get() + ->shouldBeCalledOnce() + ->willReturn(JWK::parseKeySet(json_decode($this->testJwk1, true))); + $cacheItem->set(Argument::any()) + ->shouldBeCalledOnce() + ->willReturn(true); + + $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cache->getItem($this->testJwkUriKey) + ->shouldBeCalledOnce() + ->willReturn($cacheItem->reveal()); + $cache->save(Argument::any()) + ->shouldBeCalledOnce() + ->willReturn(true); + + $cachedKeySet = new CachedKeySet( + $this->testJwkUri, + $this->getMockHttpClient($this->testJwk2), // updated JWK + $this->getMockHttpFactory(), + $cache->reveal() + ); + $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['foo']); + $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + + $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['bar']); + $this->assertEquals('bar', $cachedKeySet['bar']->getAlgorithm()); + } + + private function getMockHttpClient($testJwk) + { + $body = $this->prophesize('Psr\Http\Message\StreamInterface'); + $body->__toString() + ->shouldBeCalledOnce() + ->willReturn($testJwk); + + $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); + $response->getBody() + ->shouldBeCalledOnce() + ->willReturn($body->reveal()); + + $http = $this->prophesize('Psr\Http\Client\ClientInterface'); + $http->sendRequest(Argument::any()) + ->shouldBeCalledOnce() + ->willReturn($response->reveal()); + + return $http->reveal(); + } + + private function getMockHttpFactory() + { + $request = $this->prophesize('Psr\Http\Message\RequestInterface'); + $factory = $this->prophesize('Psr\Http\Message\RequestFactoryInterface'); + $factory->createRequest('get', $this->testJwkUri) + ->shouldBeCalledOnce() + ->willReturn($request->reveal()); + + return $factory->reveal(); + } + + private function getMockEmptyCache() + { + $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + $cacheItem->isHit() + ->shouldBeCalledOnce() + ->willReturn(false); + $cacheItem->set(Argument::any()) + ->willReturn(true); + + $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cache->getItem($this->testJwkUriKey) + ->shouldBeCalledOnce() + ->willReturn($cacheItem->reveal()); + $cache->save(Argument::any()) + ->willReturn(true); + + return $cache->reveal(); + } + + public function testCacheItemWithExpiresAfter() + { + $expiresAfter = 10; + $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + $cacheItem->isHit() + ->shouldBeCalledOnce() + ->willReturn(false); + $cacheItem->set(Argument::any()) + ->shouldBeCalledOnce(); + $cacheItem->expiresAfter($expiresAfter) + ->shouldBeCalledOnce(); + + $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cache->getItem($this->testJwkUriKey) + ->shouldBeCalledOnce() + ->willReturn($cacheItem->reveal()); + $cache->save(Argument::any()) + ->shouldBeCalledOnce(); + + $cachedKeySet = new CachedKeySet( + $this->testJwkUri, + $this->getMockHttpClient($this->testJwk1), + $this->getMockHttpFactory(), + $cache->reveal(), + $expiresAfter + ); + $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['foo']); + $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + } + + public function testFullIntegration() + { + if (!class_exists(TestMemoryCacheItemPool::class)) { + $this->markTestSkipped('Use phpunit-system.xml.dist to run this tests'); + } + + $cache = new TestMemoryCacheItemPool(); + $http = new \GuzzleHttp\Client(); + $factory = new \GuzzleHttp\Psr7\HttpFactory(); + + $cachedKeySet = new CachedKeySet( + $this->googleRsaUri, + $http, + $factory, + $cache + ); + + $this->assertArrayHasKey('182e450a35a2081faa1d9ae1d2d75a0f23d91df8', $cachedKeySet); + } + + /* + * For compatibility with PHPUnit 4.8 and PHP < 5.6 + */ + public function setExpectedException($exceptionName, $message = '', $code = null) + { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + if ($message) { + $this->expectExceptionMessage($message); + } + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } +} diff --git a/tests/bootstrap-system.php b/tests/bootstrap-system.php new file mode 100644 index 00000000..7f0d7f9f --- /dev/null +++ b/tests/bootstrap-system.php @@ -0,0 +1,172 @@ +getItems([$key])); + } + + public function getItems(array $keys = []) + { + $items = []; + + foreach ($keys as $key) { + $items[$key] = $this->hasItem($key) ? clone $this->items[$key] : new TestMemoryCacheItem($key); + } + + return $items; + } + + public function hasItem($key) + { + return isset($this->items[$key]) && $this->items[$key]->isHit(); + } + + public function clear() + { + $this->items = []; + $this->deferredItems = []; + + return true; + } + + public function deleteItem($key) + { + return $this->deleteItems([$key]); + } + + public function deleteItems(array $keys) + { + foreach ($keys as $key) { + unset($this->items[$key]); + } + + return true; + } + + public function save(CacheItemInterface $item) + { + $this->items[$item->getKey()] = $item; + + return true; + } + + public function saveDeferred(CacheItemInterface $item) + { + $this->deferredItems[$item->getKey()] = $item; + + return true; + } + + public function commit() + { + foreach ($this->deferredItems as $item) { + $this->save($item); + } + + $this->deferredItems = []; + + return true; + } +} + +/** + * A cache item. + */ +final class TestMemoryCacheItem implements CacheItemInterface +{ + private $key; + private $value; + private $expiration; + private $isHit = false; + + public function __construct($key) + { + $this->key = $key; + } + + public function getKey() + { + return $this->key; + } + + public function get() + { + return $this->isHit() ? $this->value : null; + } + + public function isHit() + { + if (!$this->isHit) { + return false; + } + + if ($this->expiration === null) { + return true; + } + + return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp(); + } + + public function set($value) + { + $this->isHit = true; + $this->value = $value; + + return $this; + } + + public function expiresAt($expiration) + { + $this->expiration = $expiration; + return $this; + } + + public function expiresAfter($time) + { + $this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S")); + return $this; + } + + protected function currentTime() + { + return new \DateTime('now', new \DateTimeZone('UTC')); + } +} From 9051308f49c029fa2c12aa2afdb513605752ee93 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 9 Feb 2022 12:43:41 -0600 Subject: [PATCH 06/36] add more tests --- src/JWT.php | 4 ++ tests/CachedKeySetTest.php | 141 ++++++++++++++++++++++++------------- 2 files changed, 95 insertions(+), 50 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 6130c59c..d6416c59 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -394,6 +394,10 @@ private static function getKey($keyOrKeyArray, $kid = null) return $keyOrKeyArray; } + if ($keyOrKeyArray instanceof CachedKeySet) { + return $keyOrKeyArray[$kid]; + } + if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { foreach ($keyOrKeyArray as $keyId => $key) { if (!$key instanceof Key) { diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index 418c5a31..1cecb31c 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -138,6 +138,97 @@ public function testCachedKeyIdRefresh() $this->assertEquals('bar', $cachedKeySet['bar']->getAlgorithm()); } + public function testCacheItemWithExpiresAfter() + { + $expiresAfter = 10; + $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + $cacheItem->isHit() + ->shouldBeCalledOnce() + ->willReturn(false); + $cacheItem->set(Argument::any()) + ->shouldBeCalledOnce(); + $cacheItem->expiresAfter($expiresAfter) + ->shouldBeCalledOnce(); + + $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cache->getItem($this->testJwkUriKey) + ->shouldBeCalledOnce() + ->willReturn($cacheItem->reveal()); + $cache->save(Argument::any()) + ->shouldBeCalledOnce(); + + $cachedKeySet = new CachedKeySet( + $this->testJwkUri, + $this->getMockHttpClient($this->testJwk1), + $this->getMockHttpFactory(), + $cache->reveal(), + $expiresAfter + ); + $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['foo']); + $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + } + + public function testJwtVerify() + { + $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); + $payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds')); + $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); + + $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + $cacheItem->isHit() + ->willReturn(true); + $cacheItem->get() + ->willReturn(JWK::parseKeySet( + json_decode(file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), true) + )); + + $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cache->getItem($this->testJwkUriKey) + ->willReturn($cacheItem->reveal()); + + $cachedKeySet = new CachedKeySet( + $this->testJwkUri, + $this->prophesize('Psr\Http\Client\ClientInterface')->reveal(), + $this->prophesize('Psr\Http\Message\RequestFactoryInterface')->reveal(), + $cache->reveal() + ); + + $result = JWT::decode($msg, $cachedKeySet); + + $this->assertEquals("foo", $result->sub); + } + + /** + * @dataProvider provideFullIntegration + */ + public function testFullIntegration($jwkUri, $kid) + { + if (!class_exists(TestMemoryCacheItemPool::class)) { + $this->markTestSkipped('Use phpunit-system.xml.dist to run this tests'); + } + + $cache = new TestMemoryCacheItemPool(); + $http = new \GuzzleHttp\Client(); + $factory = new \GuzzleHttp\Psr7\HttpFactory(); + + $cachedKeySet = new CachedKeySet( + $jwkUri, + $http, + $factory, + $cache + ); + + $this->assertArrayHasKey($kid, $cachedKeySet); + } + + public function provideFullIntegration() + { + return [ + [$this->googleRsaUri, '182e450a35a2081faa1d9ae1d2d75a0f23d91df8'], + // [$this->googleEcUri, 'LYyP2g'] + ]; + } + private function getMockHttpClient($testJwk) { $body = $this->prophesize('Psr\Http\Message\StreamInterface'); @@ -188,56 +279,6 @@ private function getMockEmptyCache() return $cache->reveal(); } - public function testCacheItemWithExpiresAfter() - { - $expiresAfter = 10; - $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); - $cacheItem->isHit() - ->shouldBeCalledOnce() - ->willReturn(false); - $cacheItem->set(Argument::any()) - ->shouldBeCalledOnce(); - $cacheItem->expiresAfter($expiresAfter) - ->shouldBeCalledOnce(); - - $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); - $cache->getItem($this->testJwkUriKey) - ->shouldBeCalledOnce() - ->willReturn($cacheItem->reveal()); - $cache->save(Argument::any()) - ->shouldBeCalledOnce(); - - $cachedKeySet = new CachedKeySet( - $this->testJwkUri, - $this->getMockHttpClient($this->testJwk1), - $this->getMockHttpFactory(), - $cache->reveal(), - $expiresAfter - ); - $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['foo']); - $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); - } - - public function testFullIntegration() - { - if (!class_exists(TestMemoryCacheItemPool::class)) { - $this->markTestSkipped('Use phpunit-system.xml.dist to run this tests'); - } - - $cache = new TestMemoryCacheItemPool(); - $http = new \GuzzleHttp\Client(); - $factory = new \GuzzleHttp\Psr7\HttpFactory(); - - $cachedKeySet = new CachedKeySet( - $this->googleRsaUri, - $http, - $factory, - $cache - ); - - $this->assertArrayHasKey('182e450a35a2081faa1d9ae1d2d75a0f23d91df8', $cachedKeySet); - } - /* * For compatibility with PHPUnit 4.8 and PHP < 5.6 */ From e366659c8bfc7264e6d05759d64086574a76d8f6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 9 Feb 2022 13:51:25 -0600 Subject: [PATCH 07/36] use psr/cache 2.0.0 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e929eea9..2bfaafd8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,7 +46,7 @@ jobs: with: timeout_minutes: 10 max_attempts: 3 - command: composer require guzzlehttp/guzzle psr/cache + command: composer require guzzlehttp/guzzle psr/cache:2.0.0 - name: Run Script run: vendor/bin/phpunit -c phpunit-system.xml.dist From 006cac96e41948a99de3f05cdfac58f1a3d7e54d Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 16 Feb 2022 06:17:37 -0800 Subject: [PATCH 08/36] Update bootstrap-system.php --- tests/bootstrap-system.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bootstrap-system.php b/tests/bootstrap-system.php index 7f0d7f9f..3cfee04a 100644 --- a/tests/bootstrap-system.php +++ b/tests/bootstrap-system.php @@ -27,7 +27,7 @@ // For cache objects if (!class_exists(CacheItemPoolInterface::class)) { - // throw new RuntimeException('You must run "composer require psr/cache" to execute the integration tests'); + throw new RuntimeException('You must run "composer require psr/cache" to execute the integration tests'); } /** From 82688dbcee0af94d519a35c4efce3f124622ed37 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 16 Feb 2022 06:20:29 -0800 Subject: [PATCH 09/36] Update CachedKeySetTest.php --- tests/CachedKeySetTest.php | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index 1cecb31c..6dfd63a1 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -5,6 +5,8 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Client\ClientInterface; use Prophecy\Argument; +use LogicException; +use OutOfBoundsException; class CachedKeySetTest extends TestCase { @@ -18,10 +20,8 @@ class CachedKeySetTest extends TestCase public function testOffsetSetThrowsException() { - $this->setExpectedException( - 'LogicException', - 'Method not implemented' - ); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Method not implemented'); $cachedKeySet = new CachedKeySet( $this->testJwkUri, @@ -35,10 +35,8 @@ public function testOffsetSetThrowsException() public function testOffsetUnsetThrowsException() { - $this->setExpectedException( - 'LogicException', - 'Method not implemented' - ); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Method not implemented'); $cachedKeySet = new CachedKeySet( $this->testJwkUri, @@ -52,10 +50,8 @@ public function testOffsetUnsetThrowsException() public function testOutOfBoundsThrowsException() { - $this->setExpectedException( - 'OutOfBoundsException', - 'Key ID not found' - ); + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionMessage('Key ID not found'); $cachedKeySet = new CachedKeySet( $this->testJwkUri, @@ -278,19 +274,4 @@ private function getMockEmptyCache() return $cache->reveal(); } - - /* - * For compatibility with PHPUnit 4.8 and PHP < 5.6 - */ - public function setExpectedException($exceptionName, $message = '', $code = null) - { - if (method_exists($this, 'expectException')) { - $this->expectException($exceptionName); - if ($message) { - $this->expectExceptionMessage($message); - } - } else { - parent::setExpectedException($exceptionName, $message, $code); - } - } } From f9d72a4eaa9ea59bd415c03de5253e901648eab5 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 16 Feb 2022 08:55:42 -0600 Subject: [PATCH 10/36] remove special tests, update for PHP 8 --- .github/workflows/tests.yml | 21 ---- composer.json | 5 +- phpunit-system.xml.dist | 18 --- phpunit.xml.dist | 3 +- tests/CachedKeySetTest.php | 213 ++++++++++++++++++++++++++++++------ tests/bootstrap-system.php | 172 ----------------------------- tests/bootstrap.php | 14 --- 7 files changed, 183 insertions(+), 263 deletions(-) delete mode 100644 phpunit-system.xml.dist delete mode 100644 tests/bootstrap-system.php delete mode 100644 tests/bootstrap.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c0fe33c8..873eae24 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,27 +27,6 @@ jobs: - name: Run Script run: vendor/bin/phpunit - test_system: - runs-on: ubuntu-latest - strategy: - matrix: - php: [ "8.0", "8.1" ] - name: PHP ${{matrix.php }} Unit Test - steps: - - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - - name: Install Dependencies - uses: nick-invision/retry@v1 - with: - timeout_minutes: 10 - max_attempts: 3 - command: composer require guzzlehttp/guzzle psr/cache:2.0.0 - - name: Run Script - run: vendor/bin/phpunit -c phpunit-system.xml.dist - style: runs-on: ubuntu-latest name: PHP Style Check diff --git a/composer.json b/composer.json index 4e190ea3..d44b9a61 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,9 @@ } }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^9.5", + "psr/cache": "^3.0", + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0" } } diff --git a/phpunit-system.xml.dist b/phpunit-system.xml.dist deleted file mode 100644 index f5874edd..00000000 --- a/phpunit-system.xml.dist +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - ./tests/CachedKeySetTest.php - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 430f6299..31195a91 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,12 +8,11 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - bootstrap="tests/bootstrap.php" + bootstrap="vendor/autoload.php" > ./tests - ./tests/CachedKeySetTest.php diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index 6dfd63a1..38bd889f 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -4,12 +4,18 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Cache\CacheItemPoolInterface; +use Psr\Cache\CacheItemInterface; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use LogicException; use OutOfBoundsException; class CachedKeySetTest extends TestCase { + use ProphecyTrait; + private $testJwkUri = 'httpjwkuri'; private $testJwkUriKey = 'jwkhttpjwkuri'; private $testJwk1 = '{"keys": [{"kid":"foo","kty":"RSA","alg":"foo","n":"","e":""}]}'; @@ -25,9 +31,9 @@ public function testOffsetSetThrowsException() $cachedKeySet = new CachedKeySet( $this->testJwkUri, - $this->prophesize('Psr\Http\Client\ClientInterface')->reveal(), - $this->prophesize('Psr\Http\Message\RequestFactoryInterface')->reveal(), - $this->prophesize('Psr\Cache\CacheItemPoolInterface')->reveal() + $this->prophesize(ClientInterface::class)->reveal(), + $this->prophesize(RequestFactoryInterface::class)->reveal(), + $this->prophesize(CacheItemPoolInterface::class)->reveal() ); $cachedKeySet['foo'] = 'bar'; @@ -40,9 +46,9 @@ public function testOffsetUnsetThrowsException() $cachedKeySet = new CachedKeySet( $this->testJwkUri, - $this->prophesize('Psr\Http\Client\ClientInterface')->reveal(), - $this->prophesize('Psr\Http\Message\RequestFactoryInterface')->reveal(), - $this->prophesize('Psr\Cache\CacheItemPoolInterface')->reveal() + $this->prophesize(ClientInterface::class)->reveal(), + $this->prophesize(RequestFactoryInterface::class)->reveal(), + $this->prophesize(CacheItemPoolInterface::class)->reveal() ); unset($cachedKeySet['foo']); @@ -72,19 +78,19 @@ public function testWithExistingKeyId() $this->getMockHttpFactory(), $this->getMockEmptyCache() ); - $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['foo']); + $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); } public function testKeyIdIsCached() { - $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->willReturn(true); $cacheItem->get() ->willReturn(JWK::parseKeySet(json_decode($this->testJwk1, true))); - $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwkUriKey) ->willReturn($cacheItem->reveal()); $cache->save(Argument::any()) @@ -92,17 +98,17 @@ public function testKeyIdIsCached() $cachedKeySet = new CachedKeySet( $this->testJwkUri, - $this->prophesize('Psr\Http\Client\ClientInterface')->reveal(), - $this->prophesize('Psr\Http\Message\RequestFactoryInterface')->reveal(), + $this->prophesize(ClientInterface::class)->reveal(), + $this->prophesize(RequestFactoryInterface::class)->reveal(), $cache->reveal() ); - $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['foo']); + $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); } public function testCachedKeyIdRefresh() { - $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->shouldBeCalledOnce() ->willReturn(true); @@ -111,9 +117,9 @@ public function testCachedKeyIdRefresh() ->willReturn(JWK::parseKeySet(json_decode($this->testJwk1, true))); $cacheItem->set(Argument::any()) ->shouldBeCalledOnce() - ->willReturn(true); + ->will(function() { return $this; }); - $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwkUriKey) ->shouldBeCalledOnce() ->willReturn($cacheItem->reveal()); @@ -127,26 +133,28 @@ public function testCachedKeyIdRefresh() $this->getMockHttpFactory(), $cache->reveal() ); - $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['foo']); + $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); - $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['bar']); + $this->assertInstanceOf(Key::class, $cachedKeySet['bar']); $this->assertEquals('bar', $cachedKeySet['bar']->getAlgorithm()); } public function testCacheItemWithExpiresAfter() { $expiresAfter = 10; - $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->shouldBeCalledOnce() ->willReturn(false); $cacheItem->set(Argument::any()) - ->shouldBeCalledOnce(); + ->shouldBeCalledOnce() + ->will(function() { return $this; }); $cacheItem->expiresAfter($expiresAfter) - ->shouldBeCalledOnce(); + ->shouldBeCalledOnce() + ->will(function() { return $this; }); - $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwkUriKey) ->shouldBeCalledOnce() ->willReturn($cacheItem->reveal()); @@ -160,7 +168,7 @@ public function testCacheItemWithExpiresAfter() $cache->reveal(), $expiresAfter ); - $this->assertInstanceOf('Firebase\JWT\Key', $cachedKeySet['foo']); + $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); } @@ -170,7 +178,7 @@ public function testJwtVerify() $payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds')); $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); - $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->willReturn(true); $cacheItem->get() @@ -178,14 +186,14 @@ public function testJwtVerify() json_decode(file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), true) )); - $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwkUriKey) ->willReturn($cacheItem->reveal()); $cachedKeySet = new CachedKeySet( $this->testJwkUri, - $this->prophesize('Psr\Http\Client\ClientInterface')->reveal(), - $this->prophesize('Psr\Http\Message\RequestFactoryInterface')->reveal(), + $this->prophesize(ClientInterface::class)->reveal(), + $this->prophesize(RequestFactoryInterface::class)->reveal(), $cache->reveal() ); @@ -199,10 +207,6 @@ public function testJwtVerify() */ public function testFullIntegration($jwkUri, $kid) { - if (!class_exists(TestMemoryCacheItemPool::class)) { - $this->markTestSkipped('Use phpunit-system.xml.dist to run this tests'); - } - $cache = new TestMemoryCacheItemPool(); $http = new \GuzzleHttp\Client(); $factory = new \GuzzleHttp\Psr7\HttpFactory(); @@ -237,7 +241,7 @@ private function getMockHttpClient($testJwk) ->shouldBeCalledOnce() ->willReturn($body->reveal()); - $http = $this->prophesize('Psr\Http\Client\ClientInterface'); + $http = $this->prophesize(ClientInterface::class); $http->sendRequest(Argument::any()) ->shouldBeCalledOnce() ->willReturn($response->reveal()); @@ -248,7 +252,7 @@ private function getMockHttpClient($testJwk) private function getMockHttpFactory() { $request = $this->prophesize('Psr\Http\Message\RequestInterface'); - $factory = $this->prophesize('Psr\Http\Message\RequestFactoryInterface'); + $factory = $this->prophesize(RequestFactoryInterface::class); $factory->createRequest('get', $this->testJwkUri) ->shouldBeCalledOnce() ->willReturn($request->reveal()); @@ -258,14 +262,14 @@ private function getMockHttpFactory() private function getMockEmptyCache() { - $cacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); + $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->shouldBeCalledOnce() ->willReturn(false); $cacheItem->set(Argument::any()) - ->willReturn(true); + ->will(function() { return $this; }); - $cache = $this->prophesize('Psr\Cache\CacheItemPoolInterface'); + $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwkUriKey) ->shouldBeCalledOnce() ->willReturn($cacheItem->reveal()); @@ -275,3 +279,142 @@ private function getMockEmptyCache() return $cache->reveal(); } } + +/** + * A cache item pool + */ +final class TestMemoryCacheItemPool implements CacheItemPoolInterface +{ + private $items; + private $deferredItems; + + public function getItem(string $key): CacheItemInterface + { + return current($this->getItems([$key])); + } + + public function getItems(array $keys = []): iterable + { + $items = []; + + foreach ($keys as $key) { + $items[$key] = $this->hasItem($key) ? clone $this->items[$key] : new TestMemoryCacheItem($key); + } + + return $items; + } + + public function hasItem(string $key): bool + { + return isset($this->items[$key]) && $this->items[$key]->isHit(); + } + + public function clear(): bool + { + $this->items = []; + $this->deferredItems = []; + + return true; + } + + public function deleteItem(string $key): bool + { + return $this->deleteItems([$key]); + } + + public function deleteItems(array $keys): bool + { + foreach ($keys as $key) { + unset($this->items[$key]); + } + + return true; + } + + public function save(CacheItemInterface $item): bool + { + $this->items[$item->getKey()] = $item; + + return true; + } + + public function saveDeferred(CacheItemInterface $item): bool + { + $this->deferredItems[$item->getKey()] = $item; + + return true; + } + + public function commit(): bool + { + foreach ($this->deferredItems as $item) { + $this->save($item); + } + + $this->deferredItems = []; + + return true; + } +} + +/** + * A cache item. + */ +final class TestMemoryCacheItem implements CacheItemInterface +{ + private $value; + private $expiration; + private $isHit = false; + + public function __construct(private string $key) + { + } + + public function getKey(): string + { + return $this->key; + } + + public function get(): mixed + { + return $this->isHit() ? $this->value : null; + } + + public function isHit(): bool + { + if (!$this->isHit) { + return false; + } + + if ($this->expiration === null) { + return true; + } + + return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp(); + } + + public function set(mixed $value): static + { + $this->isHit = true; + $this->value = $value; + + return $this; + } + + public function expiresAt(?\DateTimeInterface $expiration): static + { + $this->expiration = $expiration; + return $this; + } + + public function expiresAfter(\DateInterval|int|null $time): static + { + $this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S")); + return $this; + } + + protected function currentTime() + { + return new \DateTime('now', new \DateTimeZone('UTC')); + } +} diff --git a/tests/bootstrap-system.php b/tests/bootstrap-system.php deleted file mode 100644 index 3cfee04a..00000000 --- a/tests/bootstrap-system.php +++ /dev/null @@ -1,172 +0,0 @@ -getItems([$key])); - } - - public function getItems(array $keys = []) - { - $items = []; - - foreach ($keys as $key) { - $items[$key] = $this->hasItem($key) ? clone $this->items[$key] : new TestMemoryCacheItem($key); - } - - return $items; - } - - public function hasItem($key) - { - return isset($this->items[$key]) && $this->items[$key]->isHit(); - } - - public function clear() - { - $this->items = []; - $this->deferredItems = []; - - return true; - } - - public function deleteItem($key) - { - return $this->deleteItems([$key]); - } - - public function deleteItems(array $keys) - { - foreach ($keys as $key) { - unset($this->items[$key]); - } - - return true; - } - - public function save(CacheItemInterface $item) - { - $this->items[$item->getKey()] = $item; - - return true; - } - - public function saveDeferred(CacheItemInterface $item) - { - $this->deferredItems[$item->getKey()] = $item; - - return true; - } - - public function commit() - { - foreach ($this->deferredItems as $item) { - $this->save($item); - } - - $this->deferredItems = []; - - return true; - } -} - -/** - * A cache item. - */ -final class TestMemoryCacheItem implements CacheItemInterface -{ - private $key; - private $value; - private $expiration; - private $isHit = false; - - public function __construct($key) - { - $this->key = $key; - } - - public function getKey() - { - return $this->key; - } - - public function get() - { - return $this->isHit() ? $this->value : null; - } - - public function isHit() - { - if (!$this->isHit) { - return false; - } - - if ($this->expiration === null) { - return true; - } - - return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp(); - } - - public function set($value) - { - $this->isHit = true; - $this->value = $value; - - return $this; - } - - public function expiresAt($expiration) - { - $this->expiration = $expiration; - return $this; - } - - public function expiresAfter($time) - { - $this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S")); - return $this; - } - - protected function currentTime() - { - return new \DateTime('now', new \DateTimeZone('UTC')); - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index 385b6706..00000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,14 +0,0 @@ - Date: Wed, 16 Feb 2022 08:57:42 -0600 Subject: [PATCH 11/36] style fix --- tests/CachedKeySetTest.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index 38bd889f..f0704f04 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -117,7 +117,9 @@ public function testCachedKeyIdRefresh() ->willReturn(JWK::parseKeySet(json_decode($this->testJwk1, true))); $cacheItem->set(Argument::any()) ->shouldBeCalledOnce() - ->will(function() { return $this; }); + ->will(function () { + return $this; + }); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwkUriKey) @@ -149,10 +151,14 @@ public function testCacheItemWithExpiresAfter() ->willReturn(false); $cacheItem->set(Argument::any()) ->shouldBeCalledOnce() - ->will(function() { return $this; }); + ->will(function () { + return $this; + }); $cacheItem->expiresAfter($expiresAfter) ->shouldBeCalledOnce() - ->will(function() { return $this; }); + ->will(function () { + return $this; + }); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwkUriKey) @@ -267,7 +273,9 @@ private function getMockEmptyCache() ->shouldBeCalledOnce() ->willReturn(false); $cacheItem->set(Argument::any()) - ->will(function() { return $this; }); + ->will(function () { + return $this; + }); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwkUriKey) From a0f800831eb5187b50b1f6575a98cfbace611a52 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 16 Feb 2022 13:03:07 -0800 Subject: [PATCH 12/36] Update README.md --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index d489c69f..f11bc736 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,42 @@ $jwks = ['keys' => []]; JWT::decode($payload, JWK::parseKeySet($jwks)); ``` +Using Cached Key Sets +--------------------- + +The `CachedKeySet` class can be used to fetch JWK keys from a public URI. This has +the following advantages: + +1. The results are cached for performance +2. If an unrecognized key is requested, the cache is refreshed, to accomodate for key rotation. + +```php +use Firebase\JWT\CachedKeySet; +use Firebase\JWT\JWT; + +// The URI for the JWK keys you wish to cash the results from +$jwkUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; + +// Create an HTTP client (can be any PSR-7 compatible HTTP client) +$httpClient = new GuzzleHttp\Client(); + +// Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory) +$httpFactory = new GuzzleHttp\Psr\HttpFactory(); + +// Create a cache item pool (can be any PSR-6 compatible cache item pool) +$cacheItemPool = Phpfastcache\CacheManager::getInstance('files'); + +$keySet = new CachedKeySet( + $jwkUri, + $httpClient, + $httpFactory, + $cacheItemPool +); + +$decoded = JWT::decode($payload, $keySet); +``` + + Miscellaneous ------------- From e5f9a53246b69e3f1711e3af3a5e2b5e1b54ea05 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sun, 20 Mar 2022 17:21:45 -0600 Subject: [PATCH 13/36] fix phpstan --- composer.json | 2 +- src/CachedKeySet.php | 76 ++++++++++++++++++++++++++++++++++++-------- src/JWT.php | 15 +++------ 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/composer.json b/composer.json index 6065b85b..ca8cc44f 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ } }, "require-dev": { - "phpunit/phpunit": "^7.5||9.5" + "phpunit/phpunit": "^7.5||9.5", "psr/cache": "^3.0", "guzzlehttp/guzzle": "^7.4", "phpspec/prophecy-phpunit": "^2.0" diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index e16fa893..f84c4d9e 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -7,29 +7,64 @@ use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; use Psr\Cache\CacheItemPoolInterface; +use Psr\Cache\CacheItemInterface; use LogicException; use OutOfBoundsException; use RuntimeException; +/** + * @template-implements ArrayAccess + */ class CachedKeySet implements ArrayAccess { + /** + * @var string + */ private $jwkUri; + /** + * @var ClientInterface + */ private $httpClient; + /** + * @var RequestFactoryInterface + */ private $httpFactory; + /** + * @var CacheItemPoolInterface + */ private $cache; + /** + * @var int + */ private $expiresAfter; + /** + * @var ?CacheItemInterface + */ private $cacheItem; + /** + * @var array + */ private $keySet; + /** + * @var string + */ + private $cacheKey; + /** + * @var string + */ private $cacheKeyPrefix = 'jwk'; + /** + * @var int + */ private $maxKeyLength = 64; public function __construct( - $jwkUri, + string $jwkUri, ClientInterface $httpClient, RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, - $expiresAfter = null + int $expiresAfter = null ) { $this->jwkUri = $jwkUri; $this->cacheKey = $this->getCacheKey($jwkUri); @@ -39,7 +74,11 @@ public function __construct( $this->expiresAfter = $expiresAfter; } - public function offsetGet($keyId) + /** + * @param string $keyId + * @return Key + */ + public function offsetGet($keyId): Key { if (!$this->keyIdExists($keyId)) { throw new OutOfBoundsException('Key ID not found'); @@ -47,22 +86,33 @@ public function offsetGet($keyId) return $this->keySet[$keyId]; } - public function offsetExists($keyId) + /** + * @param string $keyId + * @return bool + */ + public function offsetExists($keyId): bool { return $this->keyIdExists($keyId); } - public function offsetSet($offset, $value) + /** + * @param string $offset + * @param Key $value + */ + public function offsetSet($offset, $value): void { throw new LogicException('Method not implemented'); } - public function offsetUnset($offset) + /** + * @param string $offset + */ + public function offsetUnset($offset): void { throw new LogicException('Method not implemented'); } - private function keyIdExists($keyId) + private function keyIdExists(string $keyId): bool { $keySetToCache = null; if (null === $this->keySet) { @@ -97,18 +147,18 @@ private function keyIdExists($keyId) return true; } - private function getCacheItem() + private function getCacheItem(): CacheItemInterface { - if ($this->cacheItem) { - return $this->cacheItem; + if (is_null($this->cacheItem)) { + $this->cacheItem = $this->cache->getItem($this->cacheKey); } - return $this->cacheItem = $this->cache->getItem($this->cacheKey); + return $this->cacheItem; } - private function getCacheKey($jwkUri) + private function getCacheKey(string $jwkUri): string { - if (is_null($jwkUri)) { + if (empty($jwkUri)) { throw new RuntimeException('JWK URI is empty'); } diff --git a/src/JWT.php b/src/JWT.php index 5b54df40..6378c078 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -60,7 +60,7 @@ class JWT * Decodes a JWT string into a PHP object. * * @param string $jwt The JWT - * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. + * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. * If the algorithm used is asymmetric, this is the public key * Each Key object contains an algorithm and matching key. * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', @@ -395,7 +395,7 @@ public static function urlsafeB64Encode(string $input): string /** * Determine if an algorithm has been provided for each Key * - * @param Key|ArrayAccess|array $keyOrKeyArray + * @param Key|ArrayAccess|array $keyOrKeyArray * @param string|null $kid * * @throws UnexpectedValueException @@ -411,18 +411,11 @@ private static function getKey( } if ($keyOrKeyArray instanceof CachedKeySet) { + // Skip "isset" check, as this will automatically refresh if not set return $keyOrKeyArray[$kid]; } - foreach ($keyOrKeyArray as $keyId => $key) { - if (!$key instanceof Key) { - throw new TypeError( - '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' - . 'array of Firebase\JWT\Key keys' - ); - } - } - if (!isset($kid)) { + if (empty($kid)) { throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); } if (!isset($keyOrKeyArray[$kid])) { From 5a98f720455044b9f2c47c097aa92fae7015583b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 23 Mar 2022 08:37:27 -0600 Subject: [PATCH 14/36] fix tests --- .github/workflows/tests.yml | 1 + tests/CachedKeySetTest.php | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 68d4f10b..38ae5828 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,5 +53,6 @@ jobs: php-version: '8.0' - name: Run Script run: | + composer install composer global require phpstan/phpstan ~/.composer/vendor/bin/phpstan analyse diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index f0704f04..fbcfd837 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -211,12 +211,21 @@ public function testJwtVerify() /** * @dataProvider provideFullIntegration */ - public function testFullIntegration($jwkUri, $kid) + public function testFullIntegration($jwkUri) { + // Create cache and http objects $cache = new TestMemoryCacheItemPool(); $http = new \GuzzleHttp\Client(); $factory = new \GuzzleHttp\Psr7\HttpFactory(); + // Determine "kid" dynamically, because these constantly change + $response = $http->get($jwkUri); + $json = (string) $response->getBody(); + $keys = json_decode($json, true); + $kid = $keys['keys'][0]['kid'] ?? null; + $this->assertNotNull($kid); + + // Instantiate the cached key set $cachedKeySet = new CachedKeySet( $jwkUri, $http, @@ -230,7 +239,7 @@ public function testFullIntegration($jwkUri, $kid) public function provideFullIntegration() { return [ - [$this->googleRsaUri, '182e450a35a2081faa1d9ae1d2d75a0f23d91df8'], + [$this->googleRsaUri], // [$this->googleEcUri, 'LYyP2g'] ]; } From 7341c6d2c15e6bf6f5cb32383c93ae598da86414 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 23 Mar 2022 08:53:44 -0600 Subject: [PATCH 15/36] downgrade psr cache for compatibility (force-push to trigger actions) --- composer.json | 2 +- tests/CachedKeySetTest.php | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index ca8cc44f..53a49af1 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ }, "require-dev": { "phpunit/phpunit": "^7.5||9.5", - "psr/cache": "^3.0", + "psr/cache": "^1.0|^2.0", "guzzlehttp/guzzle": "^7.4", "phpspec/prophecy-phpunit": "^2.0" } diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index fbcfd837..f97d1ec4 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -305,7 +305,7 @@ final class TestMemoryCacheItemPool implements CacheItemPoolInterface private $items; private $deferredItems; - public function getItem(string $key): CacheItemInterface + public function getItem($key): CacheItemInterface { return current($this->getItems([$key])); } @@ -321,7 +321,7 @@ public function getItems(array $keys = []): iterable return $items; } - public function hasItem(string $key): bool + public function hasItem($key): bool { return isset($this->items[$key]) && $this->items[$key]->isHit(); } @@ -334,7 +334,7 @@ public function clear(): bool return true; } - public function deleteItem(string $key): bool + public function deleteItem($key): bool { return $this->deleteItems([$key]); } @@ -379,12 +379,14 @@ public function commit(): bool */ final class TestMemoryCacheItem implements CacheItemInterface { + private $key; private $value; private $expiration; private $isHit = false; - public function __construct(private string $key) + public function __construct(string $key) { + $this->key = $key; } public function getKey(): string @@ -410,7 +412,7 @@ public function isHit(): bool return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp(); } - public function set(mixed $value): static + public function set($value) { $this->isHit = true; $this->value = $value; @@ -418,13 +420,13 @@ public function set(mixed $value): static return $this; } - public function expiresAt(?\DateTimeInterface $expiration): static + public function expiresAt($expiration) { $this->expiration = $expiration; return $this; } - public function expiresAfter(\DateInterval|int|null $time): static + public function expiresAfter($time) { $this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S")); return $this; From bcccc519bd00e65a9435c2a46c28cfd61a34501e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 23 Mar 2022 11:32:03 -0700 Subject: [PATCH 16/36] Update composer.json --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 53a49af1..106de5ad 100644 --- a/composer.json +++ b/composer.json @@ -32,8 +32,8 @@ }, "require-dev": { "phpunit/phpunit": "^7.5||9.5", - "psr/cache": "^1.0|^2.0", + "psr/cache": "^1.0||^2.0", "guzzlehttp/guzzle": "^7.4", - "phpspec/prophecy-phpunit": "^2.0" + "phpspec/prophecy-phpunit": "^1.1||^2.0" } } From 679746c31697356e9b41dcb3de84840600490e6f Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 23 Mar 2022 12:46:12 -0600 Subject: [PATCH 17/36] maybe fix dependency hell --- composer.json | 4 ++-- tests/CachedKeySetTest.php | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 106de5ad..3970caa1 100644 --- a/composer.json +++ b/composer.json @@ -31,9 +31,9 @@ } }, "require-dev": { - "phpunit/phpunit": "^7.5||9.5", + "phpunit/phpunit": "^7.5||^9.5", "psr/cache": "^1.0||^2.0", "guzzlehttp/guzzle": "^7.4", - "phpspec/prophecy-phpunit": "^1.1||^2.0" + "phpspec/prophecy-phpunit": "^1.1" } } diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index f97d1ec4..a2a49522 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -8,14 +8,11 @@ use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemInterface; use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; use LogicException; use OutOfBoundsException; class CachedKeySetTest extends TestCase { - use ProphecyTrait; - private $testJwkUri = 'httpjwkuri'; private $testJwkUriKey = 'jwkhttpjwkuri'; private $testJwk1 = '{"keys": [{"kid":"foo","kty":"RSA","alg":"foo","n":"","e":""}]}'; From 3060488b59b439537653cdf815c04fbbd2756e00 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 23 Mar 2022 12:51:57 -0600 Subject: [PATCH 18/36] add guzzle 6 for PHP 7.1 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3970caa1..ad676378 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "require-dev": { "phpunit/phpunit": "^7.5||^9.5", "psr/cache": "^1.0||^2.0", - "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/guzzle": "^6.5||^7.4", "phpspec/prophecy-phpunit": "^1.1" } } From 7595b5cacd3bb802a6cc5791fc468ed53c1a5597 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 24 Mar 2022 16:09:20 -0600 Subject: [PATCH 19/36] fix php7.1 tests --- composer.json | 4 +++- tests/CachedKeySetTest.php | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ad676378..36346ae6 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,8 @@ "phpunit/phpunit": "^7.5||^9.5", "psr/cache": "^1.0||^2.0", "guzzlehttp/guzzle": "^6.5||^7.4", - "phpspec/prophecy-phpunit": "^1.1" + "phpspec/prophecy-phpunit": "^1.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" } } diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index a2a49522..17ffb2df 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -210,6 +210,9 @@ public function testJwtVerify() */ public function testFullIntegration($jwkUri) { + if (!class_exists(\GuzzleHttp\Psr7\HttpFactory::class)) { + self::markTestSkipped('Guzzle 7 only'); + } // Create cache and http objects $cache = new TestMemoryCacheItemPool(); $http = new \GuzzleHttp\Client(); From 320a1400c52ac695918b4ec063ed0bfc91f0d57b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 24 Mar 2022 16:15:30 -0600 Subject: [PATCH 20/36] rearange deps --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 36346ae6..2a3cb2df 100644 --- a/composer.json +++ b/composer.json @@ -31,10 +31,10 @@ } }, "require-dev": { - "phpunit/phpunit": "^7.5||^9.5", - "psr/cache": "^1.0||^2.0", "guzzlehttp/guzzle": "^6.5||^7.4", "phpspec/prophecy-phpunit": "^1.1", + "phpunit/phpunit": "^7.5||^9.5", + "psr/cache": "^1.0||^2.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" } From 7f20b2d455545958ef8ee70cf5721e8acf3f5e23 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 13 Apr 2022 14:39:15 -0700 Subject: [PATCH 21/36] Update README.md Co-authored-by: Morten Hauberg-Lund --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e9be7bf..6db95f80 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ the following advantages: use Firebase\JWT\CachedKeySet; use Firebase\JWT\JWT; -// The URI for the JWK keys you wish to cash the results from +// The URI for the JWK keys you wish to cache the results from $jwkUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; // Create an HTTP client (can be any PSR-7 compatible HTTP client) From 341a41530cbe3d68f14eaf5c041cbf3ac44a8b0f Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 13 Apr 2022 14:42:06 -0700 Subject: [PATCH 22/36] Update src/CachedKeySet.php Co-authored-by: Morten Hauberg-Lund --- src/CachedKeySet.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index f84c4d9e..fb131b8f 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -5,7 +5,6 @@ use ArrayAccess; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; -use Psr\Http\Message\RequestInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemInterface; use LogicException; From 663bfd0a4a9f47f64fe7ae70e7d56b602c78e6b2 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 13 Apr 2022 14:42:25 -0700 Subject: [PATCH 23/36] Update src/CachedKeySet.php Co-authored-by: Morten Hauberg-Lund --- src/CachedKeySet.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index fb131b8f..a53cda64 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -36,7 +36,6 @@ class CachedKeySet implements ArrayAccess * @var int */ private $expiresAfter; - /** * @var ?CacheItemInterface */ From 617e67a40e0eb5c9a8b6fde6a77531bb2701e407 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 15 Apr 2022 16:40:26 -0500 Subject: [PATCH 24/36] add rate limit --- src/CachedKeySet.php | 61 ++++++++++++++++++++++++++++++++++---- tests/CachedKeySetTest.php | 36 ++++++++++++++++++---- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index a53cda64..117d83d2 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -56,20 +56,34 @@ class CachedKeySet implements ArrayAccess * @var int */ private $maxKeyLength = 64; + /** + * @var bool + */ + private $rateLimit; + /** + * @var string + */ + private $rateLimitCacheKey; + /** + * @var int + */ + private $maxCallsPerMinute = 10; public function __construct( string $jwkUri, ClientInterface $httpClient, RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, - int $expiresAfter = null + int $expiresAfter = null, + bool $rateLimit = false ) { $this->jwkUri = $jwkUri; - $this->cacheKey = $this->getCacheKey($jwkUri); $this->httpClient = $httpClient; $this->httpFactory = $httpFactory; $this->cache = $cache; $this->expiresAfter = $expiresAfter; + $this->rateLimit = $rateLimit; + $this->setCacheKeys(); } /** @@ -123,6 +137,9 @@ private function keyIdExists(string $keyId): bool } if (!isset($this->keySet[$keyId])) { + if ($this->rateLimitExceeded()) { + return false; + } $request = $this->httpFactory->createRequest('get', $this->jwkUri); $jwkResponse = $this->httpClient->sendRequest($request); $jwk = json_decode((string) $jwkResponse->getBody(), true); @@ -145,6 +162,26 @@ private function keyIdExists(string $keyId): bool return true; } + private function rateLimitExceeded(): bool + { + if (!$this->rateLimit) { + return false; + } + + $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); + if (!$cacheItem->isHit()) { + $cacheItem->expiresAfter(1); // # of calls are cached each minute + } + + $callsPerMinute = (int) $cacheItem->get(); + if (++$callsPerMinute > $this->maxCallsPerMinute) { + return true; + } + $cacheItem->set($callsPerMinute); + $this->cache->save($cacheItem); + return false; + } + private function getCacheItem(): CacheItemInterface { if (is_null($this->cacheItem)) { @@ -154,14 +191,14 @@ private function getCacheItem(): CacheItemInterface return $this->cacheItem; } - private function getCacheKey(string $jwkUri): string + private function setCacheKeys(): void { - if (empty($jwkUri)) { + if (empty($this->jwkUri)) { throw new RuntimeException('JWK URI is empty'); } // ensure we do not have illegal characters - $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $jwkUri); + $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwkUri); // add prefix $key = $this->cacheKeyPrefix . $key; @@ -171,6 +208,18 @@ private function getCacheKey(string $jwkUri): string $key = substr(hash('sha256', $key), 0, $this->maxKeyLength); } - return $key; + $this->cacheKey = $key; + + if ($this->rateLimit) { + // add prefix + $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key; + + // Hash keys if they exceed $maxKeyLength of 64 + if (strlen($rateLimitKey) > $this->maxKeyLength) { + $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength); + } + + $this->rateLimitCacheKey = $rateLimitKey; + } } } diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index 17ffb2df..5f7bd9aa 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -2,6 +2,7 @@ namespace Firebase\JWT; +use GuzzleHttp\Exception\RequestException; use PHPUnit\Framework\TestCase; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; @@ -205,6 +206,29 @@ public function testJwtVerify() $this->assertEquals("foo", $result->sub); } + public function testRateLimit() + { + // We request the key 11 times, HTTP should only be called 10 times + $shouldBeCalledTimes = 10; + + // Instantiate the cached key set + $cachedKeySet = new CachedKeySet( + $this->testJwkUri, + $this->getMockHttpClient($this->testJwk1, $shouldBeCalledTimes), + $factory = $this->getMockHttpFactory($shouldBeCalledTimes), + new TestMemoryCacheItemPool(), + 10, // expires after seconds + true // enable rate limiting + ); + + $invalidKid = 'invalidkey'; + for ($i = 0; $i < 10; $i++) { + $this->assertFalse(isset($cachedKeySet[$invalidKid])); + } + // The 11th time does not call HTTP + $this->assertFalse(isset($cachedKeySet[$invalidKid])); + } + /** * @dataProvider provideFullIntegration */ @@ -244,32 +268,32 @@ public function provideFullIntegration() ]; } - private function getMockHttpClient($testJwk) + private function getMockHttpClient($testJwk, int $timesCalled = 1) { $body = $this->prophesize('Psr\Http\Message\StreamInterface'); $body->__toString() - ->shouldBeCalledOnce() + ->shouldBeCalledTimes($timesCalled) ->willReturn($testJwk); $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); $response->getBody() - ->shouldBeCalledOnce() + ->shouldBeCalledTimes($timesCalled) ->willReturn($body->reveal()); $http = $this->prophesize(ClientInterface::class); $http->sendRequest(Argument::any()) - ->shouldBeCalledOnce() + ->shouldBeCalledTimes($timesCalled) ->willReturn($response->reveal()); return $http->reveal(); } - private function getMockHttpFactory() + private function getMockHttpFactory(int $timesCalled = 1) { $request = $this->prophesize('Psr\Http\Message\RequestInterface'); $factory = $this->prophesize(RequestFactoryInterface::class); $factory->createRequest('get', $this->testJwkUri) - ->shouldBeCalledOnce() + ->shouldBeCalledTimes($timesCalled) ->willReturn($request->reveal()); return $factory->reveal(); From acdc97af6e23a372620098c0f54c0308f2f9439b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sat, 16 Apr 2022 10:49:28 -0500 Subject: [PATCH 25/36] remove mixed keyword for < PHP 8.0 --- tests/CachedKeySetTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index 5f7bd9aa..bfd8d71f 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -418,7 +418,7 @@ public function getKey(): string return $this->key; } - public function get(): mixed + public function get() { return $this->isHit() ? $this->value : null; } From 967c5cbd04a3a21c0db37fd44896ff2c3d8bcf3a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sat, 16 Apr 2022 11:08:11 -0500 Subject: [PATCH 26/36] change jwk to jwks to be mmore correct --- src/CachedKeySet.php | 22 ++++++------ tests/CachedKeySetTest.php | 72 +++++++++++++++++++++++--------------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index 117d83d2..1aa694d2 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -19,7 +19,7 @@ class CachedKeySet implements ArrayAccess /** * @var string */ - private $jwkUri; + private $jwksUri; /** * @var ClientInterface */ @@ -51,7 +51,7 @@ class CachedKeySet implements ArrayAccess /** * @var string */ - private $cacheKeyPrefix = 'jwk'; + private $cacheKeyPrefix = 'jwks'; /** * @var int */ @@ -70,14 +70,14 @@ class CachedKeySet implements ArrayAccess private $maxCallsPerMinute = 10; public function __construct( - string $jwkUri, + string $jwksUri, ClientInterface $httpClient, RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, int $expiresAfter = null, bool $rateLimit = false ) { - $this->jwkUri = $jwkUri; + $this->jwksUri = $jwksUri; $this->httpClient = $httpClient; $this->httpFactory = $httpFactory; $this->cache = $cache; @@ -140,10 +140,10 @@ private function keyIdExists(string $keyId): bool if ($this->rateLimitExceeded()) { return false; } - $request = $this->httpFactory->createRequest('get', $this->jwkUri); - $jwkResponse = $this->httpClient->sendRequest($request); - $jwk = json_decode((string) $jwkResponse->getBody(), true); - $this->keySet = $keySetToCache = JWK::parseKeySet($jwk); + $request = $this->httpFactory->createRequest('get', $this->jwksUri); + $jwksResponse = $this->httpClient->sendRequest($request); + $jwks = json_decode((string) $jwksResponse->getBody(), true); + $this->keySet = $keySetToCache = JWK::parseKeySet($jwks); if (!isset($this->keySet[$keyId])) { return false; @@ -193,12 +193,12 @@ private function getCacheItem(): CacheItemInterface private function setCacheKeys(): void { - if (empty($this->jwkUri)) { - throw new RuntimeException('JWK URI is empty'); + if (empty($this->jwksUri)) { + throw new RuntimeException('JWKS URI is empty'); } // ensure we do not have illegal characters - $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwkUri); + $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri); // add prefix $key = $this->cacheKeyPrefix . $key; diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index bfd8d71f..5580e014 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -11,24 +11,40 @@ use Prophecy\Argument; use LogicException; use OutOfBoundsException; +use RuntimeException; class CachedKeySetTest extends TestCase { - private $testJwkUri = 'httpjwkuri'; - private $testJwkUriKey = 'jwkhttpjwkuri'; - private $testJwk1 = '{"keys": [{"kid":"foo","kty":"RSA","alg":"foo","n":"","e":""}]}'; - private $testJwk2 = '{"keys": [{"kid":"bar","kty":"RSA","alg":"bar","n":"","e":""}]}'; + private $testJwksUri = 'https://jwk.uri'; + private $testJwksUriKey = 'jwkshttpsjwk.uri'; + private $testJwks1 = '{"keys": [{"kid":"foo","kty":"RSA","alg":"foo","n":"","e":""}]}'; + private $testJwks2 = '{"keys": [{"kid":"bar","kty":"RSA","alg":"bar","n":"","e":""}]}'; private $googleRsaUri = 'https://www.googleapis.com/oauth2/v3/certs'; // private $googleEcUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; + public function testEmptyUriThrowsException() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('JWKS URI is empty'); + + $cachedKeySet = new CachedKeySet( + '', + $this->prophesize(ClientInterface::class)->reveal(), + $this->prophesize(RequestFactoryInterface::class)->reveal(), + $this->prophesize(CacheItemPoolInterface::class)->reveal() + ); + + $cachedKeySet['foo']; + } + public function testOffsetSetThrowsException() { $this->expectException(LogicException::class); $this->expectExceptionMessage('Method not implemented'); $cachedKeySet = new CachedKeySet( - $this->testJwkUri, + $this->testJwksUri, $this->prophesize(ClientInterface::class)->reveal(), $this->prophesize(RequestFactoryInterface::class)->reveal(), $this->prophesize(CacheItemPoolInterface::class)->reveal() @@ -43,7 +59,7 @@ public function testOffsetUnsetThrowsException() $this->expectExceptionMessage('Method not implemented'); $cachedKeySet = new CachedKeySet( - $this->testJwkUri, + $this->testJwksUri, $this->prophesize(ClientInterface::class)->reveal(), $this->prophesize(RequestFactoryInterface::class)->reveal(), $this->prophesize(CacheItemPoolInterface::class)->reveal() @@ -58,8 +74,8 @@ public function testOutOfBoundsThrowsException() $this->expectExceptionMessage('Key ID not found'); $cachedKeySet = new CachedKeySet( - $this->testJwkUri, - $this->getMockHttpClient($this->testJwk1), + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks1), $this->getMockHttpFactory(), $this->getMockEmptyCache() ); @@ -71,8 +87,8 @@ public function testOutOfBoundsThrowsException() public function testWithExistingKeyId() { $cachedKeySet = new CachedKeySet( - $this->testJwkUri, - $this->getMockHttpClient($this->testJwk1), + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks1), $this->getMockHttpFactory(), $this->getMockEmptyCache() ); @@ -86,16 +102,16 @@ public function testKeyIdIsCached() $cacheItem->isHit() ->willReturn(true); $cacheItem->get() - ->willReturn(JWK::parseKeySet(json_decode($this->testJwk1, true))); + ->willReturn(JWK::parseKeySet(json_decode($this->testJwks1, true))); $cache = $this->prophesize(CacheItemPoolInterface::class); - $cache->getItem($this->testJwkUriKey) + $cache->getItem($this->testJwksUriKey) ->willReturn($cacheItem->reveal()); $cache->save(Argument::any()) ->willReturn(true); $cachedKeySet = new CachedKeySet( - $this->testJwkUri, + $this->testJwksUri, $this->prophesize(ClientInterface::class)->reveal(), $this->prophesize(RequestFactoryInterface::class)->reveal(), $cache->reveal() @@ -112,7 +128,7 @@ public function testCachedKeyIdRefresh() ->willReturn(true); $cacheItem->get() ->shouldBeCalledOnce() - ->willReturn(JWK::parseKeySet(json_decode($this->testJwk1, true))); + ->willReturn(JWK::parseKeySet(json_decode($this->testJwks1, true))); $cacheItem->set(Argument::any()) ->shouldBeCalledOnce() ->will(function () { @@ -120,7 +136,7 @@ public function testCachedKeyIdRefresh() }); $cache = $this->prophesize(CacheItemPoolInterface::class); - $cache->getItem($this->testJwkUriKey) + $cache->getItem($this->testJwksUriKey) ->shouldBeCalledOnce() ->willReturn($cacheItem->reveal()); $cache->save(Argument::any()) @@ -128,8 +144,8 @@ public function testCachedKeyIdRefresh() ->willReturn(true); $cachedKeySet = new CachedKeySet( - $this->testJwkUri, - $this->getMockHttpClient($this->testJwk2), // updated JWK + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks2), // updated JWK $this->getMockHttpFactory(), $cache->reveal() ); @@ -159,15 +175,15 @@ public function testCacheItemWithExpiresAfter() }); $cache = $this->prophesize(CacheItemPoolInterface::class); - $cache->getItem($this->testJwkUriKey) + $cache->getItem($this->testJwksUriKey) ->shouldBeCalledOnce() ->willReturn($cacheItem->reveal()); $cache->save(Argument::any()) ->shouldBeCalledOnce(); $cachedKeySet = new CachedKeySet( - $this->testJwkUri, - $this->getMockHttpClient($this->testJwk1), + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks1), $this->getMockHttpFactory(), $cache->reveal(), $expiresAfter @@ -191,11 +207,11 @@ public function testJwtVerify() )); $cache = $this->prophesize(CacheItemPoolInterface::class); - $cache->getItem($this->testJwkUriKey) + $cache->getItem($this->testJwksUriKey) ->willReturn($cacheItem->reveal()); $cachedKeySet = new CachedKeySet( - $this->testJwkUri, + $this->testJwksUri, $this->prophesize(ClientInterface::class)->reveal(), $this->prophesize(RequestFactoryInterface::class)->reveal(), $cache->reveal() @@ -213,8 +229,8 @@ public function testRateLimit() // Instantiate the cached key set $cachedKeySet = new CachedKeySet( - $this->testJwkUri, - $this->getMockHttpClient($this->testJwk1, $shouldBeCalledTimes), + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks1, $shouldBeCalledTimes), $factory = $this->getMockHttpFactory($shouldBeCalledTimes), new TestMemoryCacheItemPool(), 10, // expires after seconds @@ -268,12 +284,12 @@ public function provideFullIntegration() ]; } - private function getMockHttpClient($testJwk, int $timesCalled = 1) + private function getMockHttpClient($testJwks, int $timesCalled = 1) { $body = $this->prophesize('Psr\Http\Message\StreamInterface'); $body->__toString() ->shouldBeCalledTimes($timesCalled) - ->willReturn($testJwk); + ->willReturn($testJwks); $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); $response->getBody() @@ -292,7 +308,7 @@ private function getMockHttpFactory(int $timesCalled = 1) { $request = $this->prophesize('Psr\Http\Message\RequestInterface'); $factory = $this->prophesize(RequestFactoryInterface::class); - $factory->createRequest('get', $this->testJwkUri) + $factory->createRequest('get', $this->testJwksUri) ->shouldBeCalledTimes($timesCalled) ->willReturn($request->reveal()); @@ -311,7 +327,7 @@ private function getMockEmptyCache() }); $cache = $this->prophesize(CacheItemPoolInterface::class); - $cache->getItem($this->testJwkUriKey) + $cache->getItem($this->testJwksUriKey) ->shouldBeCalledOnce() ->willReturn($cacheItem->reveal()); $cache->save(Argument::any()) From 182a136f0787b5cf0bfd42095808e08b6bf3c8c6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 18 Apr 2022 11:14:45 -0700 Subject: [PATCH 27/36] update README with ratelimit info --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6db95f80..62bacddf 100644 --- a/README.md +++ b/README.md @@ -206,11 +206,12 @@ JWT::decode($payload, JWK::parseKeySet($jwks)); Using Cached Key Sets --------------------- -The `CachedKeySet` class can be used to fetch JWK keys from a public URI. This has -the following advantages: +The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI. +This has the following advantages: 1. The results are cached for performance 2. If an unrecognized key is requested, the cache is refreshed, to accomodate for key rotation. +3. If rate limiting is enabled, the JWKS URI will not make more than 10 requests a second. ```php use Firebase\JWT\CachedKeySet; @@ -233,6 +234,8 @@ $keySet = new CachedKeySet( $httpClient, $httpFactory, $cacheItemPool + null, // $expiresAfter int seconds to set the JWKS to expire + true, // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys ); $decoded = JWT::decode($payload, $keySet); From 98adbe445355774f2a031522f5c2ab532a6beea8 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 19 Apr 2022 11:49:10 -0700 Subject: [PATCH 28/36] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 62bacddf..efbc6662 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,7 @@ $keySet = new CachedKeySet( $httpFactory, $cacheItemPool null, // $expiresAfter int seconds to set the JWKS to expire - true, // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys + true // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys ); $decoded = JWT::decode($payload, $keySet); From 2af9d51676eefd5cbbd575535077bc3280a82cff Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 25 Apr 2022 07:12:22 -0700 Subject: [PATCH 29/36] Update README.md Co-authored-by: David Supplee --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index efbc6662..df56ad77 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ Using Cached Key Sets The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI. This has the following advantages: -1. The results are cached for performance +1. The results are cached for performance. 2. If an unrecognized key is requested, the cache is refreshed, to accomodate for key rotation. 3. If rate limiting is enabled, the JWKS URI will not make more than 10 requests a second. From 7ba9d700166f86b3ecc8d9caa3ba9d7feb458dd5 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 25 Apr 2022 07:12:52 -0700 Subject: [PATCH 30/36] Update README.md Co-authored-by: David Supplee --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df56ad77..dee30e39 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ $keySet = new CachedKeySet( $jwkUri, $httpClient, $httpFactory, - $cacheItemPool + $cacheItemPool, null, // $expiresAfter int seconds to set the JWKS to expire true // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys ); From b6573336d535aa9ddd8ef9a4e431792bbb6ebea8 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 25 Apr 2022 07:13:07 -0700 Subject: [PATCH 31/36] Update README.md Co-authored-by: David Supplee --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index dee30e39..48b72c22 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,6 @@ $keySet = new CachedKeySet( $decoded = JWT::decode($payload, $keySet); ``` - Miscellaneous ------------- From dd33453d6de012da315fb746f3248ea53462cf36 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 25 Apr 2022 07:43:55 -0700 Subject: [PATCH 32/36] Update src/CachedKeySet.php Co-authored-by: David Supplee --- src/CachedKeySet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index 1aa694d2..c3db20d4 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -33,7 +33,7 @@ class CachedKeySet implements ArrayAccess */ private $cache; /** - * @var int + * @var ?int */ private $expiresAfter; /** From 2d226861184bc61e57dc4b7ee40f7ec28f6b47a5 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 25 Apr 2022 08:49:36 -0600 Subject: [PATCH 33/36] address more review comments --- README.md | 4 ++-- src/CachedKeySet.php | 3 --- tests/CachedKeySetTest.php | 5 ++++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e5e80c24..5b0b33ee 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ use Firebase\JWT\JWT; $jwkUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; // Create an HTTP client (can be any PSR-7 compatible HTTP client) -$httpClient = new GuzzleHttp\Client(); +$httpClient = new GuzzleHttp\Client(); // Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory) $httpFactory = new GuzzleHttp\Psr\HttpFactory(); @@ -238,7 +238,7 @@ $keySet = new CachedKeySet( true // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys ); -$decoded = JWT::decode($payload, $keySet); +$decoded = JWT::decode($jwt, $keySet); ``` Miscellaneous diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index 1aa694d2..6d2bfd9b 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -11,9 +11,6 @@ use OutOfBoundsException; use RuntimeException; -/** - * @template-implements ArrayAccess - */ class CachedKeySet implements ArrayAccess { /** diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index 5580e014..d0c47c32 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -248,7 +248,7 @@ public function testRateLimit() /** * @dataProvider provideFullIntegration */ - public function testFullIntegration($jwkUri) + public function testFullIntegration(string $jwkUri): void { if (!class_exists(\GuzzleHttp\Psr7\HttpFactory::class)) { self::markTestSkipped('Guzzle 7 only'); @@ -274,6 +274,9 @@ public function testFullIntegration($jwkUri) ); $this->assertArrayHasKey($kid, $cachedKeySet); + $key = $cachedKeySet[$kid]; + $this->assertInstanceOf(Key::class, $key); + $this->assertEquals($keys['keys'][0]['alg'], $key->getAlgorithm()); } public function provideFullIntegration() From 04b46c1c91934c60a0430dca8db16e950239ea67 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 25 Apr 2022 15:09:15 -0600 Subject: [PATCH 34/36] fix phpstan implements --- src/CachedKeySet.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index cb759170..e274842a 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -11,6 +11,9 @@ use OutOfBoundsException; use RuntimeException; +/** + * @implements ArrayAccess + */ class CachedKeySet implements ArrayAccess { /** From e3f21d50a3419395b615f9a7c3de3aa665c55713 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 25 Apr 2022 15:13:25 -0600 Subject: [PATCH 35/36] add example for cached key set example --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5b0b33ee..42e8b6db 100644 --- a/README.md +++ b/README.md @@ -217,8 +217,8 @@ This has the following advantages: use Firebase\JWT\CachedKeySet; use Firebase\JWT\JWT; -// The URI for the JWK keys you wish to cache the results from -$jwkUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; +// The URI for the JWKS you wish to cache the results from +$jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; // Create an HTTP client (can be any PSR-7 compatible HTTP client) $httpClient = new GuzzleHttp\Client(); @@ -230,7 +230,7 @@ $httpFactory = new GuzzleHttp\Psr\HttpFactory(); $cacheItemPool = Phpfastcache\CacheManager::getInstance('files'); $keySet = new CachedKeySet( - $jwkUri, + $jwksUri, $httpClient, $httpFactory, $cacheItemPool, @@ -238,6 +238,7 @@ $keySet = new CachedKeySet( true // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys ); +$jwt = 'eyJhbGci...'; // Some JWT signed by a key from the $jwkUri above $decoded = JWT::decode($jwt, $keySet); ``` From 8aa0a5b9579893bca6846bc42ee6d8da178b4c5f Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 27 Apr 2022 16:24:57 -0600 Subject: [PATCH 36/36] cs fixer and phpstan fixes --- src/CachedKeySet.php | 14 +++++++------- src/JWT.php | 2 +- tests/CachedKeySetTest.php | 13 ++++++------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index e274842a..077dceb0 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -3,12 +3,12 @@ namespace Firebase\JWT; use ArrayAccess; -use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\RequestFactoryInterface; -use Psr\Cache\CacheItemPoolInterface; -use Psr\Cache\CacheItemInterface; use LogicException; use OutOfBoundsException; +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; use RuntimeException; /** @@ -184,7 +184,7 @@ private function rateLimitExceeded(): bool private function getCacheItem(): CacheItemInterface { - if (is_null($this->cacheItem)) { + if (\is_null($this->cacheItem)) { $this->cacheItem = $this->cache->getItem($this->cacheKey); } @@ -204,7 +204,7 @@ private function setCacheKeys(): void $key = $this->cacheKeyPrefix . $key; // Hash keys if they exceed $maxKeyLength of 64 - if (strlen($key) > $this->maxKeyLength) { + if (\strlen($key) > $this->maxKeyLength) { $key = substr(hash('sha256', $key), 0, $this->maxKeyLength); } @@ -215,7 +215,7 @@ private function setCacheKeys(): void $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key; // Hash keys if they exceed $maxKeyLength of 64 - if (strlen($rateLimitKey) > $this->maxKeyLength) { + if (\strlen($rateLimitKey) > $this->maxKeyLength) { $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength); } diff --git a/src/JWT.php b/src/JWT.php index b3f2caf7..9011292f 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -2,6 +2,7 @@ namespace Firebase\JWT; +use ArrayAccess; use DateTime; use DomainException; use Exception; @@ -9,7 +10,6 @@ use OpenSSLAsymmetricKey; use OpenSSLCertificate; use stdClass; -use TypeError; use UnexpectedValueException; /** diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index d0c47c32..22e1de5f 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -2,15 +2,14 @@ namespace Firebase\JWT; -use GuzzleHttp\Exception\RequestException; +use LogicException; +use OutOfBoundsException; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; -use Psr\Cache\CacheItemPoolInterface; -use Psr\Cache\CacheItemInterface; -use Prophecy\Argument; -use LogicException; -use OutOfBoundsException; use RuntimeException; class CachedKeySetTest extends TestCase @@ -219,7 +218,7 @@ public function testJwtVerify() $result = JWT::decode($msg, $cachedKeySet); - $this->assertEquals("foo", $result->sub); + $this->assertEquals('foo', $result->sub); } public function testRateLimit()