diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index 077dceb0..f1580c92 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -68,6 +68,10 @@ class CachedKeySet implements ArrayAccess * @var int */ private $maxCallsPerMinute = 10; + /** + * @var string|null + */ + private $defaultAlg; public function __construct( string $jwksUri, @@ -75,7 +79,8 @@ public function __construct( RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, int $expiresAfter = null, - bool $rateLimit = false + bool $rateLimit = false, + string $defaultAlg = null ) { $this->jwksUri = $jwksUri; $this->httpClient = $httpClient; @@ -83,6 +88,7 @@ public function __construct( $this->cache = $cache; $this->expiresAfter = $expiresAfter; $this->rateLimit = $rateLimit; + $this->defaultAlg = $defaultAlg; $this->setCacheKeys(); } @@ -143,7 +149,7 @@ private function keyIdExists(string $keyId): bool $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); + $this->keySet = $keySetToCache = JWK::parseKeySet($jwks, $this->defaultAlg); if (!isset($this->keySet[$keyId])) { return false; diff --git a/src/JWK.php b/src/JWK.php index dbc446e6..7f225701 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -24,6 +24,8 @@ class JWK * Parse a set of JWK keys * * @param array $jwks The JSON Web Key Set as an associative array + * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the + * JSON Web Key Set * * @return array An associative array of key IDs (kid) to Key objects * @@ -33,7 +35,7 @@ class JWK * * @uses parseKey */ - public static function parseKeySet(array $jwks): array + public static function parseKeySet(array $jwks, string $defaultAlg = null): array { $keys = []; @@ -47,7 +49,7 @@ public static function parseKeySet(array $jwks): array foreach ($jwks['keys'] as $k => $v) { $kid = isset($v['kid']) ? $v['kid'] : $k; - if ($key = self::parseKey($v)) { + if ($key = self::parseKey($v, $defaultAlg)) { $keys[(string) $kid] = $key; } } @@ -63,6 +65,8 @@ public static function parseKeySet(array $jwks): array * Parse a JWK key * * @param array $jwk An individual JWK + * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the + * JSON Web Key Set * * @return Key The key object for the JWK * @@ -72,7 +76,7 @@ public static function parseKeySet(array $jwks): array * * @uses createPemFromModulusAndExponent */ - public static function parseKey(array $jwk): ?Key + public static function parseKey(array $jwk, string $defaultAlg = null): ?Key { if (empty($jwk)) { throw new InvalidArgumentException('JWK must not be empty'); @@ -83,10 +87,14 @@ public static function parseKey(array $jwk): ?Key } if (!isset($jwk['alg'])) { - // The "alg" parameter is optional in a KTY, but is required for parsing in - // this library. Add it manually to your JWK array if it doesn't already exist. - // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 - throw new UnexpectedValueException('JWK must contain an "alg" parameter'); + if (\is_null($defaultAlg)) { + // The "alg" parameter is optional in a KTY, but an algorithm is required + // for parsing in this library. Use the $defaultAlg parameter when parsing the + // key set in order to prevent this error. + // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + throw new UnexpectedValueException('JWK must contain an "alg" parameter'); + } + $jwk['alg'] = $defaultAlg; } switch ($jwk['kty']) { diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index 22e1de5f..a13c80fd 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -18,6 +18,7 @@ class CachedKeySetTest extends TestCase 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 $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'; @@ -95,6 +96,21 @@ public function testWithExistingKeyId() $this->assertEquals('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->assertEquals('baz256', $cachedKeySet['baz']->getAlgorithm()); + } + public function testKeyIdIsCached() { $cacheItem = $this->prophesize(CacheItemInterface::class); diff --git a/tests/JWKTest.php b/tests/JWKTest.php index 4167a2ba..0bd4f636 100644 --- a/tests/JWKTest.php +++ b/tests/JWKTest.php @@ -58,6 +58,18 @@ public function testParsePrivateKeyWithoutAlg() JWK::parseKeySet($jwkSet); } + public function testParsePrivateKeyWithoutAlgWithDefaultAlgParameter() + { + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), + true + ); + unset($jwkSet['keys'][0]['alg']); + + $jwks = JWK::parseKeySet($jwkSet, 'foo'); + $this->assertEquals('foo', $jwks['jwk1']->getAlgorithm()); + } + public function testParseKeyWithEmptyDValue() { $jwkSet = json_decode(