-
Notifications
You must be signed in to change notification settings - Fork 215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add custom JWKS path and kid check to JWKFetcher + tests #287
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,16 +6,26 @@ | |
use Auth0\SDK\Helpers\Cache\CacheHandler; | ||
use Auth0\SDK\Helpers\Cache\NoCacheHandler; | ||
|
||
use GuzzleHttp\Exception\RequestException; | ||
use GuzzleHttp\Exception\ClientException; | ||
|
||
/** | ||
* Class JWKFetcher. | ||
* | ||
* @package Auth0\SDK\Helpers | ||
*/ | ||
class JWKFetcher | ||
{ | ||
|
||
/** | ||
* Cache handler or null for no caching. | ||
* | ||
* @var CacheHandler|NoCacheHandler | ||
* @var CacheHandler|null | ||
*/ | ||
private $cache = null; | ||
|
||
/** | ||
* Options for the Guzzle HTTP client. | ||
* | ||
* @var array | ||
*/ | ||
|
@@ -24,10 +34,10 @@ class JWKFetcher | |
/** | ||
* JWKFetcher constructor. | ||
* | ||
* @param CacheHandler|null $cache | ||
* @param array $guzzleOptions | ||
* @param CacheHandler|null $cache Cache handler or null for no caching. | ||
* @param array $guzzleOptions Options for the Guzzle HTTP client. | ||
*/ | ||
public function __construct(CacheHandler $cache = null, $guzzleOptions = []) | ||
public function __construct(CacheHandler $cache = null, array $guzzleOptions = []) | ||
{ | ||
if ($cache === null) { | ||
$cache = new NoCacheHandler(); | ||
|
@@ -38,22 +48,32 @@ public function __construct(CacheHandler $cache = null, $guzzleOptions = []) | |
} | ||
|
||
/** | ||
* Convert a certificate to PEM format. | ||
* | ||
* @param string $cert X509 certificate to convert to PEM format. | ||
* | ||
* @param string $cert | ||
* @return string | ||
*/ | ||
protected function convertCertToPem($cert) | ||
lbalmaceda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
return '-----BEGIN CERTIFICATE-----'.PHP_EOL.chunk_split($cert, 64, PHP_EOL).'-----END CERTIFICATE-----'.PHP_EOL; | ||
$output = '-----BEGIN CERTIFICATE-----'.PHP_EOL; | ||
$output .= chunk_split($cert, 64, PHP_EOL); | ||
$output .= '-----END CERTIFICATE-----'.PHP_EOL; | ||
return $output; | ||
} | ||
|
||
// phpcs:disable | ||
/** | ||
* 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this is going to get deprecated. Anyway, I'd document this parameter like: "The auth0 issuer which will get appended the |
||
* | ||
* @return array|mixed|null | ||
* | ||
* @throws \Exception | ||
* | ||
* @codeCoverageIgnore | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. /**
* @luchoIngore
*/ |
||
*/ | ||
public function fetchKeys($iss) | ||
lbalmaceda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
|
@@ -79,4 +99,100 @@ public function fetchKeys($iss) | |
|
||
return $secret; | ||
} | ||
// phpcs:enable | ||
|
||
/** | ||
* Fetch x509 cert for RS256 token decoding. | ||
* | ||
* @param string $jwks_url URL to the JWKS. | ||
* @param string|null $kid Key ID to use; returns first JWK if $kid is null or empty. | ||
* | ||
* @return string|null - Null if an x5c key could not be found for a key ID or if the JWKS is empty/invalid. | ||
*/ | ||
public function requestJwkX5c($jwks_url, $kid = null) | ||
{ | ||
$cache_key = $jwks_url.'|'.(string) $kid; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. think of the case that kid is null (not passed as arg). This will also break the cache impl below ;) I personally would skip cache in this particular case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if |
||
|
||
$x5c = $this->cache->get($cache_key); | ||
if (! is_null($x5c)) { | ||
return $x5c; | ||
} | ||
|
||
$jwks = $this->requestJwks($jwks_url); | ||
$jwk = $this->findJwk($jwks, $kid); | ||
|
||
if ($this->subArrayHasEmptyFirstItem($jwk, 'x5c')) { | ||
return null; | ||
} | ||
|
||
$x5c = $this->convertCertToPem($jwk['x5c'][0]); | ||
$this->cache->set($cache_key, $x5c); | ||
return $x5c; | ||
} | ||
|
||
/** | ||
* Get a JWKS from a specific URL. | ||
* | ||
* @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 requestJwks($jwks_url) | ||
{ | ||
$request = new RequestBuilder([ | ||
'domain' => $jwks_url, | ||
'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 findJwk(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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why? you could easily unit-test this :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's private, why would I do that? Also:
|
||
*/ | ||
private function subArrayHasEmptyFirstItem($array, $key) | ||
{ | ||
return empty($array) || ! is_array($array[$key]) || empty($array[$key][0]); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But above you say "NoCacheHandler" as well. Should people pass NoCacheHandler or null for this to have no cache?