-
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
Conversation
d1decae
to
5f2660d
Compare
94dad65
to
5fba1de
Compare
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.
Is there any chance that you could keep the existing [method that returns an array instance with "kid" as keys] as deprecated and add a new one that just returns the key for a given kid? Making the code from now on use the new one and keeping the deprecated one just in case someone was using it directly.
src/Helpers/JWKFetcher.php
Outdated
|
||
if (($secret = $this->cache->get($url)) === null) { | ||
$secret = []; | ||
// No kid passed so get the kid of the first JWK. |
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.
This assumption is wrong. kid
is not required by the spec, so if the array contains a single element (one key) and kid==null then yes you might return that key directly. But no need to be picking the kid value here just "to use it later"
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.
You'll see further down in this file that the kid
is used in the returned value:
$secret[$kid] = $this->convertCertToPem($x5c);
That's how it was stored before so we can't change that.
src/Helpers/JWKFetcher.php
Outdated
// 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); |
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.
this cache is a little bit of a lie 😛 you're saving under the same key every time an array with only 1 key (the one that matches the "kid" passed). So if your app requires a second kid, the previous one would be overwritten. Not longer useful as a "cache", is it? ⚡️
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.
Totally agree but legacy 🤷♂️
src/Helpers/JWKFetcher.php
Outdated
public function getProp(array $jwks, $prop, $kid = null) | ||
{ | ||
$r_key = null; | ||
if (! $kid) { |
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.
shouldn't this be "is null or empty" or that's what this is already doing?
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.
That's what this is already doing. There would be a problem if $kid
was not defined but it always is.
src/Helpers/JWKFetcher.php
Outdated
} | ||
|
||
/** | ||
* Get a specific property from a JWKS using a key, if provided. |
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.
using a "key id" if provided. Or something that makes clearer that "key" refers to the JWK and not the prop name.
src/Helpers/JWKFetcher.php
Outdated
* | ||
* @return null|string | ||
*/ | ||
public function getProp(array $jwks, $prop, $kid = null) |
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.
No need to keep this public. Also, I think it would be more useful having a method that given a JWKS returns the array for the given "kid" value. Then you could do result["prop"] to obtain it. Looks clearer than having a "get prop" method that is also checking for "kid" IMO.
src/Helpers/JWKFetcher.php
Outdated
} | ||
|
||
return $secret; | ||
// If the value is an array, get the first element. | ||
return is_array( $r_key[$prop] ) ? $r_key[$prop][0] : $r_key[$prop]; |
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.
If I'm iterating over props of a file I know, I'd like to receive it as is, not being handled as non-array stuff. So I expect this method behaves the same on "any type of prop". e.g. make this return the array or null, and you later could do result[0] on the base of knowing how the JWKS is constructed.
src/Helpers/JWKFetcher.php
Outdated
* 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. |
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.
how can this fail and how can the dev know the reason? Is this "catchable"?
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.
Can fail with an Exception in the HTTP client, I'll add docs.
@lbalmaceda - RE: re-writing and deprecating ... that's not a bad idea, TBH. I'll make the 👍 changes above and move that to it's own method. |
31c8465
to
5be5132
Compare
* | ||
* @throws \Exception If the Guzzle HTTP client cannot complete the request. | ||
* @codeCoverageIgnore |
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.
👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀
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.
🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈 🙈
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.
/**
* @luchoIngore
*/
src/Helpers/JWKFetcher.php
Outdated
* @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. |
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.
"if not provided and a single JSON Web Key exists on the set, that one will be returned"
src/Helpers/JWKFetcher.php
Outdated
// 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); |
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.
I'd call this findKeyById
or findJwk
break; | ||
} | ||
} | ||
$cache_key = $jwks_url.'|'.(string) $kid; |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
null
is technically a value we can handle (first key) so why not cache for that value?
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.
if null
get's converted to "null"
(note the quotes) then I guess it's a valid string and you will end up having a valid key, so yes, you will be able to handle just fine null kids (pun intended)
src/Helpers/JWKFetcher.php
Outdated
*/ | ||
public function getProp(array $jwks, $prop, $kid = null) | ||
public function getJwksX5c($jwks_url, $kid = null) |
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.
this is a key's certificate. So it would be JWK not JWKS
src/Helpers/JWKFetcher.php
Outdated
} | ||
|
||
/** | ||
* Check if an array within an array has at least one element. |
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.
method is called subArrayHasOneElement
, not HasAtLeastOne ⚡️ . Choose either but be concise
* | ||
* @return boolean | ||
* | ||
* @codeCoverageIgnore |
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.
why? you could easily unit-test this :D
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.
It's private, why would I do that? Also:
Please note that final, private, protected, and static methods cannot be stubbed or mocked.
src/JWTVerifier.php
Outdated
@@ -125,7 +131,7 @@ public function verifyAndDecode($jwt) | |||
throw new CoreException("We can't trust on a token issued by: `{$body->iss}`."); | |||
} | |||
|
|||
$secret = $this->JWKFetcher->fetchKeys($body->iss); | |||
$secret = $this->JWKFetcher->fetchKeys($body->iss.$this->jwks_path); |
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.
let's move the $body->iss.$this->jwks_path
one line above to something like jwks_url
for clarity
src/Helpers/JWKFetcher.php
Outdated
*/ | ||
protected function getJwks($domain, $path) | ||
protected function getJwks($jwks_url) |
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.
suggestion: I'd call this "fetch" or "request" jwks. Since it's performing an external query
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.
So does getJwkX5c
... change that too?
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.
If you feel like it, go ahead. Remember that's the method you want to expose to the user (the public method). These other in turn are internal (protected methods)
8a6b3d8
to
a339af8
Compare
@@ -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. |
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?
/** | ||
* TODO: Deprecate, use $this->getJwksX5c() instead. | ||
* | ||
* @param string $iss |
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.
I know this is going to get deprecated. Anyway, I'd document this parameter like: "The auth0 issuer which will get appended the /.well-known/jwks.json
path to fetch the jwks."
src/Helpers/JWKFetcher.php
Outdated
*/ | ||
private function getJwk(array $jwks, $kid = null) | ||
{ | ||
if (! $this->subArrayHasNonEmptyFirstItem($jwks, 'keys')) { |
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.
Method is negated ("..has NON empty..") but you're also negating it on every use. What about making it return the final result you want?
7da85e7
to
3fa2637
Compare
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.
Usually we don't add a doc block to the test cases. That confused me because then I found a few "test helper functions". We just make sure the name of the test is clear enough to communicate the intent of the test. (e.g. using "shouldDoThis" or "shouldReturnThis" or "shouldThrowWhenThisHappened".
This is not a blocker, but for future tests you might want to implement this.
tests/Helpers/JWKFetcherTest.php
Outdated
$jwks_url = 'https://localhost/.well-known/jwks.json'; | ||
$kid = '__test_kid_2__'; | ||
|
||
$pem = $jwksFetcher->getJwksX5c( $jwks_url, $kid ); |
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.
before this line I'd make sure that cache for that key is not defined so test is fully independent:
`$cache_handler->set( $jwks_url.'|'.$kid, null );`
$this->assertEquals( '__new_value__', $pem ); | ||
} | ||
|
||
/** |
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.
I'd add a comment line above this saying that from now on, below you can find "test helper functions".
tests/Helpers/JWKFetcherTest.php
Outdated
|
||
public function testCacheReturn() | ||
{ | ||
$cache_handler = new TestCacheHandler(); |
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.
Testing the CacheHandler this way is sort of wrong, since this TestCacheHandler
is not the implementation that the user will use, right? wink. Instead, you should mock a CacheHandler
and assert that get/set/remove
methods get called N times with the given XYZ arguments. You can then remove the TestCacheHandler
class introduced in this PR.
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.
Well, we don't know the implementation being used, right? So, in this case, it's testing that "some" CacheHandler implementation will work, as long as it's following the right interface. I could add this as a MemoryCacheHandler
"official" implementation?
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.
Well, we don't know the implementation being used, right?
No, but you know the interface and you know you're going to call it to
a) store
b) retrieve
c) delete
Adding a subclass for testing purposes is not wrong but it's not the right way either. IMO, you should create a new mock from the interface definition and assert the calls are being made correctly.
I could add this as a MemoryCacheHandler "official" implementation
If you want, go ahead. But remember whatever you ship can't be undone 💃 and unless you have a "best practice in storage/cache" to recommend users to begin using this class, I'd stay away
3fa2637
to
213a958
Compare
213a958
to
e990275
Compare
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.
LGTM. Left a few comments via DM 👍
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Add a custom path to a JWKS and kid checking (this will be added to the ID token validation in another PR).
$kid
and$path
parameters to\Auth0\SDK\Helpers\JWKFetcher::fetchKeys
\Auth0\SDK\Helpers\JWKFetcher::getJwks
to call the JWKS URL\Auth0\SDK\Helpers\JWKFetcher::getProp
to get a key from the JWKSCloses #276