forked from luciferous/jwt
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: add cached keyset #397
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
Merged
Merged
Changes from all commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
63ae30a
WIP: add CachedKeySet
bshaffer 6fa256e
add tests
bshaffer 66881d7
fix cs
bshaffer ff7339f
remove require-dev deps
bshaffer dc6e704
add integration test and integration test config
bshaffer 9051308
add more tests
bshaffer e366659
use psr/cache 2.0.0
bshaffer 56bb69e
Merge branch 'main' into add-cached-keyset
bshaffer 006cac9
Update bootstrap-system.php
bshaffer 82688db
Update CachedKeySetTest.php
bshaffer 19e8699
Merge branch 'main' into add-cached-keyset
bshaffer f9d72a4
remove special tests, update for PHP 8
bshaffer 6df0fbc
style fix
bshaffer 351f0cf
Merge branch 'main' into add-cached-keyset
bshaffer a0f8008
Update README.md
bshaffer 83ac363
Merge branch 'main' into add-cached-keyset
bshaffer e5f9a53
fix phpstan
bshaffer 5a98f72
fix tests
bshaffer 7341c6d
downgrade psr cache for compatibility (force-push to trigger actions)
bshaffer 58ff40e
Merge branch 'main' into add-cached-keyset
bshaffer bcccc51
Update composer.json
bshaffer 679746c
maybe fix dependency hell
bshaffer 3060488
add guzzle 6 for PHP 7.1
bshaffer 7595b5c
fix php7.1 tests
bshaffer 320a140
rearange deps
bshaffer 7f20b2d
Update README.md
bshaffer 341a415
Update src/CachedKeySet.php
bshaffer 663bfd0
Update src/CachedKeySet.php
bshaffer 617e67a
add rate limit
bshaffer acdc97a
remove mixed keyword for < PHP 8.0
bshaffer 4e9cebb
Merge branch 'main' into add-cached-keyset
bshaffer 967c5cb
change jwk to jwks to be mmore correct
bshaffer 27aa10a
Merge branch 'add-cached-keyset' of github.com:firebase/php-jwt into …
bshaffer 182a136
update README with ratelimit info
bshaffer 98adbe4
Update README.md
bshaffer 995b530
Merge branch 'main' into add-cached-keyset
bshaffer 2af9d51
Update README.md
bshaffer 7ba9d70
Update README.md
bshaffer b657333
Update README.md
bshaffer 1728e58
Merge branch 'main' into add-cached-keyset
bshaffer dd33453
Update src/CachedKeySet.php
bshaffer 2d22686
address more review comments
bshaffer d3a41ad
Merge branch 'add-cached-keyset' of github.com:firebase/php-jwt into …
bshaffer 04b46c1
fix phpstan implements
bshaffer e3f21d5
add example for cached key set example
bshaffer e7b31c6
Merge branch 'main' into add-cached-keyset
bshaffer 8aa0a5b
cs fixer and phpstan fixes
bshaffer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
<?php | ||
|
||
namespace Firebase\JWT; | ||
|
||
use ArrayAccess; | ||
use LogicException; | ||
use OutOfBoundsException; | ||
use Psr\Cache\CacheItemInterface; | ||
use Psr\Cache\CacheItemPoolInterface; | ||
use Psr\Http\Client\ClientInterface; | ||
use Psr\Http\Message\RequestFactoryInterface; | ||
use RuntimeException; | ||
|
||
/** | ||
* @implements ArrayAccess<string, Key> | ||
*/ | ||
class CachedKeySet implements ArrayAccess | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
private $jwksUri; | ||
/** | ||
* @var ClientInterface | ||
*/ | ||
private $httpClient; | ||
/** | ||
* @var RequestFactoryInterface | ||
*/ | ||
private $httpFactory; | ||
/** | ||
* @var CacheItemPoolInterface | ||
*/ | ||
private $cache; | ||
/** | ||
* @var ?int | ||
*/ | ||
private $expiresAfter; | ||
/** | ||
* @var ?CacheItemInterface | ||
*/ | ||
private $cacheItem; | ||
/** | ||
* @var array<string, Key> | ||
*/ | ||
private $keySet; | ||
/** | ||
* @var string | ||
*/ | ||
private $cacheKey; | ||
/** | ||
* @var string | ||
*/ | ||
private $cacheKeyPrefix = 'jwks'; | ||
/** | ||
* @var int | ||
*/ | ||
private $maxKeyLength = 64; | ||
/** | ||
* @var bool | ||
*/ | ||
private $rateLimit; | ||
/** | ||
* @var string | ||
*/ | ||
private $rateLimitCacheKey; | ||
/** | ||
* @var int | ||
*/ | ||
private $maxCallsPerMinute = 10; | ||
|
||
public function __construct( | ||
string $jwksUri, | ||
ClientInterface $httpClient, | ||
RequestFactoryInterface $httpFactory, | ||
CacheItemPoolInterface $cache, | ||
int $expiresAfter = null, | ||
bool $rateLimit = false | ||
) { | ||
$this->jwksUri = $jwksUri; | ||
$this->httpClient = $httpClient; | ||
$this->httpFactory = $httpFactory; | ||
$this->cache = $cache; | ||
$this->expiresAfter = $expiresAfter; | ||
$this->rateLimit = $rateLimit; | ||
$this->setCacheKeys(); | ||
} | ||
|
||
/** | ||
* @param string $keyId | ||
* @return Key | ||
*/ | ||
public function offsetGet($keyId): Key | ||
{ | ||
if (!$this->keyIdExists($keyId)) { | ||
throw new OutOfBoundsException('Key ID not found'); | ||
} | ||
return $this->keySet[$keyId]; | ||
} | ||
|
||
/** | ||
* @param string $keyId | ||
* @return bool | ||
*/ | ||
public function offsetExists($keyId): bool | ||
{ | ||
return $this->keyIdExists($keyId); | ||
} | ||
|
||
/** | ||
* @param string $offset | ||
* @param Key $value | ||
*/ | ||
public function offsetSet($offset, $value): void | ||
{ | ||
throw new LogicException('Method not implemented'); | ||
} | ||
|
||
/** | ||
* @param string $offset | ||
*/ | ||
public function offsetUnset($offset): void | ||
{ | ||
throw new LogicException('Method not implemented'); | ||
} | ||
|
||
private function keyIdExists(string $keyId): bool | ||
{ | ||
$keySetToCache = null; | ||
if (null === $this->keySet) { | ||
$item = $this->getCacheItem(); | ||
// Try to load keys from cache | ||
if ($item->isHit()) { | ||
// item found! Return it | ||
$this->keySet = $item->get(); | ||
} | ||
} | ||
|
||
if (!isset($this->keySet[$keyId])) { | ||
if ($this->rateLimitExceeded()) { | ||
return false; | ||
} | ||
$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); | ||
|
||
if (!isset($this->keySet[$keyId])) { | ||
return false; | ||
} | ||
} | ||
|
||
if ($keySetToCache) { | ||
$item = $this->getCacheItem(); | ||
$item->set($keySetToCache); | ||
if ($this->expiresAfter) { | ||
$item->expiresAfter($this->expiresAfter); | ||
} | ||
$this->cache->save($item); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private function rateLimitExceeded(): bool | ||
{ | ||
if (!$this->rateLimit) { | ||
return false; | ||
} | ||
|
||
$cacheItem = $this->cache->getItem($this->rateLimitCacheKey); | ||
if (!$cacheItem->isHit()) { | ||
$cacheItem->expiresAfter(1); // # of calls are cached each minute | ||
} | ||
|
||
$callsPerMinute = (int) $cacheItem->get(); | ||
if (++$callsPerMinute > $this->maxCallsPerMinute) { | ||
return true; | ||
} | ||
$cacheItem->set($callsPerMinute); | ||
$this->cache->save($cacheItem); | ||
return false; | ||
} | ||
|
||
private function getCacheItem(): CacheItemInterface | ||
{ | ||
if (\is_null($this->cacheItem)) { | ||
$this->cacheItem = $this->cache->getItem($this->cacheKey); | ||
} | ||
|
||
return $this->cacheItem; | ||
} | ||
|
||
private function setCacheKeys(): void | ||
{ | ||
if (empty($this->jwksUri)) { | ||
throw new RuntimeException('JWKS URI is empty'); | ||
} | ||
|
||
// ensure we do not have illegal characters | ||
$key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri); | ||
|
||
// add prefix | ||
$key = $this->cacheKeyPrefix . $key; | ||
|
||
// Hash keys if they exceed $maxKeyLength of 64 | ||
if (\strlen($key) > $this->maxKeyLength) { | ||
$key = substr(hash('sha256', $key), 0, $this->maxKeyLength); | ||
} | ||
|
||
$this->cacheKey = $key; | ||
|
||
if ($this->rateLimit) { | ||
// add prefix | ||
$rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key; | ||
|
||
// Hash keys if they exceed $maxKeyLength of 64 | ||
if (\strlen($rateLimitKey) > $this->maxKeyLength) { | ||
$rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength); | ||
} | ||
|
||
$this->rateLimitCacheKey = $rateLimitKey; | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back 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.
this code is currently not proof against ddos attacks via key spamming, or is it?
this scenario was described here: https://github.com/auth0/node-jwks-rsa#rate-limiting
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, it does not. We can add this to the cache without too much issue, however. I'll take a look. Thanks for pointing this out.