diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist
index e1e09df09..c3824bdfc 100644
--- a/.phpcs.xml.dist
+++ b/.phpcs.xml.dist
@@ -95,6 +95,7 @@
+
diff --git a/src/Helpers/JWKFetcher.php b/src/Helpers/JWKFetcher.php
index 6a263d8cd..738eb9153 100644
--- a/src/Helpers/JWKFetcher.php
+++ b/src/Helpers/JWKFetcher.php
@@ -6,6 +6,9 @@
use Auth0\SDK\Helpers\Cache\CacheHandler;
use Auth0\SDK\Helpers\Cache\NoCacheHandler;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Exception\ClientException;
+
/**
* Class JWKFetcher.
*
@@ -17,7 +20,7 @@ class JWKFetcher
/**
* Cache handler or null for no caching.
*
- * @var CacheHandler|NoCacheHandler|null
+ * @var CacheHandler|null
*/
private $cache = null;
@@ -59,101 +62,138 @@ protected function convertCertToPem($cert)
return $output;
}
+ // phpcs:disable
/**
- * Fetch x509 cert for RS256 token decoding.
+ * Appends the default JWKS path to a token issuer to return all keys from a JWKS.
+ * TODO: Deprecate, use $this->getJwksX5c() instead and explain why/how.
+ *
+ * @param string $iss
*
- * @param string $domain Base domain for the JWKS, including scheme.
- * @param string|null $kid Kid to use.
- * @param string $path Path to the JWKS from the $domain.
+ * @return array|mixed|null
*
- * @return mixed
+ * @throws \Exception
*
- * @throws \Exception If the Guzzle HTTP client cannot complete the request.
+ * @codeCoverageIgnore
*/
- public function fetchKeys($domain, $kid = null, $path = '.well-known/jwks.json')
+ public function fetchKeys($iss)
{
- $jwks_url = $domain.$path;
-
- // Check for a cached JWKS value.
- $secret = $this->cache->get($jwks_url);
- if (! is_null($secret)) {
- return $secret;
- }
-
- $secret = [];
+ $url = "{$iss}.well-known/jwks.json";
- $jwks = $this->getJwks( $domain, $path );
+ if (($secret = $this->cache->get($url)) === null) {
+ $secret = [];
- if (! is_array( $jwks['keys'] ) || empty( $jwks['keys'] )) {
- return $secret;
- }
-
- // No kid passed so get the kid of the first JWK.
- if (is_null($kid)) {
- $kid = $this->getProp( $jwks, 'kid' );
- }
+ $request = new RequestBuilder([
+ 'domain' => $iss,
+ 'basePath' => '.well-known/jwks.json',
+ 'method' => 'GET',
+ 'guzzleOptions' => $this->guzzleOptions
+ ]);
+ $jwks = $request->call();
- $x5c = $this->getProp( $jwks, 'x5c', $kid );
+ foreach ($jwks['keys'] as $key) {
+ $secret[$key['kid']] = $this->convertCertToPem($key['x5c'][0]);
+ }
- // Need the kid and x5c for a well-formed return value.
- if (! is_null($kid) && ! is_null($x5c)) {
- $secret[$kid] = $this->convertCertToPem($x5c);
- $this->cache->set($jwks_url, $secret);
+ $this->cache->set($url, $secret);
}
return $secret;
}
+ // phpcs:enable
/**
- * Get a specific property from a JWKS using a key, if provided.
+ * Fetch x509 cert for RS256 token decoding.
*
- * @param array $jwks JWKS to parse.
- * @param string $prop Property to retrieve.
- * @param null|string $kid Kid to check.
+ * @param string $jwks_url URL to the JWKS.
+ * @param string|null $kid Key ID.
*
- * @return null|string
+ * @return string|null
*/
- public function getProp(array $jwks, $prop, $kid = null)
+ public function getJwksX5c($jwks_url, $kid = null)
{
- $r_key = null;
- if (! $kid) {
- // No kid indicated, get the first entry.
- $r_key = $jwks['keys'][0];
- } else {
- // Iterate through the JWKS for the correct kid.
- foreach ($jwks['keys'] as $key) {
- if (isset($key['kid']) && $key['kid'] === $kid) {
- $r_key = $key;
- break;
- }
- }
+ $cache_key = $jwks_url.'|'.(string) $kid;
+
+ $x5c = $this->cache->get($cache_key);
+ if (! is_null($x5c)) {
+ return $x5c;
}
- // If a key was not found or the property does not exist, return.
- if (is_null($r_key) || ! isset($r_key[$prop])) {
+ $jwks = $this->getJwks($jwks_url);
+ $jwk = $this->getJwk($jwks, $kid);
+
+ if ($this->subArrayHasEmptyFirstItem($jwk, 'x5c')) {
return null;
}
- // If the value is an array, get the first element.
- return is_array( $r_key[$prop] ) ? $r_key[$prop][0] : $r_key[$prop];
+ $x5c = $this->convertCertToPem($jwk['x5c'][0]);
+ $this->cache->set($cache_key, $x5c);
+ return $x5c;
}
/**
* Get a JWKS given a domain and path to call.
*
- * @param string $domain Base domain for the JWKS, including scheme.
- * @param string $path Path to the JWKS from the $domain.
+ * @param string $jwks_url URL to the JWKS.
*
* @return mixed|string
+ *
+ * @throws RequestException If $jwks_url is empty or malformed.
+ * @throws ClientException If the JWKS cannot be retrieved.
+ *
+ * @codeCoverageIgnore
*/
- protected function getJwks($domain, $path)
+ protected function getJwks($jwks_url)
{
$request = new RequestBuilder([
- 'domain' => $domain,
- 'basePath' => $path,
+ 'domain' => $jwks_url,
+ 'basePath' => '',
'method' => 'GET',
'guzzleOptions' => $this->guzzleOptions
]);
return $request->call();
}
+
+ /**
+ * Get a JWK from a JWKS using a key ID, if provided.
+ *
+ * @param array $jwks JWKS to parse.
+ * @param null|string $kid Key ID to return; returns first JWK if $kid is null or empty.
+ *
+ * @return array|null Null if the keys array is empty or if the key ID is not found.
+ *
+ * @codeCoverageIgnore
+ */
+ private function getJwk(array $jwks, $kid = null)
+ {
+ if ($this->subArrayHasEmptyFirstItem($jwks, 'keys')) {
+ return null;
+ }
+
+ if (! $kid) {
+ return $jwks['keys'][0];
+ }
+
+ foreach ($jwks['keys'] as $key) {
+ if (isset($key['kid']) && $key['kid'] === $kid) {
+ return $key;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Check if an array within an array has a non-empty first item.
+ *
+ * @param array|null $array Main array to check.
+ * @param string $key Key pointing to a sub-array.
+ *
+ * @return boolean
+ *
+ * @codeCoverageIgnore
+ */
+ private function subArrayHasEmptyFirstItem($array, $key)
+ {
+ return empty($array) || ! is_array($array[$key]) || empty($array[$key][0]);
+ }
}
diff --git a/src/JWTVerifier.php b/src/JWTVerifier.php
index 1aa90d842..73218435a 100644
--- a/src/JWTVerifier.php
+++ b/src/JWTVerifier.php
@@ -23,6 +23,8 @@ class JWTVerifier
protected $secret_base64_encoded = null;
+ protected $jwks_path = '.well-known/jwks.json';
+
/**
* JWTVerifier Constructor.
*
@@ -87,6 +89,10 @@ public function __construct($config)
$this->client_secret = $config['client_secret'];
}
+ if (isset($config['jwks_path'])) {
+ $this->jwks_path = (string) $config['jwks_path'];
+ }
+
$this->JWKFetcher = new JWKFetcher($cache, $guzzleOptions);
}
@@ -125,7 +131,8 @@ public function verifyAndDecode($jwt)
throw new CoreException("We can't trust on a token issued by: `{$body->iss}`.");
}
- $secret = $this->JWKFetcher->fetchKeys($body->iss);
+ $jwks_url = $body->iss.$this->jwks_path;
+ $secret = $this->JWKFetcher->fetchKeys($jwks_url);
} else if ($head->alg === 'HS256') {
if ($this->secret_base64_encoded) {
$secret = JWT::urlsafeB64Decode($this->client_secret);
diff --git a/src/Store/SessionStore.php b/src/Store/SessionStore.php
index 2707cfc1a..caad5fba2 100644
--- a/src/Store/SessionStore.php
+++ b/src/Store/SessionStore.php
@@ -122,9 +122,10 @@ public function delete($key)
public function getSessionKeyName($key)
{
$key_name = $key;
- if ( ! empty( $this->session_base_name ) ) {
+ if (! empty( $this->session_base_name )) {
$key_name = $this->session_base_name.'_'.$key_name;
}
+
return $key_name;
}
}
diff --git a/tests/Helpers/JWKFetcherTest.php b/tests/Helpers/JWKFetcherTest.php
index 35857eeb1..a19606088 100644
--- a/tests/Helpers/JWKFetcherTest.php
+++ b/tests/Helpers/JWKFetcherTest.php
@@ -1,7 +1,8 @@
getStub();
- $keys = $jwksFetcher->fetchKeys( 'https://localhost/' );
+ $pem = $jwksFetcher->getJwksX5c( 'https://localhost/.well-known/jwks.json' );
- $this->assertCount(1, $keys);
+ $this->assertNotEmpty($pem);
- $pem_parts = $this->getPemParts( $keys );
+ $pem_parts = explode( PHP_EOL, $pem );
$this->assertEquals( 4, count($pem_parts) );
$this->assertEquals( '-----BEGIN CERTIFICATE-----', $pem_parts[0] );
@@ -36,15 +37,15 @@ public function testFetchKeysWithoutKid()
*
* @return void
*/
- public function testFetchKeysWithKid()
+ public function testGetJwksX5cWithKid()
{
$jwksFetcher = $this->getStub();
- $keys = $jwksFetcher->fetchKeys( 'https://localhost/', '__test_kid_2__' );
+ $pem = $jwksFetcher->getJwksX5c( 'https://localhost/.well-known/jwks.json', '__test_kid_2__' );
- $this->assertCount(1, $keys);
+ $this->assertNotEmpty($pem);
- $pem_parts = $this->getPemParts( $keys );
+ $pem_parts = explode( PHP_EOL, $pem );
$this->assertEquals( 4, count($pem_parts) );
$this->assertEquals( '-----BEGIN CERTIFICATE-----', $pem_parts[0] );
@@ -57,15 +58,15 @@ public function testFetchKeysWithKid()
*
* @return void
*/
- public function testFetchKeysWithPath()
+ public function testGetJwksX5cWithPath()
{
$jwksFetcher = $this->getStub();
- $keys = $jwksFetcher->fetchKeys( 'https://localhost/', '__test_custom_kid__', '.custom/jwks.json' );
+ $pem = $jwksFetcher->getJwksX5c( 'https://localhost/.custom/jwks.json', '__test_custom_kid__' );
- $this->assertCount(1, $keys);
+ $this->assertNotEmpty($pem);
- $pem_parts = $this->getPemParts( $keys );
+ $pem_parts = explode( PHP_EOL, $pem );
$this->assertEquals( 4, count( $pem_parts ) );
$this->assertEquals( '-----BEGIN CERTIFICATE-----', $pem_parts[0] );
@@ -78,13 +79,13 @@ public function testFetchKeysWithPath()
*
* @return void
*/
- public function testFetchKeysNoX5c()
+ public function testGetJwksX5cNoX5c()
{
$jwksFetcher = $this->getStub();
- $keys = $jwksFetcher->fetchKeys( 'https://localhost/', '__no_x5c_test_kid_2__' );
+ $pem = $jwksFetcher->getJwksX5c( 'https://localhost/.custom/jwks.json', '__no_x5c_test_kid_2__' );
- $this->assertEmpty($keys);
+ $this->assertEmpty($pem);
}
/**
@@ -117,26 +118,21 @@ public function testConvertCertToPem()
$this->assertEquals( '-----END CERTIFICATE-----', $pem_parts[3] );
}
- /**
- * Test that the protected getProp method returns correctly.
- *
- * @return void
- */
- public function testGetProp()
+ public function testCacheReturn()
{
- $jwks = $this->getLocalJwks( 'test', '-jwks' );
+ $cache_handler = new TestCacheHandler();
+ $jwksFetcher = $this->getStub($cache_handler);
- $jwksFetcher = new JWKFetcher();
+ $jwks_url = 'https://localhost/.well-known/jwks.json';
+ $kid = '__test_kid_2__';
- $this->assertEquals( '__string_value_1__', $jwksFetcher->getProp( $jwks, 'string' ) );
- $this->assertEquals( '__array_value_1__', $jwksFetcher->getProp( $jwks, 'array' ) );
- $this->assertNull( $jwksFetcher->getProp( $jwks, 'invalid' ) );
+ $pem = $jwksFetcher->getJwksX5c( $jwks_url, $kid );
+ $this->assertNotEmpty($pem);
- $test_kid = '__kid_value__';
+ $cache_handler->set( $jwks_url.'|'.$kid, '__new_value__' );
+ $pem = $jwksFetcher->getJwksX5c( $jwks_url, $kid );
- $this->assertEquals( '__string_value_2__', $jwksFetcher->getProp( $jwks, 'string', $test_kid ) );
- $this->assertEquals( '__array_value_3__', $jwksFetcher->getProp( $jwks, 'array', $test_kid ) );
- $this->assertNull( $jwksFetcher->getProp( $jwks, 'invalid', $test_kid ) );
+ $this->assertEquals( '__new_value__', $pem );
}
/**
@@ -165,11 +161,14 @@ public function getLocalJwks($domain = '', $path = '')
/**
* Stub the JWKFetcher class.
*
+ * @param null $cache_handler - Cache handler to use or null if no cache.
+ *
* @return \PHPUnit_Framework_MockObject_MockObject
*/
- private function getStub()
+ private function getStub($cache_handler = null)
{
$stub = $this->getMockBuilder(JWKFetcher::class)
+ ->setConstructorArgs([$cache_handler])
->setMethods(['getJwks'])
->getMock();
diff --git a/tests/TestCacheHandler.php b/tests/TestCacheHandler.php
new file mode 100644
index 000000000..e43556676
--- /dev/null
+++ b/tests/TestCacheHandler.php
@@ -0,0 +1,40 @@
+cache[$key]) ? $this->cache[$key] : null;
+ }
+
+ /**
+ *
+ * @param string $key
+ */
+ public function delete($key)
+ {
+ unset($this->cache[$key]);
+ }
+
+ /**
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ public function set($key, $value)
+ {
+ $this->cache[$key] = $value;
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 891158492..cf58548f2 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,8 +1,8 @@