OXIESEC PANEL
- Current Dir:
/
/
var
/
www
/
firebase
/
vendor
/
firebase
/
php-jwt
/
tests
Server IP: 139.59.38.164
Upload:
Create Dir:
Name
Size
Modified
Perms
📁
..
-
08/12/2024 10:33:09 AM
rwxr-xr-x
📄
CachedKeySetTest.php
20.48 KB
08/12/2024 10:33:10 AM
rw-r--r--
📄
JWKTest.php
5.1 KB
08/12/2024 10:33:10 AM
rw-r--r--
📄
JWTTest.php
18.89 KB
08/12/2024 10:33:10 AM
rw-r--r--
📁
data
-
08/12/2024 10:33:40 AM
rwxr-xr-x
Editing: CachedKeySetTest.php
Close
<?php namespace Firebase\JWT; use LogicException; use OutOfBoundsException; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use RuntimeException; class CachedKeySetTest extends TestCase { use ProphecyTrait; private $testJwksUri = 'https://jwk.uri'; private $testJwksUriKey = 'jwkshttpsjwk.uri'; private $testJwks1 = '{"keys": [{"kid":"foo","kty":"RSA","alg":"foo","n":"","e":""}]}'; private $testCachedJwks1 = ['foo' => ['kid' => 'foo', 'kty' => 'RSA', 'alg' => 'foo', 'n' => '', 'e' => '']]; private $testJwks2 = '{"keys": [{"kid":"bar","kty":"RSA","alg":"bar","n":"","e":""}]}'; private $testJwks3 = '{"keys": [{"kid":"baz","kty":"RSA","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->testJwksUri, $this->prophesize(ClientInterface::class)->reveal(), $this->prophesize(RequestFactoryInterface::class)->reveal(), $this->prophesize(CacheItemPoolInterface::class)->reveal() ); $cachedKeySet['foo'] = 'bar'; } public function testOffsetUnsetThrowsException() { $this->expectException(LogicException::class); $this->expectExceptionMessage('Method not implemented'); $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->prophesize(ClientInterface::class)->reveal(), $this->prophesize(RequestFactoryInterface::class)->reveal(), $this->prophesize(CacheItemPoolInterface::class)->reveal() ); unset($cachedKeySet['foo']); } public function testOutOfBoundsThrowsException() { $this->expectException(OutOfBoundsException::class); $this->expectExceptionMessage('Key ID not found'); $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->getMockHttpClient($this->testJwks1), $this->getMockHttpFactory(), $this->getMockEmptyCache() ); // keyID doesn't exist $cachedKeySet['bar']; } public function testInvalidHttpResponseThrowsException() { $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage('HTTP Error: 404 URL not found'); $this->expectExceptionCode(404); $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); $response->getStatusCode() ->shouldBeCalled() ->willReturn(404); $response->getReasonPhrase() ->shouldBeCalledTimes(1) ->willReturn('URL not found'); $http = $this->prophesize(ClientInterface::class); $http->sendRequest(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($response->reveal()); $cachedKeySet = new CachedKeySet( $this->testJwksUri, $http->reveal(), $this->getMockHttpFactory(), $this->getMockEmptyCache() ); isset($cachedKeySet[0]); } public function testWithExistingKeyId() { $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->getMockHttpClient($this->testJwks1), $this->getMockHttpFactory(), $this->getMockEmptyCache() ); $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); } public function testWithDefaultAlg() { $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->getMockHttpClient($this->testJwks3), $this->getMockHttpFactory(), $this->getMockEmptyCache(), null, false, 'baz256' ); $this->assertInstanceOf(Key::class, $cachedKeySet['baz']); $this->assertSame('baz256', $cachedKeySet['baz']->getAlgorithm()); } public function testKeyIdIsCached() { $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->willReturn(true); $cacheItem->get() ->willReturn($this->testCachedJwks1); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwksUriKey) ->willReturn($cacheItem->reveal()); $cache->save(Argument::any()) ->willReturn(true); $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->prophesize(ClientInterface::class)->reveal(), $this->prophesize(RequestFactoryInterface::class)->reveal(), $cache->reveal() ); $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); } public function testCachedKeyIdRefresh() { $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->shouldBeCalledOnce() ->willReturn(true); $cacheItem->get() ->shouldBeCalledOnce() ->willReturn($this->testCachedJwks1); $cacheItem->set(Argument::any()) ->shouldBeCalledOnce() ->will(function () { return $this; }); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwksUriKey) ->shouldBeCalledOnce() ->willReturn($cacheItem->reveal()); $cache->save(Argument::any()) ->shouldBeCalledOnce() ->willReturn(true); $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->getMockHttpClient($this->testJwks2), // updated JWK $this->getMockHttpFactory(), $cache->reveal() ); $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); $this->assertInstanceOf(Key::class, $cachedKeySet['bar']); $this->assertSame('bar', $cachedKeySet['bar']->getAlgorithm()); } public function testKeyIdIsCachedFromPreviousFormat() { $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->willReturn(true); $cacheItem->get() ->willReturn($this->testJwks1); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwksUriKey) ->willReturn($cacheItem->reveal()); $cache->save(Argument::any()) ->willReturn(true); $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->prophesize(ClientInterface::class)->reveal(), $this->prophesize(RequestFactoryInterface::class)->reveal(), $cache->reveal() ); $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); } public function testCachedKeyIdRefreshFromPreviousFormat() { $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->shouldBeCalledOnce() ->willReturn(true); $cacheItem->get() ->shouldBeCalledOnce() ->willReturn($this->testJwks1); $cacheItem->set(Argument::any()) ->shouldBeCalledOnce() ->will(function () { return $this; }); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwksUriKey) ->shouldBeCalledOnce() ->willReturn($cacheItem->reveal()); $cache->save(Argument::any()) ->shouldBeCalledOnce() ->willReturn(true); $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->getMockHttpClient($this->testJwks2), // updated JWK $this->getMockHttpFactory(), $cache->reveal() ); $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); $this->assertInstanceOf(Key::class, $cachedKeySet['bar']); $this->assertSame('bar', $cachedKeySet['bar']->getAlgorithm()); } public function testCacheItemWithExpiresAfter() { $expiresAfter = 10; $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->shouldBeCalledOnce() ->willReturn(false); $cacheItem->set(Argument::any()) ->shouldBeCalledOnce() ->will(function () { return $this; }); $cacheItem->expiresAfter($expiresAfter) ->shouldBeCalledOnce() ->will(function () { return $this; }); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwksUriKey) ->shouldBeCalledOnce() ->willReturn($cacheItem->reveal()); $cache->save(Argument::any()) ->shouldBeCalledOnce(); $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->getMockHttpClient($this->testJwks1), $this->getMockHttpFactory(), $cache->reveal(), $expiresAfter ); $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); } public function testJwtVerify() { $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); $payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')]; $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); // format the cached value to match the expected format $cachedJwks = []; $rsaKeySet = file_get_contents(__DIR__ . '/data/rsa-jwkset.json'); foreach (json_decode($rsaKeySet, true)['keys'] as $k => $v) { $cachedJwks[$v['kid']] = $v; } $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->willReturn(true); $cacheItem->get() ->willReturn($cachedJwks); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwksUriKey) ->willReturn($cacheItem->reveal()); $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->prophesize(ClientInterface::class)->reveal(), $this->prophesize(RequestFactoryInterface::class)->reveal(), $cache->reveal() ); $result = JWT::decode($msg, $cachedKeySet); $this->assertSame('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->testJwksUri, $this->getMockHttpClient($this->testJwks1, $shouldBeCalledTimes), $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])); } public function testRateLimitWithExpiresAfter() { // We request the key 17 times, HTTP should only be called 15 times $shouldBeCalledTimes = 10; $cachedTimes = 2; $afterExpirationTimes = 5; $totalHttpTimes = $shouldBeCalledTimes + $afterExpirationTimes; $cachePool = new TestMemoryCacheItemPool(); // Instantiate the cached key set $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->getMockHttpClient($this->testJwks1, $totalHttpTimes), $this->getMockHttpFactory($totalHttpTimes), $cachePool, 10, // expires after seconds true // enable rate limiting ); // Set the rate limit cache to expire after 1 second $cacheItem = $cachePool->getItem('jwksratelimitjwkshttpsjwk.uri'); $cacheItem->set([ 'expiry' => new \DateTime('+1 second', new \DateTimeZone('UTC')), 'callsPerMinute' => 0, ]); $cacheItem->expiresAfter(1); $cachePool->save($cacheItem); $invalidKid = 'invalidkey'; for ($i = 0; $i < $shouldBeCalledTimes; $i++) { $this->assertFalse(isset($cachedKeySet[$invalidKid])); } // The next calls do not call HTTP for ($i = 0; $i < $cachedTimes; $i++) { $this->assertFalse(isset($cachedKeySet[$invalidKid])); } sleep(1); // wait for cache to expire // These calls DO call HTTP because the cache has expired for ($i = 0; $i < $afterExpirationTimes; $i++) { $this->assertFalse(isset($cachedKeySet[$invalidKid])); } } /** * @dataProvider provideFullIntegration */ public function testFullIntegration(string $jwkUri): void { if (!class_exists(\GuzzleHttp\Psr7\HttpFactory::class)) { self::markTestSkipped('Guzzle 7 only'); } // 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, $factory, $cache ); $this->assertArrayHasKey($kid, $cachedKeySet); $key = $cachedKeySet[$kid]; $this->assertInstanceOf(Key::class, $key); $this->assertSame($keys['keys'][0]['alg'], $key->getAlgorithm()); } public function provideFullIntegration() { return [ [$this->googleRsaUri], [$this->googleEcUri, 'LYyP2g'] ]; } private function getMockHttpClient($testJwks, int $timesCalled = 1) { $body = $this->prophesize('Psr\Http\Message\StreamInterface'); $body->__toString() ->shouldBeCalledTimes($timesCalled) ->willReturn($testJwks); $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); $response->getBody() ->shouldBeCalledTimes($timesCalled) ->willReturn($body->reveal()); $response->getStatusCode() ->shouldBeCalledTimes($timesCalled) ->willReturn(200); $http = $this->prophesize(ClientInterface::class); $http->sendRequest(Argument::any()) ->shouldBeCalledTimes($timesCalled) ->willReturn($response->reveal()); return $http->reveal(); } private function getMockHttpFactory(int $timesCalled = 1) { $request = $this->prophesize('Psr\Http\Message\RequestInterface'); $factory = $this->prophesize(RequestFactoryInterface::class); $factory->createRequest('GET', $this->testJwksUri) ->shouldBeCalledTimes($timesCalled) ->willReturn($request->reveal()); return $factory->reveal(); } private function getMockEmptyCache() { $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->shouldBeCalledOnce() ->willReturn(false); $cacheItem->set(Argument::any()) ->will(function () { return $this; }); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwksUriKey) ->shouldBeCalledOnce() ->willReturn($cacheItem->reveal()); $cache->save(Argument::any()) ->willReturn(true); return $cache->reveal(); } } /** * A cache item pool */ final class TestMemoryCacheItemPool implements CacheItemPoolInterface { private $items; private $deferredItems; public function getItem($key): CacheItemInterface { $item = current($this->getItems([$key])); $item->expiresAt(null); // mimic symfony cache behavior return $item; } 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($key): bool { return isset($this->items[$key]) && $this->items[$key]->isHit(); } public function clear(): bool { $this->items = []; $this->deferredItems = []; return true; } public function deleteItem($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 $key; private $value; private $expiration; private $isHit = false; public function __construct(string $key) { $this->key = $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($expiration): static { $this->expiration = $expiration; return $this; } public function expiresAfter($time): static { $this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S")); return $this; } protected function currentTime() { return new \DateTime('now', new \DateTimeZone('UTC')); } }