Skip to content
This repository has been archived by the owner on Feb 6, 2023. It is now read-only.

Commit

Permalink
Adding cache driver and depricating metadata
Browse files Browse the repository at this point in the history
I understand that these seem like drastic changes, but this maintains
existing functionality, while significantly improving performance.

*Removing metadata retrieval in JwtVerifier constructor*

The Okta documentation clearly indicates that the keys endpoint is not
dynamic (see here: https://developer.okta.com/docs/reference/api/oidc/#keys).

Retrieving the metadata every time the model is instantiated is
unnessesary network overhead, process latency, and app complexity.
There really just isn't a good reason for doing so.

In order to preserve backwards compatibility, that request process has
been moved into the `getMeaData()` function, and a `@deprecated` tag
added.

*Adding caching functionality*

Okta's own documentation strongly suggests caching the retrieved keys
(see
https://developer.okta.com/docs/reference/api/oidc/#best-practices).

In an effort to improve upon this, you can now pass an implementation of `\Psr\SimpleCache\CacheInterface`.

The ttl is hardcoded at 24 hours, though okta suggests caching for up to 90 days.

NOTE: If no implemenation is passed, an instance will be created only using an in-memory
store that is only valid for the lifecycle of the request.

*various code cleanup*

- Return types added to modified methods
- unused imports removed
- removed irrelivent checks in validation methods
- bringing in mockery library to facilitate tests
- added missing `ext-json` dependency
  • Loading branch information
clink-aaron committed Feb 28, 2022
1 parent cc12ae3 commit ec3c827
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 34 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ $jwtVerifier = (new \Okta\JwtVerifier\JwtVerifierBuilder())
->build();
```

### Caching
It's strongly suggested to cache the keys to improve performance. You can pass an implementation of `\Psr\SimpleCache\CacheInterface`
to the Adaptor constructor.

For example, in laravel:
```php
// note: named parameters are only valid for php >= 8.0
->setAdaptor(new \Okta\JwtVerifier\Adaptors\FirebasePhpJwt(request: null, leeway: 120, cache: app('cache')->store()))
```

If using symphony, you may need to use an adaptor:
https://symfony.com/doc/current/components/cache/psr6_psr16_adapters.html

## Validating an Access Token

After you have a `$jwtVerifier` from the above section and an `access_token` from a successful sign in, or
Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@
"php-http/message": "^1.8",
"php-http/discovery": "^1.9",
"php-http/curl-client": "^2.1",
"bretterer/iso_duration_converter": "^0.1.0"
"bretterer/iso_duration_converter": "^0.1.0",
"ext-json": "*",
"illuminate/cache": "^8.83.1 || ^9.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0 ",
"symfony/var-dumper": "^5.1",
"squizlabs/php_codesniffer": "^3.5",
"php-http/mock-client": "^1.4",
"guzzlehttp/psr7": "~2.0.0",
"firebase/php-jwt": "^5.2"
"firebase/php-jwt": "^5.2",
"mockery/mockery": "^1.5"
}
}
2 changes: 1 addition & 1 deletion src/Adaptors/Adaptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

interface Adaptor
{
public function getKeys($jku);
public function getKeys(string $jku);
public function decode($jwt, $keys): Jwt;
public static function isPackageAvailable();
}
27 changes: 24 additions & 3 deletions src/Adaptors/FirebasePhpJwt.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

namespace Okta\JwtVerifier\Adaptors;

use Carbon\Carbon;
use Firebase\JWT\JWT as FirebaseJWT;
use Illuminate\Cache\ArrayStore;
use Okta\JwtVerifier\Jwt;
use Okta\JwtVerifier\Request;
use UnexpectedValueException;
Expand All @@ -36,15 +38,34 @@ class FirebasePhpJwt implements Adaptor
*/
private $leeway;

public function __construct(Request $request = null, int $leeway = 120)
public function __construct(Request $request = null, int $leeway = 120, \Psr\SimpleCache\CacheInterface $cache = null)
{
$this->request = $request ?: new Request();
$this->leeway = $leeway;
$this->leeway = $leeway ?: 120;
$this->cache = $cache ?: new \Illuminate\Cache\Repository(new ArrayStore(true));
}

public function getKeys($jku)
public function clearCache(string $jku)
{
$cacheKey = 'keys-' . md5($jku);
return $this->cache->delete($cacheKey);
}

/**
* Caching the keys in accordance with best practices: https://developer.okta.com/docs/reference/api/oidc/#best-practices
*/
public function getKeys(string $jku): array
{
$cacheKey = 'keys-' . md5($jku);

$cached = $this->cache->get($cacheKey);
if($cached){
return self::parseKeySet($cached);
}

$keys = json_decode($this->request->setUrl($jku)->get()->getBody()->getContents());
$this->cache->set($cacheKey, $keys, Carbon::now()->addDay());

return self::parseKeySet($keys);
}

Expand Down
63 changes: 36 additions & 27 deletions src/JwtVerifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@

namespace Okta\JwtVerifier;

use Http\Client\Common\PluginClient;
use Http\Client\HttpClient;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\UriFactoryDiscovery;
use Carbon\Carbon;
use Okta\JwtVerifier\Adaptors\Adaptor;
use Okta\JwtVerifier\Adaptors\AutoDiscover;
use Okta\JwtVerifier\Discovery\DiscoveryMethod;
Expand Down Expand Up @@ -59,6 +55,10 @@ class JwtVerifier
*/
protected $adaptor;

protected string $jwksUri;

private Request $request;

public function __construct(
string $issuer,
DiscoveryMethod $discovery = null,
Expand All @@ -70,15 +70,25 @@ public function __construct(
$this->issuer = $issuer;
$this->discovery = $discovery ?: new Oauth;
$this->adaptor = $adaptor ?: AutoDiscover::getAdaptor();
$request = $request ?: new Request;
$this->wellknown = $this->issuer.$this->discovery->getWellKnown();
$this->metaData = json_decode($request->setUrl($this->wellknown)->get()
->getBody());
$this->request = $request ?: new Request;

$this->claimsToValidate = $claimsToValidate;

$this->jwksUri = "$issuer/v1/keys";

}

public function getIssuer()
public function clearCache(): bool
{
return $this->adaptor->clearCache($this->jwksUri);
}

public function getJwksUri(): string
{
return $this->jwksUri;
}

public function getIssuer(): string
{
return $this->issuer;
}
Expand All @@ -88,18 +98,26 @@ public function getDiscovery()
return $this->discovery;
}

/**
* @deprecated you should no longer rely on this method for client metadata
*/
public function getMetaData()
{
return $this->metaData;
$this->wellknown = $this->issuer.$this->discovery->getWellKnown();
return json_decode($this->request->setUrl($this->wellknown)->get()->getBody());
}

public function verify($jwt)
/**
* @return array|mixed
*/
public function getKeys()
{
if($this->metaData->jwks_uri == null) {
throw new \DomainException("Could not access a valid JWKS_URI from the metadata. We made a call to {$this->wellknown} endpoint, but jwks_uri was null. Please make sure you are using a custom authorization server for the jwt verifier.");
}
return $this->adaptor->getKeys($this->jwksUri);
}

$keys = $this->adaptor->getKeys($this->metaData->jwks_uri);
public function verify($jwt)
{
$keys = $this->getKeys();

$decoded = $this->adaptor->decode($jwt, $keys);

Expand All @@ -110,12 +128,7 @@ public function verify($jwt)

public function verifyIdToken($jwt)
{

if($this->metaData->jwks_uri == null) {
throw new \DomainException("Could not access a valid JWKS_URI from the metadata. We made a call to {$this->wellknown} endpoint, but jwks_uri was null. Please make sure you are using a custom authorization server for the jwt verifier.");
}

$keys = $this->adaptor->getKeys($this->metaData->jwks_uri);
$keys = $this->getKeys();

$decoded = $this->adaptor->decode($jwt, $keys);

Expand All @@ -126,11 +139,7 @@ public function verifyIdToken($jwt)

public function verifyAccessToken($jwt)
{
if($this->metaData->jwks_uri == null) {
throw new \DomainException("Could not access a valid JWKS_URI from the metadata. We made a call to {$this->wellknown} endpoint, but jwks_uri was null. Please make sure you are using a custom authorization server for the jwt verifier.");
}

$keys = $this->adaptor->getKeys($this->metaData->jwks_uri);
$keys = $this->getKeys();

$decoded = $this->adaptor->decode($jwt, $keys);

Expand Down
40 changes: 39 additions & 1 deletion tests/Unit/JwtVerifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
******************************************************************************/

use Okta\JwtVerifier\JwtVerifier;
use PHPUnit\Framework\TestCase;

class JwtVerifierTest extends BaseTestCase
{
use \Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;

/** @test */
public function can_get_issuer_off_object()
{
Expand Down Expand Up @@ -101,5 +102,42 @@ public function will_get_meta_data_when_verifier_is_constructed()

}

public function test_no_request_on_instantiation()
{
$fakeClient = Mockery::mock(Okta\JwtVerifier\Request::class);
$fakeClient->expects('setUrl->get')->never();

$verifier = new Okta\JwtVerifier\JwtVerifier('https://my.issuer.com', null, null, $fakeClient);
}

public function test_generated_keys_uri_correct()
{
$verifier = new Okta\JwtVerifier\JwtVerifier('https://my.issuer.com/oauth2/default');
['path' => $path] = parse_url($verifier->getJwksUri());
$this->assertEquals('/oauth2/default/v1/keys', $path);
}

public function test_keys_cached()
{
$fakeRequest = Mockery::mock(\Okta\JwtVerifier\Request::class);
$expected = [
'keys' => [[
'kty' => 'RSA',
'alg' => 'RS256',
'kid' => 'abc-123',
'use' => 'sig',
'e' => 'AQAB',
'n' => 'abc123'
]]
];

$fakeRequest->expects('setUrl->get->getBody->getContents')->andReturn(json_encode($expected))->times(1);
$adaptor = new \Okta\JwtVerifier\Adaptors\FirebasePhpJwt($fakeRequest);

$adaptor->getKeys('abc');
$adaptor->getKeys('abc');
$adaptor->getKeys('abc');
}


}

0 comments on commit ec3c827

Please sign in to comment.