auth0 / node-jwks-rsa

A library to retrieve RSA public keys from a JWKS (JSON Web Key Set) endpoint.
MIT License
836 stars 236 forks source link

Cache broken when key is missing kid #294

Closed sfiodorov closed 2 years ago

sfiodorov commented 2 years ago

Describe the problem

node-jwks-rsa integration Have jwks endpoint with the single key without a "kid" field. Each try to validate JWT signed by this key leads to HTTP requests to jwks endpoint.

What was the expected behavior?

Since that package supports single key without kid (see JwksClient.js line :74) it should support the cache as well

sfiodorov commented 2 years ago

changing cache.js to have fake kid in case of kid is not defined might help : from:

return promisify(memoizer({
    hash: (kid) => kid,
    load: callbackify(client.getSigningKey.bind(client)),
    maxAge: cacheMaxAge,
    max: cacheMaxEntries
  }));

to

return promisify(memoizer({
    hash: (kid) => kid || "fakeKid",
    load: callbackify(client.getSigningKey.bind(client)),
    maxAge: cacheMaxAge,
    max: cacheMaxEntries
  }));
adamjmcgrath commented 2 years ago

Thanks for raising this @sfiodorov - we'll investigate. In the meantime, please continue to use your workaround

adamjmcgrath commented 2 years ago

Hi @sfiodorov - I'm not able to reproduce your issue

When I use a JWKS with a single JWK without a kid, the caching seems to work as expected - see https://replit.com/@AdamMcGrath/DryLightcyanNanotechnology

jose adds a kid if one is not present, and the SDK uses that for the cache key

sfiodorov commented 2 years ago

@adamjmcgrath , how jose adds the same kid to both jwt header and the signing key to be matched?

adamjmcgrath commented 2 years ago

@sfiodorov - I'm assuming your JWT header doesn't have a kid, if your JWKS doesn't have one to match against (see that getSigningKey is not being called with a value for kid in https://replit.com/@AdamMcGrath/DryLightcyanNanotechnology)

sfiodorov commented 2 years ago

Yes, both jwt header and key are missing kid. You are right here keys created by a call to getSigningKeys() are with the kid. But the problem that happens after the call to JWKS endpoint. When the key comes from JWKS it comes without a kid, so it is cached with undefined. Need to have some stub key for this case before calling to memoizer

adamjmcgrath commented 2 years ago

When the key comes from JWKS it comes without a kid, so it is cached with undefined.

The SigningKey is cached (not the JWKS) and the SigningKey is created with a kid

Perhaps you could update https://replit.com/@AdamMcGrath/DryLightcyanNanotechnology to demonstrate the issue, because I'm still not seeing one

sfiodorov commented 2 years ago

@adamjmcgrath thanks, i will try to reproduce with this code

sfiodorov commented 2 years ago

Wierd. When running your example I see that at lru-memoizer\lib\async.js that is used by the cache, the key is cached with an undefined key. The same cache does not work with the "undefined" cache key at my code. image

Please let me know if it is correct that you cache wiht "undefined" as key cache key

adamjmcgrath commented 2 years ago

Please let me know if it is correct that you cache wiht "undefined" as key cache key

Yes - it comes from the value of the kid passed to getSigningKey

Doesn't seem to be a problem in any version of node I've tested, what happens when you do:

$ node -e "LRU=require('lru-cache');lru = new LRU(); lru.set(undefined, 'foo'); console.log(lru.get(undefined))"
adamjmcgrath commented 2 years ago

Or, for that matter:

$ node -e "lru = new Map(); lru.set(undefined, 'foo'); console.log(lru.get(undefined))"
sfiodorov commented 2 years ago

Seems like it is some issue on my side. Thanks for your assistance. I will need to recheck my implementation side.