From b35950ca03cc47d4875531d9e8fe84ac6b1fff6e Mon Sep 17 00:00:00 2001 From: Nicky De Maeyer Date: Tue, 17 Nov 2020 11:32:30 +0100 Subject: [PATCH 1/3] improved discovery classes --- .gitignore | 1 + src/Discovery/DiscoveryMethod.php | 15 ++++++--- src/Discovery/DiscoveryMethodInterface.php | 23 +++++++++++++ src/Discovery/Oauth.php | 14 +++----- src/Discovery/Oidc.php | 12 +++---- src/JwtVerifier.php | 11 +++--- src/JwtVerifierBuilder.php | 7 ++-- tests/Unit/Discovery/DiscoveryMethodTest.php | 35 ++++++++++++++++++++ tests/Unit/Discovery/OauthTest.php | 2 +- tests/Unit/Discovery/OidcTest.php | 34 +++++++++++++++++++ 10 files changed, 121 insertions(+), 33 deletions(-) create mode 100644 src/Discovery/DiscoveryMethodInterface.php create mode 100644 tests/Unit/Discovery/DiscoveryMethodTest.php create mode 100644 tests/Unit/Discovery/OidcTest.php diff --git a/.gitignore b/.gitignore index 15b433a..75349d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor/ .vscode +.idea/ diff --git a/src/Discovery/DiscoveryMethod.php b/src/Discovery/DiscoveryMethod.php index 5cd9b99..1c5e513 100644 --- a/src/Discovery/DiscoveryMethod.php +++ b/src/Discovery/DiscoveryMethod.php @@ -18,12 +18,17 @@ namespace Okta\JwtVerifier\Discovery; -abstract class DiscoveryMethod +class DiscoveryMethod implements DiscoveryMethodInterface { - protected $wellKnownUri; + protected $wellKnown; - public function getWellKnown() + public function __construct(string $wellKnown) { - return $this->wellKnownUri; + $this->wellKnown = $wellKnown; } -} \ No newline at end of file + + public function getWellKnown(): string + { + return $this->wellKnown; + } +} diff --git a/src/Discovery/DiscoveryMethodInterface.php b/src/Discovery/DiscoveryMethodInterface.php new file mode 100644 index 0000000..9edd7d5 --- /dev/null +++ b/src/Discovery/DiscoveryMethodInterface.php @@ -0,0 +1,23 @@ +wellKnownUri; + return '/.well-known/oauth-authorization-server'; } - -} \ No newline at end of file +} diff --git a/src/Discovery/Oidc.php b/src/Discovery/Oidc.php index 574d826..d853cd8 100644 --- a/src/Discovery/Oidc.php +++ b/src/Discovery/Oidc.php @@ -17,22 +17,18 @@ namespace Okta\JwtVerifier\Discovery; -use Okta\JwtVerifier\Discovery\DiscoveryMethod as Discovery; - -class Oidc extends Discovery +class Oidc implements DiscoveryMethodInterface { - protected $wellKnownUri = '/.well-known/openid-configuration'; - /** * Get the defined well-known URI. This is the URI * that is concatenated to the issuer URL. * * @return string */ - public function getWellKnownUri(): string + public function getWellKnown(): string { - return $this->wellKnownUri; + return '/.well-known/openid-configuration'; } -} \ No newline at end of file +} diff --git a/src/JwtVerifier.php b/src/JwtVerifier.php index 5c5aa9e..a4bb15a 100644 --- a/src/JwtVerifier.php +++ b/src/JwtVerifier.php @@ -24,7 +24,7 @@ use Http\Discovery\UriFactoryDiscovery; use Okta\JwtVerifier\Adaptors\Adaptor; use Okta\JwtVerifier\Adaptors\AutoDiscover; -use Okta\JwtVerifier\Discovery\DiscoveryMethod; +use Okta\JwtVerifier\Discovery\DiscoveryMethodInterface; use Okta\JwtVerifier\Discovery\Oauth; class JwtVerifier @@ -35,7 +35,7 @@ class JwtVerifier protected $issuer; /** - * @var DiscoveryMethod + * @var DiscoveryMethodInterface */ protected $discovery; @@ -61,7 +61,7 @@ class JwtVerifier public function __construct( string $issuer, - DiscoveryMethod $discovery = null, + DiscoveryMethodInterface $discovery = null, Adaptor $adaptor = null, Request $request = null, int $leeway = 120, @@ -72,8 +72,9 @@ public function __construct( $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->metaData = json_decode( + $request->setUrl($this->wellknown)->get()->getBody() + ); $this->claimsToValidate = $claimsToValidate; } diff --git a/src/JwtVerifierBuilder.php b/src/JwtVerifierBuilder.php index 1b91b1d..2e46701 100644 --- a/src/JwtVerifierBuilder.php +++ b/src/JwtVerifierBuilder.php @@ -17,9 +17,8 @@ namespace Okta\JwtVerifier; -use Okta\JwtVerifier\Discovery\Oauth; +use Okta\JwtVerifier\Discovery\DiscoveryMethodInterface; use Okta\JwtVerifier\Adaptors\Adaptor; -use Okta\JwtVerifier\Discovery\DiscoveryMethod; use Bretterer\IsoDurationConverter\DurationParser; class JwtVerifierBuilder @@ -54,10 +53,10 @@ public function setIssuer(string $issuer): self /** * Set the Discovery class. This class should be an instance of DiscoveryMethod. * - * @param DiscoveryMethod $discoveryMethod The DiscoveryMethod instance. + * @param DiscoveryMethodInterface $discoveryMethod The DiscoveryMethod instance. * @return JwtVerifierBuilder */ - public function setDiscovery(DiscoveryMethod $discoveryMethod): self + public function setDiscovery(DiscoveryMethodInterface $discoveryMethod): self { $this->discovery = $discoveryMethod; diff --git a/tests/Unit/Discovery/DiscoveryMethodTest.php b/tests/Unit/Discovery/DiscoveryMethodTest.php new file mode 100644 index 0000000..1902ac4 --- /dev/null +++ b/tests/Unit/Discovery/DiscoveryMethodTest.php @@ -0,0 +1,35 @@ +assertEquals( + 'test', + $oauth->getWellKnown(), + '.well-known endpoint is not set correctly' + ); + } + +} diff --git a/tests/Unit/Discovery/OauthTest.php b/tests/Unit/Discovery/OauthTest.php index c520837..cc719cc 100644 --- a/tests/Unit/Discovery/OauthTest.php +++ b/tests/Unit/Discovery/OauthTest.php @@ -26,7 +26,7 @@ public function sets_well_known_correctly() $this->assertEquals( '/.well-known/oauth-authorization-server', - $oauth->getWellKnownUri(), + $oauth->getWellKnown(), '.well-known endpoint is not set correctly' ); } diff --git a/tests/Unit/Discovery/OidcTest.php b/tests/Unit/Discovery/OidcTest.php new file mode 100644 index 0000000..4e1d756 --- /dev/null +++ b/tests/Unit/Discovery/OidcTest.php @@ -0,0 +1,34 @@ +assertEquals( + '/.well-known/openid-configuration', + $oauth->getWellKnown(), + '.well-known endpoint is not set correctly' + ); + } + +} From 3768fa5a553720a3301c2c727b92c1e5f1825e3a Mon Sep 17 00:00:00 2001 From: Nicky De Maeyer Date: Tue, 17 Nov 2020 13:28:50 +0100 Subject: [PATCH 2/3] improved types, classes and codestyle. added dockerfile to run tests locally --- Dockerfile | 9 ++ README.md | 12 ++- composer.json | 1 + src/Adaptors/Adaptor.php | 4 +- src/Adaptors/AutoDiscover.php | 10 +- src/Adaptors/FirebasePhpJwt.php | 43 +++++---- ...terface.php => DefaultDiscoveryMethod.php} | 14 ++- src/Discovery/DiscoveryMethod.php | 21 ++--- src/Discovery/Oauth.php | 8 +- src/Discovery/Oidc.php | 10 +- src/Jwt.php | 44 ++++++--- src/JwtVerifier.php | 92 ++++++++++--------- src/JwtVerifierBuilder.php | 88 ++++++++++++------ src/Request.php | 39 ++++---- tests/BaseTestCase.php | 6 +- ...est.php => DefaultDiscoveryMethodTest.php} | 10 +- tests/Unit/JwtVerifierBuilderTest.php | 38 ++++---- tests/Unit/JwtVerifierTest.php | 50 +++++----- tests/Unit/RequestTest.php | 28 +++--- 19 files changed, 302 insertions(+), 225 deletions(-) create mode 100644 Dockerfile rename src/Discovery/{DiscoveryMethodInterface.php => DefaultDiscoveryMethod.php} (82%) rename tests/Unit/Discovery/{DiscoveryMethodTest.php => DefaultDiscoveryMethodTest.php} (84%) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dc46f77 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM php:7.2-cli-stretch + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +COPY . /app +WORKDIR /app + +RUN composer install + diff --git a/README.md b/README.md index 420240e..211f471 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,18 @@ If you run into problems using the SDK, you can The above are the basic steps for verifying an access token locally. The steps are not tied directly to a framework so you could plug in the `okta/okta-jwt` into the framework of your choice. +## Development + +The repository contains a `Dockerfile` you can use to run the test suite locally: + +```bash +# build the docker +docker build -t okta-jwt-verifier . +# run the tests +docker run --rm --volume "$PWD":/app okta-jwt-verifier vendor/bin/phpunit +``` [devforum]: https://devforum.okta.com/ [lang-landing]: https://developer.okta.com/code/php/ [github-issues]: /okta/okta-jwt-verifier-php/issues -[github-releases]: /okta/okta-jwt-verifier-php/releases \ No newline at end of file +[github-releases]: /okta/okta-jwt-verifier-php/releases diff --git a/composer.json b/composer.json index 9960be1..77a755c 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ }, "require": { "php": "^7.2", + "ext-json": "*", "nesbot/carbon": "^2.0", "psr/http-message": "^1.0", "php-http/client-common": "^2.3", diff --git a/src/Adaptors/Adaptor.php b/src/Adaptors/Adaptor.php index 9f38eba..887db24 100644 --- a/src/Adaptors/Adaptor.php +++ b/src/Adaptors/Adaptor.php @@ -23,5 +23,5 @@ interface Adaptor { public function getKeys($jku); public function decode($jwt, $keys): Jwt; - public static function isPackageAvailable(); -} \ No newline at end of file + public static function isPackageAvailable(): bool; +} diff --git a/src/Adaptors/AutoDiscover.php b/src/Adaptors/AutoDiscover.php index 6f4b1e1..20fd695 100644 --- a/src/Adaptors/AutoDiscover.php +++ b/src/Adaptors/AutoDiscover.php @@ -17,6 +17,8 @@ namespace Okta\JwtVerifier\Adaptors; +use RuntimeException; + class AutoDiscover { private static $adaptors = [ @@ -25,15 +27,15 @@ class AutoDiscover public static function getAdaptor() { - foreach(self::$adaptors as $adaptor) { - if($adaptor::isPackageAvailable()) { + foreach (self::$adaptors as $adaptor) { + if ($adaptor instanceof Adaptor && $adaptor::isPackageAvailable()) { return new $adaptor(); } } - throw new \Exception( + throw new RuntimeException( 'Could not discover JWT Library, Please make sure one is included and the Adaptor is used' ); } -} \ No newline at end of file +} diff --git a/src/Adaptors/FirebasePhpJwt.php b/src/Adaptors/FirebasePhpJwt.php index 3ad017c..d3b187e 100644 --- a/src/Adaptors/FirebasePhpJwt.php +++ b/src/Adaptors/FirebasePhpJwt.php @@ -42,7 +42,7 @@ public function __construct(Request $request = null, int $leeway = 120) $this->leeway = $leeway; } - public function getKeys($jku) + public function getKeys($jku): array { $keys = json_decode($this->request->setUrl($jku)->get()->getBody()->getContents()); return self::parseKeySet($keys); @@ -55,7 +55,7 @@ public function decode($jwt, $keys): Jwt return (new Jwt($jwt, $decoded)); } - public static function isPackageAvailable() + public static function isPackageAvailable(): bool { return class_exists(FirebaseJWT::class); } @@ -65,27 +65,31 @@ public static function isPackageAvailable() * @param $source * @return array an associative array represents the set of keys */ - public static function parseKeySet($source) + public static function parseKeySet($source): array { $keys = []; if (is_string($source)) { $source = json_decode($source, true); } else if (is_object($source)) { - if (property_exists($source, 'keys')) + if (property_exists($source, 'keys')) { $source = (array)$source; - else + } else { $source = [$source]; + } } if (is_array($source)) { - if (isset($source['keys'])) + if (isset($source['keys'])) { $source = $source['keys']; + } foreach ($source as $k => $v) { if (!is_string($k)) { - if (is_array($v) && isset($v['kid'])) + if (is_array($v) && isset($v['kid'])) { $k = $v['kid']; - elseif (is_object($v) && property_exists($v, 'kid')) + } + elseif (is_object($v) && property_exists($v, 'kid')) { $k = $v->{'kid'}; + } } try { $v = self::parseKey($v); @@ -95,10 +99,12 @@ public static function parseKeySet($source) } } } - if (0 < count($keys)) { - return $keys; + + if (0 === count($keys)) { + throw new UnexpectedValueException('Failed to parse JWK'); } - throw new UnexpectedValueException('Failed to parse JWK'); + + return $keys; } /** @@ -108,18 +114,21 @@ public static function parseKeySet($source) */ public static function parseKey($source) { - if (!is_array($source)) + if (!is_array($source)) { $source = (array)$source; - if (!empty($source) && isset($source['kty']) && isset($source['n']) && isset($source['e'])) { + } + if (isset($source['kty'], $source['n'], $source['e'])) { switch ($source['kty']) { case 'RSA': - if (array_key_exists('d', $source)) + if (array_key_exists('d', $source)) { throw new UnexpectedValueException('Failed to parse JWK: RSA private key is not supported'); + } $pem = self::createPemFromModulusAndExponent($source['n'], $source['e']); $pKey = openssl_pkey_get_public($pem); - if ($pKey !== false) + if ($pKey !== false) { return $pKey; + } break; default: //Currently only RSA is supported @@ -138,7 +147,7 @@ public static function parseKey($source) * @param string $e the RSA exponent encoded in Base64 * @return string the RSA public key represented in PEM format */ - private static function createPemFromModulusAndExponent($n, $e) + private static function createPemFromModulusAndExponent(string $n, string $e): string { $modulus = FirebaseJWT::urlsafeB64Decode($n); $publicExponent = FirebaseJWT::urlsafeB64Decode($e); @@ -187,7 +196,7 @@ private static function createPemFromModulusAndExponent($n, $e) * @param int $length * @return string */ - private static function encodeLength($length) + private static function encodeLength(int $length): string { if ($length <= 0x7F) { return chr($length); diff --git a/src/Discovery/DiscoveryMethodInterface.php b/src/Discovery/DefaultDiscoveryMethod.php similarity index 82% rename from src/Discovery/DiscoveryMethodInterface.php rename to src/Discovery/DefaultDiscoveryMethod.php index 9edd7d5..df4c1e2 100644 --- a/src/Discovery/DiscoveryMethodInterface.php +++ b/src/Discovery/DefaultDiscoveryMethod.php @@ -17,7 +17,17 @@ namespace Okta\JwtVerifier\Discovery; -interface DiscoveryMethodInterface +class DefaultDiscoveryMethod implements DiscoveryMethod { - public function getWellKnown(): string; + protected $wellKnown; + + public function __construct(string $wellKnown) + { + $this->wellKnown = $wellKnown; + } + + public function getWellKnown(): string + { + return $this->wellKnown; + } } diff --git a/src/Discovery/DiscoveryMethod.php b/src/Discovery/DiscoveryMethod.php index 1c5e513..872dd56 100644 --- a/src/Discovery/DiscoveryMethod.php +++ b/src/Discovery/DiscoveryMethod.php @@ -17,18 +17,13 @@ namespace Okta\JwtVerifier\Discovery; - -class DiscoveryMethod implements DiscoveryMethodInterface +interface DiscoveryMethod { - protected $wellKnown; - - public function __construct(string $wellKnown) - { - $this->wellKnown = $wellKnown; - } - - public function getWellKnown(): string - { - return $this->wellKnown; - } + /** + * Get the defined well-known URI. This is the URI + * that is concatenated to the issuer URL. + * + * @return string + */ + public function getWellKnown(): string; } diff --git a/src/Discovery/Oauth.php b/src/Discovery/Oauth.php index e358fbb..53b219a 100644 --- a/src/Discovery/Oauth.php +++ b/src/Discovery/Oauth.php @@ -17,14 +17,8 @@ namespace Okta\JwtVerifier\Discovery; -class Oauth implements DiscoveryMethodInterface +class Oauth implements DiscoveryMethod { - /** - * Get the defined well-known URI. This is the URI - * that is concatenated to the issuer URL. - * - * @return string - */ public function getWellKnown(): string { return '/.well-known/oauth-authorization-server'; diff --git a/src/Discovery/Oidc.php b/src/Discovery/Oidc.php index d853cd8..d43925f 100644 --- a/src/Discovery/Oidc.php +++ b/src/Discovery/Oidc.php @@ -17,18 +17,10 @@ namespace Okta\JwtVerifier\Discovery; -class Oidc implements DiscoveryMethodInterface +class Oidc implements DiscoveryMethod { - - /** - * Get the defined well-known URI. This is the URI - * that is concatenated to the issuer URL. - * - * @return string - */ public function getWellKnown(): string { return '/.well-known/openid-configuration'; } - } diff --git a/src/Jwt.php b/src/Jwt.php index 11abf35..75bc2dd 100644 --- a/src/Jwt.php +++ b/src/Jwt.php @@ -17,8 +17,20 @@ namespace Okta\JwtVerifier; +use Carbon\Carbon; + class Jwt { + /** + * @var string + */ + private $jwt; + + /** + * @var array + */ + private $claims; + public function __construct( string $jwt, array $claims @@ -28,44 +40,48 @@ public function __construct( $this->claims = $claims; } - public function getJwt() + public function getJwt(): string { return $this->jwt; } - public function getClaims() + public function getClaims(): array { return $this->claims; } + /** + * @param bool $carbonInstance + * @return Carbon|int + */ public function getExpirationTime($carbonInstance = true) { + /** @var int $ts */ $ts = $this->toJson()->exp; - if(class_exists(\Carbon\Carbon::class) && $carbonInstance) { - return \Carbon\Carbon::createFromTimestampUTC($ts); + if ($carbonInstance && class_exists(Carbon::class)) { + return Carbon::createFromTimestampUTC($ts); } return $ts; } + /** + * @param bool $carbonInstance + * @return Carbon|int + */ public function getIssuedAt($carbonInstance = true) { + /** @var int $ts */ $ts = $this->toJson()->iat; - if(class_exists(\Carbon\Carbon::class) && $carbonInstance) { - return \Carbon\Carbon::createFromTimestampUTC($ts); + if ($carbonInstance && class_exists(Carbon::class)) { + return Carbon::createFromTimestampUTC($ts); } return $ts; } - public function toJson() + public function toJson(): object { - if(is_resource($this->claims)) { - throw new \InvalidArgumentException('Could not convert to JSON'); - } - return json_decode(json_encode($this->claims)); - } - -} \ No newline at end of file +} diff --git a/src/JwtVerifier.php b/src/JwtVerifier.php index a4bb15a..0942619 100644 --- a/src/JwtVerifier.php +++ b/src/JwtVerifier.php @@ -17,14 +17,10 @@ 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 DomainException; use Okta\JwtVerifier\Adaptors\Adaptor; use Okta\JwtVerifier\Adaptors\AutoDiscover; -use Okta\JwtVerifier\Discovery\DiscoveryMethodInterface; +use Okta\JwtVerifier\Discovery\DiscoveryMethod; use Okta\JwtVerifier\Discovery\Oauth; class JwtVerifier @@ -35,10 +31,15 @@ class JwtVerifier protected $issuer; /** - * @var DiscoveryMethodInterface + * @var DiscoveryMethod */ protected $discovery; + /** + * @var int + */ + protected $leeway; + /** * @var array */ @@ -47,7 +48,7 @@ class JwtVerifier /** * @var string */ - protected $wellknown; + protected $wellKnown; /** * @var mixed @@ -61,9 +62,9 @@ class JwtVerifier public function __construct( string $issuer, - DiscoveryMethodInterface $discovery = null, - Adaptor $adaptor = null, - Request $request = null, + ?DiscoveryMethod $discovery = null, + ?Adaptor $adaptor = null, + ?Request $request = null, int $leeway = 120, array $claimsToValidate = [] ) { @@ -71,20 +72,20 @@ public function __construct( $this->discovery = $discovery ?: new Oauth; $this->adaptor = $adaptor ?: AutoDiscover::getAdaptor(); $request = $request ?: new Request; - $this->wellknown = $this->issuer.$this->discovery->getWellKnown(); + $this->wellKnown = $this->issuer.$this->discovery->getWellKnown(); $this->metaData = json_decode( - $request->setUrl($this->wellknown)->get()->getBody() + $request->setUrl($this->wellKnown)->get()->getBody() ); - + $this->leeway = $leeway; $this->claimsToValidate = $claimsToValidate; } - public function getIssuer() + public function getIssuer(): string { return $this->issuer; } - public function getDiscovery() + public function getDiscovery(): DiscoveryMethod { return $this->discovery; } @@ -94,10 +95,10 @@ public function getMetaData() return $this->metaData; } - public function verify($jwt) + public function verify($jwt): 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."); + 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); @@ -109,46 +110,49 @@ public function verify($jwt) return $decoded; } - private function validateClaims(array $claims) + private function validateClaims(array $claims): void { $this->validateNonce($claims); $this->validateAudience($claims); $this->validateClientId($claims); } - private function validateNonce($claims) + private function validateNonce($claims): void { - if(!isset($claims['nonce']) && $this->claimsToValidate['nonce'] == null) { - return false; - } - - if($claims['nonce'] != $this->claimsToValidate['nonce']) { - throw new \Exception('Nonce does not match what is expected. Make sure to provide the nonce with - `setNonce()` from the JwtVerifierBuilder.'); + if (isset($claims['nonce']) + && $this->claimsToValidate['nonce'] !== null + && $claims['nonce'] !== $this->claimsToValidate['nonce'] + ) { + throw new DomainException( + 'Nonce does not match what is expected. ' . + 'Make sure to provide the nonce with `setNonce()` from the JwtVerifierBuilder.' + ); } } - private function validateAudience($claims) + private function validateAudience($claims): void { - if(!isset($claims['aud']) && $this->claimsToValidate['audience'] == null) { - return false; - } - - if($claims['aud'] != $this->claimsToValidate['audience']) { - throw new \Exception('Audience does not match what is expected. Make sure to provide the audience with - `setAudience()` from the JwtVerifierBuilder.'); + if (isset($claims['aud']) + && $this->claimsToValidate['audience'] !== null + && $claims['aud'] !== $this->claimsToValidate['audience'] + ) { + throw new DomainException( + 'Audience does not match what is expected. ' . + 'Make sure to provide the audience with `setAudience()` from the JwtVerifierBuilder.' + ); } } - private function validateClientId($claims) + private function validateClientId($claims): void { - if(!isset($claims['cid']) && $this->claimsToValidate['clientId'] == null) { - return false; - } - - if($claims['cid'] != $this->claimsToValidate['clientId']) { - throw new \Exception('ClientId does not match what is expected. Make sure to provide the client id with - `setClientId()` from the JwtVerifierBuilder.'); + if (isset($claims['cid']) + && $this->claimsToValidate['clientId'] !== null + && $claims['cid'] !== $this->claimsToValidate['clientId'] + ) { + throw new DomainException( + 'ClientId does not match what is expected. ' . + 'Make sure to provide the client id with `setClientId()` from the JwtVerifierBuilder.' + ); } } } diff --git a/src/JwtVerifierBuilder.php b/src/JwtVerifierBuilder.php index 2e46701..088241a 100644 --- a/src/JwtVerifierBuilder.php +++ b/src/JwtVerifierBuilder.php @@ -17,19 +17,51 @@ namespace Okta\JwtVerifier; -use Okta\JwtVerifier\Discovery\DiscoveryMethodInterface; +use InvalidArgumentException; +use Okta\JwtVerifier\Discovery\DiscoveryMethod; use Okta\JwtVerifier\Adaptors\Adaptor; use Bretterer\IsoDurationConverter\DurationParser; class JwtVerifierBuilder { + /** + * @var string|null + */ protected $issuer; + + /** + * @var DiscoveryMethod|null + */ protected $discovery; + + /** + * @var Request|null + */ protected $request; + + /** + * @var Adaptor|null + */ protected $adaptor; + + /** + * @var string|null + */ protected $audience; + + /** + * @var string|null + */ protected $clientId; + + /** + * @var string|null + */ protected $nonce; + + /** + * @var int + */ protected $leeway = 120; public function __construct(Request $request = null) @@ -53,10 +85,10 @@ public function setIssuer(string $issuer): self /** * Set the Discovery class. This class should be an instance of DiscoveryMethod. * - * @param DiscoveryMethodInterface $discoveryMethod The DiscoveryMethod instance. + * @param DiscoveryMethod $discoveryMethod The DiscoveryMethod instance. * @return JwtVerifierBuilder */ - public function setDiscovery(DiscoveryMethodInterface $discoveryMethod): self + public function setDiscovery(DiscoveryMethod $discoveryMethod): self { $this->discovery = $discoveryMethod; @@ -76,21 +108,21 @@ public function setAdaptor(Adaptor $adaptor): self return $this; } - public function setAudience($audience) + public function setAudience(string $audience): self { $this->audience = $audience; return $this; } - public function setClientId($clientId) + public function setClientId(string $clientId): self { $this->clientId = $clientId; return $this; } - public function setNonce($nonce) + public function setNonce(string $nonce): self { $this->nonce = $nonce; @@ -102,16 +134,15 @@ public function setNonce($nonce) * * @param string $leeway ISO_8601 Duration format. Default: PT2M * @return self - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function setLeeway(string $leeway = "PT2M"): self { - if(strstr($leeway, "P")) { - throw new \InvalidArgumentException("It appears that the leeway provided is not in ISO_8601 Duration Format. Please privide a duration in the format of `PT(n)S`"); + if (strpos($leeway, "P") !== false) { + throw new InvalidArgumentException("It appears that the leeway provided is not in ISO_8601 Duration Format. Please privide a duration in the format of `PT(n)S`"); } - $leeway = (new DurationParser)->parse($leeway); - $this->leeway = $leeway; + $this->leeway = (int) (new DurationParser)->parse($leeway); return $this; } @@ -119,13 +150,12 @@ public function setLeeway(string $leeway = "PT2M"): self /** * Build and return the JwtVerifier. * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * @return JwtVerifier */ public function build(): JwtVerifier { $this->validateIssuer($this->issuer); - $this->validateClientId($this->clientId); return new JwtVerifier( @@ -145,38 +175,38 @@ public function build(): JwtVerifier /** * Validate the issuer * - * @param string $issuer - * @throws \InvalidArgumentException + * @param string|null $issuer + * @throws InvalidArgumentException * @return void */ - private function validateIssuer($issuer): void { - if (null === $issuer || "" == $issuer) { - throw new \InvalidArgumentException("Your Issuer is missing. You can find your issuer from your authorization server settings in the Okta Developer Console. Find out more information aobut Authorization Servers at https://developer.okta.com/docs/guides/customize-authz-server/overview/"); + private function validateIssuer(?string $issuer): void { + if (null === $issuer || "" === $issuer) { + throw new InvalidArgumentException("Your Issuer is missing. You can find your issuer from your authorization server settings in the Okta Developer Console. Find out more information aobut Authorization Servers at https://developer.okta.com/docs/guides/customize-authz-server/overview/"); } - if (strstr($issuer, "https://") == false) { - throw new \InvalidArgumentException("Your Issuer must start with https. Current value: {$issuer}. You can copy your issuer from your authorization server settings in the Okta Developer Console. Find out more information aobut Authorization Servers at https://developer.okta.com/docs/guides/customize-authz-server/overview/"); + if (strpos($issuer, "https://") === false) { + throw new InvalidArgumentException("Your Issuer must start with https. Current value: {$issuer}. You can copy your issuer from your authorization server settings in the Okta Developer Console. Find out more information aobut Authorization Servers at https://developer.okta.com/docs/guides/customize-authz-server/overview/"); } - if (strstr($issuer, "{yourOktaDomain}") != false) { - throw new \InvalidArgumentException("Replace {yourOktaDomain} with your Okta domain. You can copy your domain from the Okta Developer Console. Follow these instructions to find it: https://bit.ly/finding-okta-domain"); + if (strpos($issuer, "{yourOktaDomain}") !== false) { + throw new InvalidArgumentException("Replace {yourOktaDomain} with your Okta domain. You can copy your domain from the Okta Developer Console. Follow these instructions to find it: https://bit.ly/finding-okta-domain"); } } /** * Validate the client id * - * @param string $cid - * @throws \InvalidArgumentException + * @param string|null $cid + * @throws InvalidArgumentException * @return void */ - private function validateClientId($cid): void { - if (null === $cid || "" == $cid) { - throw new \InvalidArgumentException("Your client ID is missing. You can copy it from the Okta Developer Console in the details for the Application you created. Follow these instructions to find it: https://bit.ly/finding-okta-app-credentials"); + private function validateClientId(?string $cid): void { + if (null === $cid || "" === $cid) { + throw new InvalidArgumentException("Your client ID is missing. You can copy it from the Okta Developer Console in the details for the Application you created. Follow these instructions to find it: https://bit.ly/finding-okta-app-credentials"); } - if (strstr($cid, "{clientId}") != false) { - throw new \InvalidArgumentException("Replace {clientId} with the client ID of your Application. You can copy it from the Okta Developer Console in the details for the Application you created. Follow these instructions to find it: https://bit.ly/finding-okta-app-credentials"); + if (strpos($cid, "{clientId}") !== false) { + throw new InvalidArgumentException("Replace {clientId} with the client ID of your Application. You can copy it from the Okta Developer Console in the details for the Application you created. Follow these instructions to find it: https://bit.ly/finding-okta-app-credentials"); } } } diff --git a/src/Request.php b/src/Request.php index 93733eb..f262911 100644 --- a/src/Request.php +++ b/src/Request.php @@ -20,18 +20,28 @@ use Http\Client\Common\PluginClient; use Http\Client\HttpClient; use Http\Discovery\HttpClientDiscovery; -use Http\Discovery\MessageFactoryDiscovery; -use Http\Discovery\UriFactoryDiscovery; -use Http\Message\MessageFactory; -use Http\Message\UriFactory; +use Http\Discovery\Psr17FactoryDiscovery; +use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\UriFactoryInterface; use Psr\Http\Message\UriInterface; class Request { + /** + * @var PluginClient + */ protected $httpClient; + + /** + * @var UriFactoryInterface + */ protected $uriFactory; - protected $messageFactory; + + /** + * @var RequestFactoryInterface + */ + protected $requestFactory; /** * The UriInterface of the request to be made. @@ -49,15 +59,14 @@ class Request public function __construct( HttpClient $httpClient = null, - UriFactory $uriFactory = null, - MessageFactory $messageFactory = null + UriFactoryInterface $uriFactory = null, + RequestFactoryInterface $messageFactory = null ) { $this->httpClient = new PluginClient( $httpClient ?: HttpClientDiscovery::find() ); - - $this->uriFactory = $uriFactory ?: UriFactoryDiscovery::find(); - $this->messageFactory = $messageFactory ?: MessageFactoryDiscovery::find(); + $this->uriFactory = $uriFactory ?: Psr17FactoryDiscovery::findUrlFactory(); + $this->requestFactory = $messageFactory ?: Psr17FactoryDiscovery::findRequestFactory(); } public function setUrl($url): Request @@ -80,18 +89,14 @@ public function get(): ResponseInterface protected function request($method): ResponseInterface { - $headers = []; - $headers['Accept'] = 'application/json'; - if (!empty($this->query)) { $this->url = $this->url->withQuery(http_build_query($this->query)); } - $request = $this->messageFactory->createRequest($method, $this->url, $headers); + $request = $this->requestFactory->createRequest($method, $this->url); + $request->withHeader('Accept', 'application/json'); return $this->httpClient->sendRequest($request); } - - -} \ No newline at end of file +} diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index e86a12a..38035ae 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -1,4 +1,5 @@ response = self::createMock('Psr\Http\Message\ResponseInterface'); + $this->response = $this->createMock(ResponseInterface::class); } -} \ No newline at end of file +} diff --git a/tests/Unit/Discovery/DiscoveryMethodTest.php b/tests/Unit/Discovery/DefaultDiscoveryMethodTest.php similarity index 84% rename from tests/Unit/Discovery/DiscoveryMethodTest.php rename to tests/Unit/Discovery/DefaultDiscoveryMethodTest.php index 1902ac4..418efbd 100644 --- a/tests/Unit/Discovery/DiscoveryMethodTest.php +++ b/tests/Unit/Discovery/DefaultDiscoveryMethodTest.php @@ -16,16 +16,16 @@ * limitations under the License. * ******************************************************************************/ -use Okta\JwtVerifier\Discovery\DiscoveryMethod; +use Okta\JwtVerifier\Discovery\DefaultDiscoveryMethod; -class DiscoveryMethodTest extends BaseTestCase +class DefaultDiscoveryMethodTest extends BaseTestCase { /** @test */ - public function sets_well_known_correctly() + public function sets_well_known_correctly(): void { - $oauth = new DiscoveryMethod('test'); + $oauth = new DefaultDiscoveryMethod('test'); - $this->assertEquals( + self::assertEquals( 'test', $oauth->getWellKnown(), '.well-known endpoint is not set correctly' diff --git a/tests/Unit/JwtVerifierBuilderTest.php b/tests/Unit/JwtVerifierBuilderTest.php index aa64502..4973065 100644 --- a/tests/Unit/JwtVerifierBuilderTest.php +++ b/tests/Unit/JwtVerifierBuilderTest.php @@ -15,15 +15,19 @@ * limitations under the License. * ******************************************************************************/ +use Http\Mock\Client; +use Okta\JwtVerifier\Adaptors\FirebasePhpJwt; +use Okta\JwtVerifier\JwtVerifier; use Okta\JwtVerifier\JwtVerifierBuilder; +use Okta\JwtVerifier\Request; class JwtVerifierBuilderTest extends BaseTestCase { /** @test */ - public function when_setting_issuer_self_is_returned() + public function when_setting_issuer_self_is_returned(): void { $verifier = new JwtVerifierBuilder(); - $this->assertInstanceOf( + self::assertInstanceOf( JwtVerifierBuilder::class, $verifier->setIssuer('https://my.issuer.com'), 'Setting the issuer does not return self.' @@ -31,10 +35,10 @@ public function when_setting_issuer_self_is_returned() } /** @test */ - public function when_setting_discovery_self_is_returned() + public function when_setting_discovery_self_is_returned(): void { $verifier = new JwtVerifierBuilder(); - $this->assertInstanceOf( + self::assertInstanceOf( JwtVerifierBuilder::class, $verifier->setDiscovery(new \Okta\JwtVerifier\Discovery\Oauth()), 'Settings discovery does not return self.' @@ -42,30 +46,30 @@ public function when_setting_discovery_self_is_returned() } /** @test */ - public function building_the_jwt_verifier_throws_exception_if_issuer_not_set() + public function building_the_jwt_verifier_throws_exception_if_issuer_not_set(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $verifier = new JwtVerifierBuilder(); $verifier->build(); } /** @test */ - public function discovery_defaults_to_oauth_when_building() + public function discovery_defaults_to_oauth_when_building(): void { $this->response ->method('getBody') ->willreturn('{"issuer": "https://example.com"}'); - $httpClient = new \Http\Mock\Client; + $httpClient = new Client; $httpClient->addResponse($this->response); - $request = new \Okta\JwtVerifier\Request($httpClient); + $request = new Request($httpClient); $verifier = new JwtVerifierBuilder($request); $verifier = $verifier->setIssuer('https://my.issuer.com')->setClientId("abc123") - ->setAdaptor(new \Okta\JwtVerifier\Adaptors\FirebasePhpJwt())->build(); + ->setAdaptor(new FirebasePhpJwt())->build(); - $this->assertInstanceOf( + self::assertInstanceOf( \Okta\JwtVerifier\Discovery\Oauth::class, $verifier->getDiscovery(), 'The builder is not defaulting to oauth2 discovery' @@ -73,23 +77,23 @@ public function discovery_defaults_to_oauth_when_building() } /** @test */ - public function building_the_verifier_returns_instance_of_jwt_verifier() + public function building_the_verifier_returns_instance_of_jwt_verifier(): void { $this->response ->method('getBody') ->willreturn('{"issuer": "https://example.com"}'); - $httpClient = new \Http\Mock\Client; + $httpClient = new Client; $httpClient->addResponse($this->response); - $request = new \Okta\JwtVerifier\Request($httpClient); + $request = new Request($httpClient); $verifier = new JwtVerifierBuilder($request); $verifier = $verifier->setIssuer('https://my.issuer.com')->setClientId("abc123") - ->setAdaptor(new \Okta\JwtVerifier\Adaptors\FirebasePhpJwt())->build(); + ->setAdaptor(new FirebasePhpJwt())->build(); - $this->assertInstanceOf( - \Okta\JwtVerifier\JwtVerifier::class, + self::assertInstanceOf( + JwtVerifier::class, $verifier, 'The verifier builder is not returning an instance of JwtVerifier' ); diff --git a/tests/Unit/JwtVerifierTest.php b/tests/Unit/JwtVerifierTest.php index aaac32d..a6d6731 100644 --- a/tests/Unit/JwtVerifierTest.php +++ b/tests/Unit/JwtVerifierTest.php @@ -15,31 +15,33 @@ * limitations under the License. * ******************************************************************************/ +use Http\Mock\Client; +use Okta\JwtVerifier\Adaptors\FirebasePhpJwt; +use Okta\JwtVerifier\Discovery\Oauth; use Okta\JwtVerifier\JwtVerifier; -use PHPUnit\Framework\TestCase; +use Okta\JwtVerifier\Request; class JwtVerifierTest extends BaseTestCase { /** @test */ - public function can_get_issuer_off_object() + public function can_get_issuer_off_object(): void { $this->response ->method('getBody') ->willreturn('{"issuer": "https://example.com"}'); - - $httpClient = new \Http\Mock\Client; + $httpClient = new Client; $httpClient->addResponse($this->response); - $request = new \Okta\JwtVerifier\Request($httpClient); + $request = new Request($httpClient); $verifier = new JwtVerifier( 'https://my.issuer.com', - new \Okta\JwtVerifier\Discovery\Oauth(), - new \Okta\JwtVerifier\Adaptors\FirebasePhpJwt(), + new Oauth(), + new FirebasePhpJwt(), $request ); - $this->assertEquals( + self::assertEquals( 'https://my.issuer.com', $verifier->getIssuer(), 'Does not return issuer correctly' @@ -47,55 +49,53 @@ public function can_get_issuer_off_object() } /** @test */ - public function can_get_discovery_off_object() + public function can_get_discovery_off_object(): void { $this->response ->method('getBody') ->willreturn('{"issuer": "https://example.com"}'); - - $httpClient = new \Http\Mock\Client; + $httpClient = new Client; $httpClient->addResponse($this->response); - $request = new \Okta\JwtVerifier\Request($httpClient); + $request = new Request($httpClient); $verifier = new JwtVerifier( 'https://my.issuer.com', - new \Okta\JwtVerifier\Discovery\Oauth(), - new \Okta\JwtVerifier\Adaptors\FirebasePhpJwt(), + new Oauth(), + new FirebasePhpJwt(), $request ); - $this->assertInstanceOf( - \Okta\JwtVerifier\Discovery\Oauth::class, + self::assertInstanceOf( + Oauth::class, $verifier->getDiscovery(), 'Does not return discovery correctly' ); } /** @test */ - public function will_get_meta_data_when_verifier_is_constructed() + public function will_get_meta_data_when_verifier_is_constructed(): void { $this->response ->method('getBody') - ->willreturn('{"issuer": "https://example.com"}'); - + ->willreturn('{"jwks_uri": "https://example.com"}'); - $httpClient = new \Http\Mock\Client; + $httpClient = new Client; $httpClient->addResponse($this->response); - $request = new \Okta\JwtVerifier\Request($httpClient); + $request = new Request($httpClient); $verifier = new JwtVerifier( 'https://my.issuer.com', - new \Okta\JwtVerifier\Discovery\Oauth(), - new \Okta\JwtVerifier\Adaptors\FirebasePhpJwt(), + new Oauth(), + new FirebasePhpJwt(), $request ); $metaData = $verifier->getMetaData(); - $this->assertEquals( + self::assertEquals( 'https://example.com', - $metaData->issuer, + $metaData->jwks_uri, 'Metadata was not accessed.' ); diff --git a/tests/Unit/RequestTest.php b/tests/Unit/RequestTest.php index 4f057d6..8b152bb 100644 --- a/tests/Unit/RequestTest.php +++ b/tests/Unit/RequestTest.php @@ -15,18 +15,18 @@ * limitations under the License. * ******************************************************************************/ +use Http\Mock\Client; use Okta\JwtVerifier\Request; -use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; class RequestTest extends BaseTestCase { /** @test */ - public function makes_request_to_correct_location() + public function makes_request_to_correct_location(): void { - $this->response->method('getStatusCode')->willReturn(200); - $httpClient = new \Http\Mock\Client; + $httpClient = new Client; $httpClient->addResponse($this->response); $request = new Request($httpClient); @@ -34,13 +34,13 @@ public function makes_request_to_correct_location() $response = $request->setUrl('http://example.com')->get(); $requests = $httpClient->getRequests(); - $this->assertInstanceOf( - \Psr\Http\Message\ResponseInterface::class, + self::assertInstanceOf( + ResponseInterface::class, $response, 'Response was not an instance of ResponseInterface.' ); - $this->assertEquals( + self::assertEquals( 'http://example.com', $requests[0]->getUri(), 'Did not make a request to the set URL' @@ -49,16 +49,16 @@ public function makes_request_to_correct_location() } /** @test */ - public function makes_request_to_correct_location_with_query() + public function makes_request_to_correct_location_with_query(): void { $this->response->method('getStatusCode')->willReturn(200); - $httpClient = new \Http\Mock\Client; + $httpClient = new Client; $httpClient->addResponse($this->response); $request = new Request($httpClient); - $response = $request + $request ->setUrl('http://example.com') ->withQuery('some','query') ->withQuery('and','another') @@ -66,13 +66,7 @@ public function makes_request_to_correct_location_with_query() $requests = $httpClient->getRequests(); - $this->assertInstanceOf( - \Psr\Http\Message\ResponseInterface::class, - $response, - 'Response was not an instance of ResponseInterface.' - ); - - $this->assertEquals( + self::assertEquals( 'http://example.com?some=query&and=another', $requests[0]->getUri(), 'Did not make a request to the set URL' From 3c06483c733eef42cce990fc71695fc5eb886bd3 Mon Sep 17 00:00:00 2001 From: Nicky De Maeyer Date: Tue, 17 Nov 2020 23:20:05 +0100 Subject: [PATCH 3/3] extracted server config from verifier, unit tested all the things --- .gitignore | 2 + Dockerfile | 5 + README.md | 6 +- composer.json | 7 +- composer.lock | 585 ++++++++++++------ src/{Adaptors => Adaptor}/Adaptor.php | 19 +- src/{Adaptors => Adaptor}/AutoDiscover.php | 8 +- src/{Adaptors => Adaptor}/FirebasePhpJwt.php | 66 +- src/Jwt.php | 39 +- src/JwtVerifier.php | 80 +-- src/JwtVerifierBuilder.php | 44 +- src/Server/DefaultServer.php | 129 ++++ .../Discovery/DefaultDiscovery.php} | 4 +- .../Discovery/Discovery.php} | 4 +- src/{ => Server}/Discovery/Oauth.php | 4 +- src/{ => Server}/Discovery/Oidc.php | 4 +- src/Server/Server.php | 27 + tests/BaseTestCase.php | 14 +- tests/Unit/Adaptor/FirebasePhpJwtTest.php | 63 ++ tests/Unit/Adaptor/Resources/jwks.json | 14 + tests/Unit/Adaptor/Resources/jwt | 1 + tests/Unit/Adaptor/Resources/public_key-0 | 9 + .../Unit/Adaptor/Resources/public_key-my-key | 9 + .../Discovery/DefaultDiscoveryMethodTest.php | 7 +- tests/Unit/Discovery/OauthTest.php | 9 +- tests/Unit/Discovery/OidcTest.php | 9 +- tests/Unit/JwtTest.php | 138 +++++ tests/Unit/JwtVerifierBuilderTest.php | 139 ++++- tests/Unit/JwtVerifierTest.php | 209 +++++-- tests/Unit/RequestTest.php | 16 + tests/Unit/Server/DefaultServerTest.php | 263 ++++++++ 31 files changed, 1454 insertions(+), 479 deletions(-) rename src/{Adaptors => Adaptor}/Adaptor.php (75%) rename src/{Adaptors => Adaptor}/AutoDiscover.php (86%) rename src/{Adaptors => Adaptor}/FirebasePhpJwt.php (78%) create mode 100644 src/Server/DefaultServer.php rename src/{Discovery/DefaultDiscoveryMethod.php => Server/Discovery/DefaultDiscovery.php} (93%) rename src/{Discovery/DiscoveryMethod.php => Server/Discovery/Discovery.php} (95%) rename src/{ => Server}/Discovery/Oauth.php (94%) rename src/{ => Server}/Discovery/Oidc.php (94%) create mode 100644 src/Server/Server.php create mode 100644 tests/Unit/Adaptor/FirebasePhpJwtTest.php create mode 100644 tests/Unit/Adaptor/Resources/jwks.json create mode 100644 tests/Unit/Adaptor/Resources/jwt create mode 100644 tests/Unit/Adaptor/Resources/public_key-0 create mode 100644 tests/Unit/Adaptor/Resources/public_key-my-key create mode 100644 tests/Unit/JwtTest.php create mode 100644 tests/Unit/Server/DefaultServerTest.php diff --git a/.gitignore b/.gitignore index 75349d5..49c88a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /vendor/ .vscode .idea/ + +.phpunit.result.cache diff --git a/Dockerfile b/Dockerfile index dc46f77..b10abab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,11 @@ FROM php:7.2-cli-stretch COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer +RUN apt-get update +RUN apt-get install -y zip git + +RUN pecl install xdebug-2.9.8 && docker-php-ext-enable xdebug + COPY . /app WORKDIR /app diff --git a/README.md b/README.md index 211f471..0097e95 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ To validate a JWT, you will need a few different items: require_once("/vendor/autoload.php"); // This should be replaced with your path to your vendor/autoload.php file $jwtVerifier = (new \Okta\JwtVerifier\JwtVerifierBuilder()) - ->setDiscovery(new \Okta\JwtVerifier\Discovery\Oauth) // This is not needed if using oauth. The other option is `new \Okta\JwtVerifier\Discovery\OIDC` - ->setAdaptor(new \Okta\JwtVerifier\Adaptors\FirebasePhpJwt) + ->setDiscovery(new \Okta\JwtVerifier\Server\Discovery\Oauth) // This is not needed if using oauth. The other option is `new \Okta\JwtVerifier\Server\Discovery\OIDC` + ->setAdaptor(new \Okta\JwtVerifier\Adaptor\FirebasePhpJwt) ->setAudience('api://default') ->setClientId('{clientId}') ->setIssuer('https://{yourOktaDomain}.com/oauth2/default') @@ -87,8 +87,6 @@ The result from the verify method is a `Jwt` object which has a few helper metho ```php dump($jwt); //Returns instance of \Okta\JwtVerifier\JWT -dump($jwt->toJson()); // Returns Claims as JSON Object - dump($jwt->getClaims()); // Returns Claims as they come from the JWT Package used dump($jwt->getIssuedAt()); // returns Carbon instance of issued at time diff --git a/composer.json b/composer.json index 77a755c..8a60405 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,11 @@ "Okta\\JwtVerifier\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Test\\": "tests/" + } + }, "require": { "php": "^7.2", "ext-json": "*", @@ -27,7 +32,7 @@ "bretterer/iso_duration_converter": "^0.1.0" }, "require-dev": { - "phpunit/phpunit": "^7.0 ", + "phpunit/phpunit": "^8.0 ", "symfony/var-dumper": "^5.1", "squizlabs/php_codesniffer": "^3.5", "php-http/mock-client": "^1.4", diff --git a/composer.lock b/composer.lock index c6e6c6b..d81f7c4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8f2dbe3cb2d5a7512588f0957f048cdd", + "content-hash": "c682fd727854e901d83364abb7adbeb6", "packages": [ { "name": "bretterer/iso_duration_converter", @@ -41,23 +41,23 @@ }, { "name": "clue/stream-filter", - "version": "v1.4.1", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/clue/php-stream-filter.git", - "reference": "5a58cc30a8bd6a4eb8f856adf61dd3e013f53f71" + "reference": "aeb7d8ea49c7963d3b581378955dbf5bc49aa320" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/php-stream-filter/zipball/5a58cc30a8bd6a4eb8f856adf61dd3e013f53f71", - "reference": "5a58cc30a8bd6a4eb8f856adf61dd3e013f53f71", + "url": "https://api.github.com/repos/clue/php-stream-filter/zipball/aeb7d8ea49c7963d3b581378955dbf5bc49aa320", + "reference": "aeb7d8ea49c7963d3b581378955dbf5bc49aa320", "shasum": "" }, "require": { "php": ">=5.3" }, "require-dev": { - "phpunit/phpunit": "^5.0 || ^4.8" + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" }, "type": "library", "autoload": { @@ -75,7 +75,7 @@ "authors": [ { "name": "Christian Lück", - "email": "christian@lueck.tv" + "email": "christian@clue.engineering" } ], "description": "A simple and modern approach to stream filtering in PHP", @@ -89,20 +89,30 @@ "stream_filter_append", "stream_filter_register" ], - "time": "2019-04-09T12:31:48+00:00" + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2020-10-02T12:38:20+00:00" }, { "name": "nesbot/carbon", - "version": "2.38.0", + "version": "2.41.5", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "d8f6a6a91d1eb9304527b040500f61923e97674b" + "reference": "c4a9caf97cfc53adfc219043bcecf42bc663acee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/d8f6a6a91d1eb9304527b040500f61923e97674b", - "reference": "d8f6a6a91d1eb9304527b040500f61923e97674b", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/c4a9caf97cfc53adfc219043bcecf42bc663acee", + "reference": "c4a9caf97cfc53adfc219043bcecf42bc663acee", "shasum": "" }, "require": { @@ -115,7 +125,7 @@ "doctrine/orm": "^2.7", "friendsofphp/php-cs-fixer": "^2.14 || ^3.0", "kylekatarnls/multi-tester": "^2.0", - "phpmd/phpmd": "^2.8", + "phpmd/phpmd": "^2.9", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^0.12.35", "phpunit/phpunit": "^7.5 || ^8.0", @@ -168,7 +178,17 @@ "datetime", "time" ], - "time": "2020-08-04T19:12:46+00:00" + "funding": [ + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2020-10-23T06:02:30+00:00" }, { "name": "php-http/client-common", @@ -243,16 +263,16 @@ }, { "name": "php-http/curl-client", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/php-http/curl-client.git", - "reference": "9e79355af46d72e10da50be20b66f74b26143441" + "reference": "f0cb9802da5c56b6553dfbef4ce5e1ee333b01de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/curl-client/zipball/9e79355af46d72e10da50be20b66f74b26143441", - "reference": "9e79355af46d72e10da50be20b66f74b26143441", + "url": "https://api.github.com/repos/php-http/curl-client/zipball/f0cb9802da5c56b6553dfbef4ce5e1ee333b01de", + "reference": "f0cb9802da5c56b6553dfbef4ce5e1ee333b01de", "shasum": "" }, "require": { @@ -272,9 +292,9 @@ }, "require-dev": { "guzzlehttp/psr7": "^1.0", + "laminas/laminas-diactoros": "^2.0", "php-http/client-integration-tests": "^2.0", - "phpunit/phpunit": "^7.5", - "zendframework/zend-diactoros": "^2.0" + "phpunit/phpunit": "^7.5" }, "type": "library", "extra": { @@ -304,20 +324,20 @@ "http", "psr-18" ], - "time": "2019-12-27T11:02:07+00:00" + "time": "2020-10-12T06:56:33+00:00" }, { "name": "php-http/discovery", - "version": "1.9.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/php-http/discovery.git", - "reference": "64a18cc891957e05d91910b3c717d6bd11fbede9" + "reference": "4366bf1bc39b663aa87459bd725501d2f1988b6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/64a18cc891957e05d91910b3c717d6bd11fbede9", - "reference": "64a18cc891957e05d91910b3c717d6bd11fbede9", + "url": "https://api.github.com/repos/php-http/discovery/zipball/4366bf1bc39b663aa87459bd725501d2f1988b6c", + "reference": "4366bf1bc39b663aa87459bd725501d2f1988b6c", "shasum": "" }, "require": { @@ -369,7 +389,7 @@ "message", "psr7" ], - "time": "2020-07-13T15:44:45+00:00" + "time": "2020-09-22T13:31:04+00:00" }, { "name": "php-http/httplug", @@ -431,21 +451,21 @@ }, { "name": "php-http/message", - "version": "1.8.0", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/php-http/message.git", - "reference": "ce8f43ac1e294b54aabf5808515c3554a19c1e1c" + "reference": "39db36d5972e9e6d00ea852b650953f928d8f10d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/ce8f43ac1e294b54aabf5808515c3554a19c1e1c", - "reference": "ce8f43ac1e294b54aabf5808515c3554a19c1e1c", + "url": "https://api.github.com/repos/php-http/message/zipball/39db36d5972e9e6d00ea852b650953f928d8f10d", + "reference": "39db36d5972e9e6d00ea852b650953f928d8f10d", "shasum": "" }, "require": { - "clue/stream-filter": "^1.4", - "php": "^7.1", + "clue/stream-filter": "^1.5", + "php": "^7.1 || ^8.0", "php-http/message-factory": "^1.0.2", "psr/http-message": "^1.0" }, @@ -453,12 +473,10 @@ "php-http/message-factory-implementation": "1.0" }, "require-dev": { - "akeneo/phpspec-skip-example-extension": "^1.0", - "coduo/phpspec-data-provider-extension": "^1.0", + "ergebnis/composer-normalize": "^2.6", "ext-zlib": "*", "guzzlehttp/psr7": "^1.0", - "henrikbjorn/phpspec-code-coverage": "^1.0", - "phpspec/phpspec": "^2.4", + "phpspec/phpspec": "^5.1 || ^6.3", "slim/slim": "^3.0", "zendframework/zend-diactoros": "^1.0" }, @@ -471,7 +489,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.10-dev" } }, "autoload": { @@ -499,7 +517,7 @@ "message", "psr-7" ], - "time": "2019-08-05T06:55:08+00:00" + "time": "2020-11-11T10:19:56+00:00" }, { "name": "php-http/message-factory", @@ -757,16 +775,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v2.1.3", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14" + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5e20b83385a77593259c9f8beb2c43cd03b2ac14", - "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", "shasum": "" }, "require": { @@ -775,7 +793,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.2-dev" }, "thanks": { "name": "symfony/contracts", @@ -803,20 +821,34 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "time": "2020-06-06T08:49:21+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.1.3", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "9ff59517938f88d90b6e65311fef08faa640f681" + "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/9ff59517938f88d90b6e65311fef08faa640f681", - "reference": "9ff59517938f88d90b6e65311fef08faa640f681", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", + "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", "shasum": "" }, "require": { @@ -825,11 +857,6 @@ "symfony/polyfill-php80": "^1.15" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\OptionsResolver\\": "" @@ -859,24 +886,38 @@ "configuration", "options" ], - "time": "2020-07-12T12:58:00+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531", + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-mbstring": "For best performance" @@ -884,7 +925,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -922,29 +963,43 @@ "portable", "shim" ], - "time": "2020-07-14T12:35:20+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de", + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de", "shasum": "" }, "require": { - "php": ">=7.0.8" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -988,20 +1043,34 @@ "portable", "shim" ], - "time": "2020-07-14T12:35:20+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/translation", - "version": "v5.1.3", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "4b9bf719f0fa5b05253c37fc7b335337ec7ec427" + "reference": "27980838fd261e04379fa91e94e81e662fe5a1b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/4b9bf719f0fa5b05253c37fc7b335337ec7ec427", - "reference": "4b9bf719f0fa5b05253c37fc7b335337ec7ec427", + "url": "https://api.github.com/repos/symfony/translation/zipball/27980838fd261e04379fa91e94e81e662fe5a1b6", + "reference": "27980838fd261e04379fa91e94e81e662fe5a1b6", "shasum": "" }, "require": { @@ -1037,11 +1106,6 @@ "symfony/yaml": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Translation\\": "" @@ -1066,20 +1130,34 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2020-06-30T17:42:22+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/translation-contracts", - "version": "v2.1.3", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "616a9773c853097607cf9dd6577d5b143ffdcd63" + "reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/616a9773c853097607cf9dd6577d5b143ffdcd63", - "reference": "616a9773c853097607cf9dd6577d5b143ffdcd63", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/e2eaa60b558f26a4b0354e1bbb25636efaaad105", + "reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105", "shasum": "" }, "require": { @@ -1091,7 +1169,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.3-dev" }, "thanks": { "name": "symfony/contracts", @@ -1127,42 +1205,51 @@ "interoperability", "standards" ], - "time": "2020-07-06T13:23:11+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-28T13:05:58+00:00" } ], "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.3.1", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", - "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", + "doctrine/coding-standard": "^8.0", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.13", - "phpstan/phpstan-phpunit": "^0.11", - "phpstan/phpstan-shim": "^0.11", - "phpunit/phpunit": "^7.0" + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" @@ -1176,7 +1263,7 @@ { "name": "Marco Pivetta", "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" + "homepage": "https://ocramius.github.io/" } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", @@ -1185,7 +1272,21 @@ "constructor", "instantiate" ], - "time": "2020-05-29T17:27:14+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-11-10T18:47:58+00:00" }, { "name": "firebase/php-jwt", @@ -1243,12 +1344,12 @@ "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "ad1de77a65b751d598ced37747bf4c17d457fbc9" + "reference": "7858757f390bbe4b3d81762a97d6e6e786bb70ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/ad1de77a65b751d598ced37747bf4c17d457fbc9", - "reference": "ad1de77a65b751d598ced37747bf4c17d457fbc9", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7858757f390bbe4b3d81762a97d6e6e786bb70ad", + "reference": "7858757f390bbe4b3d81762a97d6e6e786bb70ad", "shasum": "" }, "require": { @@ -1262,7 +1363,6 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "ergebnis/composer-normalize": "^2.0", "http-interop/http-factory-tests": "dev-master", "phpunit/phpunit": "^8.5" }, @@ -1314,20 +1414,20 @@ "uri", "url" ], - "time": "2020-05-19T19:51:52+00:00" + "time": "2020-09-21T20:15:55+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.10.1", + "version": "1.10.2", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", "shasum": "" }, "require": { @@ -1362,7 +1462,13 @@ "object", "object graph" ], - "time": "2020-06-29T13:22:24+00:00" + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-11-13T09:40:50+00:00" }, { "name": "phar-io/manifest", @@ -1580,16 +1686,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.2.1", + "version": "5.2.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44" + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d870572532cd70bc3fab58f2e23ad423c8404c44", - "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", "shasum": "" }, "require": { @@ -1628,20 +1734,20 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-08-15T11:14:08+00:00" + "time": "2020-09-03T19:13:55+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", "shasum": "" }, "require": { @@ -1673,32 +1779,32 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-06-27T10:12:23+00:00" + "time": "2020-09-17T18:55:26+00:00" }, { "name": "phpspec/prophecy", - "version": "1.11.1", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" + "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", - "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d", + "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2", - "php": "^7.2", - "phpdocumentor/reflection-docblock": "^5.0", + "php": "^7.2 || ~8.0, <8.1", + "phpdocumentor/reflection-docblock": "^5.2", "sebastian/comparator": "^3.0 || ^4.0", "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0" + "phpunit/phpunit": "^8.0 || ^9.0 <9.3" }, "type": "library", "extra": { @@ -1736,44 +1842,44 @@ "spy", "stub" ], - "time": "2020-07-08T12:44:21+00:00" + "time": "2020-09-29T09:10:42+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "6.1.4", + "version": "7.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" + "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", - "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf", + "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^7.1", - "phpunit/php-file-iterator": "^2.0", + "php": "^7.2", + "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.0", + "phpunit/php-token-stream": "^3.1.1", "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^3.1 || ^4.0", + "sebastian/environment": "^4.2.2", "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1" + "theseer/tokenizer": "^1.1.3" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "^8.2.2" }, "suggest": { - "ext-xdebug": "^2.6.0" + "ext-xdebug": "^2.7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "6.1-dev" + "dev-master": "7.0-dev" } }, "autoload": { @@ -1799,7 +1905,7 @@ "testing", "xunit" ], - "time": "2018-10-31T16:06:48+00:00" + "time": "2019-11-20T13:55:58+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1993,53 +2099,52 @@ }, { "name": "phpunit/phpunit", - "version": "7.5.20", + "version": "8.5.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9467db479d1b0487c99733bb1e7944d32deded2c" + "reference": "f5c8a5dd5e7e8d68d7562bfb48d47287d33937d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c", - "reference": "9467db479d1b0487c99733bb1e7944d32deded2c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f5c8a5dd5e7e8d68d7562bfb48d47287d33937d6", + "reference": "f5c8a5dd5e7e8d68d7562bfb48d47287d33937d6", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.1", + "doctrine/instantiator": "^1.3.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "myclabs/deep-copy": "^1.7", - "phar-io/manifest": "^1.0.2", - "phar-io/version": "^2.0", - "php": "^7.1", - "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^6.0.7", - "phpunit/php-file-iterator": "^2.0.1", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.0", + "phar-io/manifest": "^1.0.3", + "phar-io/version": "^2.0.1", + "php": "^7.2", + "phpspec/prophecy": "^1.10.3", + "phpunit/php-code-coverage": "^7.0.10", + "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^2.1", - "sebastian/comparator": "^3.0", - "sebastian/diff": "^3.0", - "sebastian/environment": "^4.0", - "sebastian/exporter": "^3.1", - "sebastian/global-state": "^2.0", + "phpunit/php-timer": "^2.1.2", + "sebastian/comparator": "^3.0.2", + "sebastian/diff": "^3.0.2", + "sebastian/environment": "^4.2.3", + "sebastian/exporter": "^3.1.2", + "sebastian/global-state": "^3.0.0", "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^2.0", + "sebastian/resource-operations": "^2.0.1", + "sebastian/type": "^1.1.3", "sebastian/version": "^2.0.1" }, - "conflict": { - "phpunit/phpunit-mock-objects": "*" - }, "require-dev": { "ext-pdo": "*" }, "suggest": { "ext-soap": "*", "ext-xdebug": "*", - "phpunit/php-invoker": "^2.0" + "phpunit/php-invoker": "^2.0.0" }, "bin": [ "phpunit" @@ -2047,7 +2152,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "7.5-dev" + "dev-master": "8.5-dev" } }, "autoload": { @@ -2073,7 +2178,17 @@ "testing", "xunit" ], - "time": "2020-01-08T08:45:45+00:00" + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-10T12:51:38+00:00" }, { "name": "ralouphie/getallheaders", @@ -2402,23 +2517,26 @@ }, { "name": "sebastian/global-state", - "version": "2.0.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.2", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "ext-dom": "*", + "phpunit/phpunit": "^8.0" }, "suggest": { "ext-uopz": "*" @@ -2426,7 +2544,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -2449,7 +2567,7 @@ "keywords": [ "global state" ], - "time": "2017-04-27T15:39:26+00:00" + "time": "2019-02-01T05:30:01+00:00" }, { "name": "sebastian/object-enumerator", @@ -2638,6 +2756,52 @@ "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "time": "2018-10-04T04:07:39+00:00" }, + { + "name": "sebastian/type", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", + "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", + "shasum": "" + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "time": "2019-07-02T08:10:15+00:00" + }, { "name": "sebastian/version", "version": "2.0.1", @@ -2683,16 +2847,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.6", + "version": "3.5.8", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", - "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", "shasum": "" }, "require": { @@ -2730,24 +2894,24 @@ "phpcs", "standards" ], - "time": "2020-08-10T04:50:15+00:00" + "time": "2020-10-23T02:01:07+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-ctype": "For best performance" @@ -2755,7 +2919,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2792,20 +2956,34 @@ "polyfill", "portable" ], - "time": "2020-07-14T12:35:20+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/var-dumper", - "version": "v5.1.3", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "2ebe1c7bb52052624d6dc1250f4abe525655d75a" + "reference": "4e13f3fcefb1fcaaa5efb5403581406f4e840b9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2ebe1c7bb52052624d6dc1250f4abe525655d75a", - "reference": "2ebe1c7bb52052624d6dc1250f4abe525655d75a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4e13f3fcefb1fcaaa5efb5403581406f4e840b9a", + "reference": "4e13f3fcefb1fcaaa5efb5403581406f4e840b9a", "shasum": "" }, "require": { @@ -2832,11 +3010,6 @@ "Resources/bin/var-dump-server" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "files": [ "Resources/functions/dump.php" @@ -2868,7 +3041,21 @@ "debug", "dump" ], - "time": "2020-06-24T13:36:18+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-27T10:11:13+00:00" }, { "name": "theseer/tokenizer", @@ -2968,7 +3155,9 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.2" + "php": "^7.2", + "ext-json": "*" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "1.1.0" } diff --git a/src/Adaptors/Adaptor.php b/src/Adaptor/Adaptor.php similarity index 75% rename from src/Adaptors/Adaptor.php rename to src/Adaptor/Adaptor.php index 887db24..c97a08a 100644 --- a/src/Adaptors/Adaptor.php +++ b/src/Adaptor/Adaptor.php @@ -15,13 +15,26 @@ * limitations under the License. * ******************************************************************************/ -namespace Okta\JwtVerifier\Adaptors; +namespace Okta\JwtVerifier\Adaptor; use Okta\JwtVerifier\Jwt; interface Adaptor { - public function getKeys($jku); - public function decode($jwt, $keys): Jwt; + /** + * Parse a set of JWK keys + * + * @param string|object|array $source + * @return array an associative array represents the set of keys + */ + public function parseKeySet($source): array; + + /** + * @param string $jwt + * @param array $keys a parsed set of keys + * @return Jwt + */ + public function decode(string $jwt, array $keys): Jwt; + public static function isPackageAvailable(): bool; } diff --git a/src/Adaptors/AutoDiscover.php b/src/Adaptor/AutoDiscover.php similarity index 86% rename from src/Adaptors/AutoDiscover.php rename to src/Adaptor/AutoDiscover.php index 20fd695..3dcd072 100644 --- a/src/Adaptors/AutoDiscover.php +++ b/src/Adaptor/AutoDiscover.php @@ -15,7 +15,7 @@ * limitations under the License. * ******************************************************************************/ -namespace Okta\JwtVerifier\Adaptors; +namespace Okta\JwtVerifier\Adaptor; use RuntimeException; @@ -28,14 +28,14 @@ class AutoDiscover public static function getAdaptor() { foreach (self::$adaptors as $adaptor) { - if ($adaptor instanceof Adaptor && $adaptor::isPackageAvailable()) { + if (is_a($adaptor, Adaptor::class, true) && $adaptor::isPackageAvailable()) { return new $adaptor(); } } throw new RuntimeException( - 'Could not discover JWT Library, - Please make sure one is included and the Adaptor is used' + 'Could not discover JWT Library, ' . + 'Please make sure one is included and the Adaptor is used' ); } } diff --git a/src/Adaptors/FirebasePhpJwt.php b/src/Adaptor/FirebasePhpJwt.php similarity index 78% rename from src/Adaptors/FirebasePhpJwt.php rename to src/Adaptor/FirebasePhpJwt.php index d3b187e..d62d53f 100644 --- a/src/Adaptors/FirebasePhpJwt.php +++ b/src/Adaptor/FirebasePhpJwt.php @@ -15,7 +15,7 @@ * limitations under the License. * ******************************************************************************/ -namespace Okta\JwtVerifier\Adaptors; +namespace Okta\JwtVerifier\Adaptor; use Firebase\JWT\JWT as FirebaseJWT; use Okta\JwtVerifier\Jwt; @@ -24,11 +24,6 @@ class FirebasePhpJwt implements Adaptor { - /** - * @var Request - */ - private $request; - /** * Leeway in seconds * @@ -36,36 +31,24 @@ class FirebasePhpJwt implements Adaptor */ private $leeway; - public function __construct(Request $request = null, int $leeway = 120) + public function __construct(int $leeway = 120) { - $this->request = $request ?: new Request(); $this->leeway = $leeway; } - public function getKeys($jku): array + public static function isPackageAvailable(): bool { - $keys = json_decode($this->request->setUrl($jku)->get()->getBody()->getContents()); - return self::parseKeySet($keys); + return class_exists(FirebaseJWT::class); } - public function decode($jwt, $keys): Jwt + public function decode(string $jwt, array $keys): Jwt { FirebaseJWT::$leeway = $this->leeway; $decoded = (array)FirebaseJWT::decode($jwt, $keys, ['RS256']); return (new Jwt($jwt, $decoded)); } - public static function isPackageAvailable(): bool - { - return class_exists(FirebaseJWT::class); - } - - /** - * Parse a set of JWK keys - * @param $source - * @return array an associative array represents the set of keys - */ - public static function parseKeySet($source): array + public function parseKeySet($source): array { $keys = []; if (is_string($source)) { @@ -92,7 +75,7 @@ public static function parseKeySet($source): array } } try { - $v = self::parseKey($v); + $v = $this->parseKey($v); $keys[$k] = $v; } catch (UnexpectedValueException $e) { //Do nothing @@ -107,32 +90,24 @@ public static function parseKeySet($source): array return $keys; } - /** - * Parse a JWK key - * @param $source - * @return resource|array an associative array represents the key - */ - public static function parseKey($source) + public function parseKey($source) { if (!is_array($source)) { $source = (array)$source; } if (isset($source['kty'], $source['n'], $source['e'])) { - switch ($source['kty']) { - case 'RSA': - if (array_key_exists('d', $source)) { - throw new UnexpectedValueException('Failed to parse JWK: RSA private key is not supported'); - } + if ($source['kty'] === 'RSA') { + if (array_key_exists('d', $source)) { + throw new UnexpectedValueException('Failed to parse JWK: RSA private key is not supported'); + } - $pem = self::createPemFromModulusAndExponent($source['n'], $source['e']); - $pKey = openssl_pkey_get_public($pem); - if ($pKey !== false) { - return $pKey; - } - break; - default: - //Currently only RSA is supported - break; + $pem = self::createPemFromModulusAndExponent($source['n'], $source['e']); + $pKey = openssl_pkey_get_public($pem); + if ($pKey !== false) { + return $pKey; + } + } else { + throw new UnexpectedValueException('Unsupported JWK, only RSA is currently supported.'); } } @@ -152,7 +127,6 @@ private static function createPemFromModulusAndExponent(string $n, string $e): s $modulus = FirebaseJWT::urlsafeB64Decode($n); $publicExponent = FirebaseJWT::urlsafeB64Decode($e); - $components = array( 'modulus' => pack('Ca*a*', 2, self::encodeLength(strlen($modulus)), $modulus), 'publicExponent' => pack('Ca*a*', 2, self::encodeLength(strlen($publicExponent)), $publicExponent) @@ -166,7 +140,6 @@ private static function createPemFromModulusAndExponent(string $n, string $e): s $components['publicExponent'] ); - // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA $RSAPublicKey = chr(0) . $RSAPublicKey; @@ -203,6 +176,7 @@ private static function encodeLength(int $length): string } $temp = ltrim(pack('N', $length), chr(0)); + return pack('Ca*', 0x80 | strlen($temp), $temp); } } diff --git a/src/Jwt.php b/src/Jwt.php index 75bc2dd..be31de1 100644 --- a/src/Jwt.php +++ b/src/Jwt.php @@ -18,31 +18,29 @@ namespace Okta\JwtVerifier; use Carbon\Carbon; +use Carbon\CarbonInterface; class Jwt { /** * @var string */ - private $jwt; + private $token; /** * @var array */ private $claims; - public function __construct( - string $jwt, - array $claims - ) + public function __construct(string $jwt, array $claims) { - $this->jwt = $jwt; + $this->token = $jwt; $this->claims = $claims; } - public function getJwt(): string + public function getToken(): string { - return $this->jwt; + return $this->token; } public function getClaims(): array @@ -52,12 +50,16 @@ public function getClaims(): array /** * @param bool $carbonInstance - * @return Carbon|int + * @return CarbonInterface|int */ public function getExpirationTime($carbonInstance = true) { - /** @var int $ts */ - $ts = $this->toJson()->exp; + if (!isset($this->claims['exp'])) { + throw new \DomainException('JWT does not contain "exp" claim'); + } + + $ts = $this->claims['exp']; + if ($carbonInstance && class_exists(Carbon::class)) { return Carbon::createFromTimestampUTC($ts); } @@ -67,21 +69,20 @@ public function getExpirationTime($carbonInstance = true) /** * @param bool $carbonInstance - * @return Carbon|int + * @return CarbonInterface|int */ public function getIssuedAt($carbonInstance = true) { - /** @var int $ts */ - $ts = $this->toJson()->iat; + if (!isset($this->claims['iat'])) { + throw new \DomainException('JWT does not contain "iat" claim'); + } + + $ts = $this->claims['iat']; + if ($carbonInstance && class_exists(Carbon::class)) { return Carbon::createFromTimestampUTC($ts); } return $ts; } - - public function toJson(): object - { - return json_decode(json_encode($this->claims)); - } } diff --git a/src/JwtVerifier.php b/src/JwtVerifier.php index 0942619..617bdfd 100644 --- a/src/JwtVerifier.php +++ b/src/JwtVerifier.php @@ -18,92 +18,56 @@ namespace Okta\JwtVerifier; use DomainException; -use Okta\JwtVerifier\Adaptors\Adaptor; -use Okta\JwtVerifier\Adaptors\AutoDiscover; -use Okta\JwtVerifier\Discovery\DiscoveryMethod; -use Okta\JwtVerifier\Discovery\Oauth; +use Okta\JwtVerifier\Adaptor\Adaptor; +use Okta\JwtVerifier\Adaptor\AutoDiscover; +use Okta\JwtVerifier\Server\Server; class JwtVerifier { - /** - * @var string - */ - protected $issuer; - - /** - * @var DiscoveryMethod - */ - protected $discovery; + public const VALIDATE_NONCE = 'nonce'; + public const VALIDATE_AUDIENCE = 'audience'; + public const VALIDATE_CLIENT_ID = 'client_id'; /** - * @var int + * @var Server */ - protected $leeway; + protected $server; /** * @var array */ protected $claimsToValidate; - /** - * @var string - */ - protected $wellKnown; - - /** - * @var mixed - */ - protected $metaData; - /** * @var Adaptor */ protected $adaptor; public function __construct( - string $issuer, - ?DiscoveryMethod $discovery = null, + Server $server, ?Adaptor $adaptor = null, - ?Request $request = null, - int $leeway = 120, array $claimsToValidate = [] ) { - $this->issuer = $issuer; - $this->discovery = $discovery ?: new Oauth; + $this->server = $server; $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->leeway = $leeway; $this->claimsToValidate = $claimsToValidate; } - public function getIssuer(): string - { - return $this->issuer; - } - - public function getDiscovery(): DiscoveryMethod + public function getServer(): Server { - return $this->discovery; + return $this->server; } - public function getMetaData() + public function getAdaptor(): Adaptor { - return $this->metaData; + return $this->adaptor; } public function verify($jwt): 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->adaptor->parseKeySet($this->server->getKeys()); - $decoded = $this->adaptor->decode($jwt, $keys); + $decoded = $this->adaptor->decode($jwt, $keys); $this->validateClaims($decoded->getClaims()); @@ -120,8 +84,8 @@ private function validateClaims(array $claims): void private function validateNonce($claims): void { if (isset($claims['nonce']) - && $this->claimsToValidate['nonce'] !== null - && $claims['nonce'] !== $this->claimsToValidate['nonce'] + && $this->claimsToValidate[self::VALIDATE_NONCE] !== null + && $claims['nonce'] !== $this->claimsToValidate[self::VALIDATE_NONCE] ) { throw new DomainException( 'Nonce does not match what is expected. ' . @@ -133,8 +97,8 @@ private function validateNonce($claims): void private function validateAudience($claims): void { if (isset($claims['aud']) - && $this->claimsToValidate['audience'] !== null - && $claims['aud'] !== $this->claimsToValidate['audience'] + && $this->claimsToValidate[self::VALIDATE_AUDIENCE] !== null + && $claims['aud'] !== $this->claimsToValidate[self::VALIDATE_AUDIENCE] ) { throw new DomainException( 'Audience does not match what is expected. ' . @@ -146,8 +110,8 @@ private function validateAudience($claims): void private function validateClientId($claims): void { if (isset($claims['cid']) - && $this->claimsToValidate['clientId'] !== null - && $claims['cid'] !== $this->claimsToValidate['clientId'] + && $this->claimsToValidate[self::VALIDATE_CLIENT_ID] !== null + && $claims['cid'] !== $this->claimsToValidate[self::VALIDATE_CLIENT_ID] ) { throw new DomainException( 'ClientId does not match what is expected. ' . diff --git a/src/JwtVerifierBuilder.php b/src/JwtVerifierBuilder.php index 088241a..b63f4ec 100644 --- a/src/JwtVerifierBuilder.php +++ b/src/JwtVerifierBuilder.php @@ -18,9 +18,9 @@ namespace Okta\JwtVerifier; use InvalidArgumentException; -use Okta\JwtVerifier\Discovery\DiscoveryMethod; -use Okta\JwtVerifier\Adaptors\Adaptor; -use Bretterer\IsoDurationConverter\DurationParser; +use Okta\JwtVerifier\Server\DefaultServer; +use Okta\JwtVerifier\Server\Discovery\Discovery; +use Okta\JwtVerifier\Adaptor\Adaptor; class JwtVerifierBuilder { @@ -30,7 +30,7 @@ class JwtVerifierBuilder protected $issuer; /** - * @var DiscoveryMethod|null + * @var Discovery|null */ protected $discovery; @@ -59,11 +59,6 @@ class JwtVerifierBuilder */ protected $nonce; - /** - * @var int - */ - protected $leeway = 120; - public function __construct(Request $request = null) { $this->request = $request; @@ -85,10 +80,10 @@ public function setIssuer(string $issuer): self /** * Set the Discovery class. This class should be an instance of DiscoveryMethod. * - * @param DiscoveryMethod $discoveryMethod The DiscoveryMethod instance. + * @param Discovery $discoveryMethod The DiscoveryMethod instance. * @return JwtVerifierBuilder */ - public function setDiscovery(DiscoveryMethod $discoveryMethod): self + public function setDiscovery(Discovery $discoveryMethod): self { $this->discovery = $discoveryMethod; @@ -129,24 +124,6 @@ public function setNonce(string $nonce): self return $this; } - /** - * Set the leeway using ISO_8601 Duration string. ie: PT2M - * - * @param string $leeway ISO_8601 Duration format. Default: PT2M - * @return self - * @throws InvalidArgumentException - */ - public function setLeeway(string $leeway = "PT2M"): self - { - if (strpos($leeway, "P") !== false) { - throw new InvalidArgumentException("It appears that the leeway provided is not in ISO_8601 Duration Format. Please privide a duration in the format of `PT(n)S`"); - } - - $this->leeway = (int) (new DurationParser)->parse($leeway); - - return $this; - } - /** * Build and return the JwtVerifier. * @@ -159,11 +136,12 @@ public function build(): JwtVerifier $this->validateClientId($this->clientId); return new JwtVerifier( - $this->issuer, - $this->discovery, + new DefaultServer( + $this->issuer, + $this->discovery, + $this->request + ), $this->adaptor, - $this->request, - $this->leeway, [ 'nonce' => $this->nonce, 'audience' => $this->audience, diff --git a/src/Server/DefaultServer.php b/src/Server/DefaultServer.php new file mode 100644 index 0000000..cef9b6f --- /dev/null +++ b/src/Server/DefaultServer.php @@ -0,0 +1,129 @@ +issuer = $issuer; + $this->discovery = $discovery ?: new Oauth; + $this->request = $request ?: new Request; + $this->wellKnown = $this->issuer . $this->discovery->getWellKnown(); + } + + public function getIssuer(): string + { + return $this->issuer; + } + + public function getDiscovery(): Discovery + { + return $this->discovery; + } + + public function getMetaData(): object + { + if ($this->metaData === null) { + $this->metaData = $this->loadMetaData($this->request, $this->wellKnown); + } + + return $this->metaData; + } + + private function loadMetaData(Request $request, string $wellKnown): object + { + $metaDataJson = $request->setUrl($wellKnown)->get()->getBody()->getContents(); + + if (!is_string($metaDataJson)) { + throw new RuntimeException( + "We made a call to {$wellKnown} endpoint, but did not receive json." + ); + } + + $metaData = json_decode($metaDataJson); + + if (json_last_error() !== JSON_ERROR_NONE + || !is_object($metaData) + || ! $metaData instanceof \stdClass + ) { + throw new RuntimeException( + "We made a call to {$wellKnown} endpoint, but did not receive a valid json object." + ); + } + + return $metaData; + } + + public function getKeys() + { + if ($this->keys === null) { + $metaData = $this->getMetaData(); + + if (!isset($metaData->jwks_uri) || $metaData->jwks_uri === null) { + throw new RuntimeException( + "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." + ); + } + + $this->keys = $this->request->setUrl($metaData->jwks_uri)->get()->getBody()->getContents(); + } + + return $this->keys; + } +} diff --git a/src/Discovery/DefaultDiscoveryMethod.php b/src/Server/Discovery/DefaultDiscovery.php similarity index 93% rename from src/Discovery/DefaultDiscoveryMethod.php rename to src/Server/Discovery/DefaultDiscovery.php index df4c1e2..b86e5c2 100644 --- a/src/Discovery/DefaultDiscoveryMethod.php +++ b/src/Server/Discovery/DefaultDiscovery.php @@ -15,9 +15,9 @@ * limitations under the License. * ******************************************************************************/ -namespace Okta\JwtVerifier\Discovery; +namespace Okta\JwtVerifier\Server\Discovery; -class DefaultDiscoveryMethod implements DiscoveryMethod +class DefaultDiscovery implements Discovery { protected $wellKnown; diff --git a/src/Discovery/DiscoveryMethod.php b/src/Server/Discovery/Discovery.php similarity index 95% rename from src/Discovery/DiscoveryMethod.php rename to src/Server/Discovery/Discovery.php index 872dd56..6375831 100644 --- a/src/Discovery/DiscoveryMethod.php +++ b/src/Server/Discovery/Discovery.php @@ -15,9 +15,9 @@ * limitations under the License. * ******************************************************************************/ -namespace Okta\JwtVerifier\Discovery; +namespace Okta\JwtVerifier\Server\Discovery; -interface DiscoveryMethod +interface Discovery { /** * Get the defined well-known URI. This is the URI diff --git a/src/Discovery/Oauth.php b/src/Server/Discovery/Oauth.php similarity index 94% rename from src/Discovery/Oauth.php rename to src/Server/Discovery/Oauth.php index 53b219a..a17f82c 100644 --- a/src/Discovery/Oauth.php +++ b/src/Server/Discovery/Oauth.php @@ -15,9 +15,9 @@ * limitations under the License. * ******************************************************************************/ -namespace Okta\JwtVerifier\Discovery; +namespace Okta\JwtVerifier\Server\Discovery; -class Oauth implements DiscoveryMethod +class Oauth implements Discovery { public function getWellKnown(): string { diff --git a/src/Discovery/Oidc.php b/src/Server/Discovery/Oidc.php similarity index 94% rename from src/Discovery/Oidc.php rename to src/Server/Discovery/Oidc.php index d43925f..bf98b6a 100644 --- a/src/Discovery/Oidc.php +++ b/src/Server/Discovery/Oidc.php @@ -15,9 +15,9 @@ * limitations under the License. * ******************************************************************************/ -namespace Okta\JwtVerifier\Discovery; +namespace Okta\JwtVerifier\Server\Discovery; -class Oidc implements DiscoveryMethod +class Oidc implements Discovery { public function getWellKnown(): string { diff --git a/src/Server/Server.php b/src/Server/Server.php new file mode 100644 index 0000000..9ad3933 --- /dev/null +++ b/src/Server/Server.php @@ -0,0 +1,27 @@ +response = $this->createMock(ResponseInterface::class); - } } diff --git a/tests/Unit/Adaptor/FirebasePhpJwtTest.php b/tests/Unit/Adaptor/FirebasePhpJwtTest.php new file mode 100644 index 0000000..1d50de4 --- /dev/null +++ b/tests/Unit/Adaptor/FirebasePhpJwtTest.php @@ -0,0 +1,63 @@ +adaptor = new FirebasePhpJwt(); + } + + /** + * @test + */ + public function can_parse_key_set(): void + { + $jwks = file_get_contents(__DIR__ . '/Resources/jwks.json'); + + $parsed = $this->adaptor->parseKeySet($jwks); + self::assertArrayHasKey(0, $parsed); + self::assertArrayHasKey('my-key', $parsed); + + $key0 = openssl_pkey_get_details($parsed[0]); + self::assertSame($key0['key'], file_get_contents(__DIR__ . '/Resources/public_key-0')); + + $myKey = openssl_pkey_get_details($parsed['my-key']); + self::assertSame($myKey['key'], file_get_contents(__DIR__ . '/Resources/public_key-my-key')); + } + + /** + * @test + */ + public function can_decode(): void + { + $jwks = file_get_contents(__DIR__ . '/Resources/jwks.json'); + $keys = $this->adaptor->parseKeySet($jwks); + $jwt = $this->adaptor->decode( + file_get_contents(__DIR__ . '/Resources/jwt'), + $keys + ); + + self::assertEquals( + [ + 'extra' => 'data', + 'iat' => 1577836800, + 'exp' => 4102444800 + ], + $jwt->getClaims() + ); + } +} diff --git a/tests/Unit/Adaptor/Resources/jwks.json b/tests/Unit/Adaptor/Resources/jwks.json new file mode 100644 index 0000000..29ffdee --- /dev/null +++ b/tests/Unit/Adaptor/Resources/jwks.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "n": "ugdjUmDYs7zdHFBN11um5WrqYC6KHvHC3cqEVcdtD2WwIDm4vr_tfsjb6sYC7Ez0KqAllCXE20K5-b0GRB6hK4oexP4VCV8QFx3GfnQuubq6c9gznklnb93HG-WfMKdRD5ugXVz0KFN4IDhLZZNsRMUgCk_BZGvyGEGwAbcrO6zzj2iCxOnJQ9gyaqNT6W91z2ICU4jpVDtDhS2o8Q7PSqf0HOaVPrzDejRhFPWhzdmIa_20tAYTY6VlNji3AUsGIUxp5FcEX7y9G3HH1jhfZobYeBnPd1wSAgOvX1T-NQ2eOTgqtOZLMa0XS32v_wuhNwy1UFEYr5PsoN-qPuskNw" + }, + { + "kid": "my-key", + "kty": "RSA", + "e": "AQAB", + "n": "8LRbj-_JN-6gGFLCTMjKlKg63NpwgsZBtP4NM9o2B3QeSAKJY_kF4_JgnjLKm-gurweptZXjgyikDeIjBb9WCXsaIcDFqa4m0DND3m0XBfFBQSV1LfQ09YrTmnM_24SXt_FyZMhkOQIq0VVx2yR2FihAVY_PgYU6u8E6WoCzRpQy64EIrCDYCkMH5TuM0vL9dTXzojGtG2MctI9oj_1-fbupYA6L1665i_e1APrWA1HdsdOHeLvwkJ1h1hSpbYABfuUVjlhEefDbS8tWElU6Xhlx3iYD8NLuX3BJWtu-3EKSuXQ4Znj_iId712OP_birhnbNU7aB2LG6PvMX3GbVMw" + } ] +} diff --git a/tests/Unit/Adaptor/Resources/jwt b/tests/Unit/Adaptor/Resources/jwt new file mode 100644 index 0000000..18f20c1 --- /dev/null +++ b/tests/Unit/Adaptor/Resources/jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im15LWtleSJ9.eyJleHRyYSI6ImRhdGEiLCJpYXQiOjE1Nzc4MzY4MDAsImV4cCI6NDEwMjQ0NDgwMH0.OT5CtROYZ57Uay5N25D2iCZUO75kiGlsB7i07IwGuSdEK7XgfvFCJfltEjs6NIajwOYuQjjz8rorbuXBPfQWQZHpEXllxf0D2ykbQpEy-NlTEUpNcUyDqL9hfOnevojvIgJeIjN5dHz06iqsZ_gMVvHGoj4mSV3mLyQZKYBjHsSxeyn_eowDewzLO3O8QcqUa6jDsd0wjfSuRYDl_NvXgoHB8YvXFbkiVZkB03_ggxYQH04V5020Ub338P7dFYMP6asi45_uqXPTZUC70NjJSQVHByNvCmYOgQVYZetl0tGrJwRsXskje5tO5Rhs3-HIACFgKEuTbc6zQ7x87aSScQ diff --git a/tests/Unit/Adaptor/Resources/public_key-0 b/tests/Unit/Adaptor/Resources/public_key-0 new file mode 100644 index 0000000..f96bfa2 --- /dev/null +++ b/tests/Unit/Adaptor/Resources/public_key-0 @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAugdjUmDYs7zdHFBN11um +5WrqYC6KHvHC3cqEVcdtD2WwIDm4vr/tfsjb6sYC7Ez0KqAllCXE20K5+b0GRB6h +K4oexP4VCV8QFx3GfnQuubq6c9gznklnb93HG+WfMKdRD5ugXVz0KFN4IDhLZZNs +RMUgCk/BZGvyGEGwAbcrO6zzj2iCxOnJQ9gyaqNT6W91z2ICU4jpVDtDhS2o8Q7P +Sqf0HOaVPrzDejRhFPWhzdmIa/20tAYTY6VlNji3AUsGIUxp5FcEX7y9G3HH1jhf +ZobYeBnPd1wSAgOvX1T+NQ2eOTgqtOZLMa0XS32v/wuhNwy1UFEYr5PsoN+qPusk +NwIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/Unit/Adaptor/Resources/public_key-my-key b/tests/Unit/Adaptor/Resources/public_key-my-key new file mode 100644 index 0000000..9ef007b --- /dev/null +++ b/tests/Unit/Adaptor/Resources/public_key-my-key @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8LRbj+/JN+6gGFLCTMjK +lKg63NpwgsZBtP4NM9o2B3QeSAKJY/kF4/JgnjLKm+gurweptZXjgyikDeIjBb9W +CXsaIcDFqa4m0DND3m0XBfFBQSV1LfQ09YrTmnM/24SXt/FyZMhkOQIq0VVx2yR2 +FihAVY/PgYU6u8E6WoCzRpQy64EIrCDYCkMH5TuM0vL9dTXzojGtG2MctI9oj/1+ +fbupYA6L1665i/e1APrWA1HdsdOHeLvwkJ1h1hSpbYABfuUVjlhEefDbS8tWElU6 +Xhlx3iYD8NLuX3BJWtu+3EKSuXQ4Znj/iId712OP/birhnbNU7aB2LG6PvMX3GbV +MwIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/Unit/Discovery/DefaultDiscoveryMethodTest.php b/tests/Unit/Discovery/DefaultDiscoveryMethodTest.php index 418efbd..5323407 100644 --- a/tests/Unit/Discovery/DefaultDiscoveryMethodTest.php +++ b/tests/Unit/Discovery/DefaultDiscoveryMethodTest.php @@ -16,14 +16,17 @@ * limitations under the License. * ******************************************************************************/ -use Okta\JwtVerifier\Discovery\DefaultDiscoveryMethod; +namespace Test\Unit\Discovery; + +use Okta\JwtVerifier\Server\Discovery\DefaultDiscovery; +use Test\BaseTestCase; class DefaultDiscoveryMethodTest extends BaseTestCase { /** @test */ public function sets_well_known_correctly(): void { - $oauth = new DefaultDiscoveryMethod('test'); + $oauth = new DefaultDiscovery('test'); self::assertEquals( 'test', diff --git a/tests/Unit/Discovery/OauthTest.php b/tests/Unit/Discovery/OauthTest.php index cc719cc..2a9196e 100644 --- a/tests/Unit/Discovery/OauthTest.php +++ b/tests/Unit/Discovery/OauthTest.php @@ -15,16 +15,19 @@ * limitations under the License. * ******************************************************************************/ -use Okta\JwtVerifier\Discovery\Oauth; +namespace Test\Unit\Discovery; + +use Okta\JwtVerifier\Server\Discovery\Oauth; +use Test\BaseTestCase; class OauthTest extends BaseTestCase { /** @test */ - public function sets_well_known_correctly() + public function sets_well_known_correctly(): void { $oauth = new Oauth(); - $this->assertEquals( + self::assertEquals( '/.well-known/oauth-authorization-server', $oauth->getWellKnown(), '.well-known endpoint is not set correctly' diff --git a/tests/Unit/Discovery/OidcTest.php b/tests/Unit/Discovery/OidcTest.php index 4e1d756..b742642 100644 --- a/tests/Unit/Discovery/OidcTest.php +++ b/tests/Unit/Discovery/OidcTest.php @@ -15,16 +15,19 @@ * limitations under the License. * ******************************************************************************/ -use Okta\JwtVerifier\Discovery\Oidc; +namespace Test\Unit\Discovery; + +use Okta\JwtVerifier\Server\Discovery\Oidc; +use Test\BaseTestCase; class OidcTest extends BaseTestCase { /** @test */ - public function sets_well_known_correctly() + public function sets_well_known_correctly(): void { $oauth = new Oidc(); - $this->assertEquals( + self::assertEquals( '/.well-known/openid-configuration', $oauth->getWellKnown(), '.well-known endpoint is not set correctly' diff --git a/tests/Unit/JwtTest.php b/tests/Unit/JwtTest.php new file mode 100644 index 0000000..34fa4b4 --- /dev/null +++ b/tests/Unit/JwtTest.php @@ -0,0 +1,138 @@ +getToken(), + 'Does not return token correctly' + ); + } + + /** @test */ + public function can_get_claims_from_constructor(): void + { + $jwt = new Jwt('', ['my-claim' => 'content']); + + self::assertSame( + ['my-claim' => 'content'], + $jwt->getClaims(), + 'Does not return claims correctly' + ); + } + + /** @test */ + public function can_get_raw_expiration_time(): void + { + $jwt = new Jwt('', [ + 'exp' => 4102444800 + ]); + + self::assertSame( + 4102444800, + $jwt->getExpirationTime(false), + 'Does not return expiration timestamp correctly' + ); + } + + /** @test */ + public function can_get_carbon_expiration_time(): void + { + $jwt = new Jwt('', [ + 'exp' => 4102444800 + ]); + + self::assertInstanceOf( + CarbonInterface::class, + $jwt->getExpirationTime() + ); + + self::assertSame( + '2100-01-01 00:00:00', + $jwt->getExpirationTime()->format(CarbonInterface::DEFAULT_TO_STRING_FORMAT), + 'Does not return expiration timestamp correctly' + ); + } + + /** @test */ + public function throws_exception_when_exp_claim_not_present(): void + { + $jwt = new Jwt('', []); + + $this->expectException(DomainException::class); + $this->expectExceptionMessage('JWT does not contain "exp" claim'); + + $jwt->getExpirationTime(); + } + + /** @test */ + public function can_get_raw_issued_at_time(): void + { + $jwt = new Jwt('', [ + 'iat' => 1577836800 + ]); + + self::assertSame( + 1577836800, + $jwt->getIssuedAt(false), + 'Does not return expiration timestamp correctly' + ); + } + + /** @test */ + public function can_get_carbon_issued_at_time(): void + { + $jwt = new Jwt('', [ + 'exp' => 1577836800 + ]); + + self::assertInstanceOf( + CarbonInterface::class, + $jwt->getExpirationTime() + ); + + self::assertSame( + '2020-01-01 00:00:00', + $jwt->getExpirationTime()->format(CarbonInterface::DEFAULT_TO_STRING_FORMAT), + 'Does not return expiration timestamp correctly' + ); + } + + /** @test */ + public function throws_exception_when_iat_claim_not_present(): void + { + $jwt = new Jwt('', []); + + $this->expectException(DomainException::class); + $this->expectExceptionMessage('JWT does not contain "iat" claim'); + + $jwt->getIssuedAt(); + } +} diff --git a/tests/Unit/JwtVerifierBuilderTest.php b/tests/Unit/JwtVerifierBuilderTest.php index 4973065..06d8f93 100644 --- a/tests/Unit/JwtVerifierBuilderTest.php +++ b/tests/Unit/JwtVerifierBuilderTest.php @@ -15,11 +15,14 @@ * limitations under the License. * ******************************************************************************/ -use Http\Mock\Client; -use Okta\JwtVerifier\Adaptors\FirebasePhpJwt; +namespace Test\Unit; + +use InvalidArgumentException; +use Okta\JwtVerifier\Adaptor\Adaptor; +use Okta\JwtVerifier\Server\Discovery\Oauth; use Okta\JwtVerifier\JwtVerifier; use Okta\JwtVerifier\JwtVerifierBuilder; -use Okta\JwtVerifier\Request; +use Test\BaseTestCase; class JwtVerifierBuilderTest extends BaseTestCase { @@ -29,7 +32,7 @@ public function when_setting_issuer_self_is_returned(): void $verifier = new JwtVerifierBuilder(); self::assertInstanceOf( JwtVerifierBuilder::class, - $verifier->setIssuer('https://my.issuer.com'), + $verifier->setIssuer('issuer'), 'Setting the issuer does not return self.' ); } @@ -40,57 +43,113 @@ public function when_setting_discovery_self_is_returned(): void $verifier = new JwtVerifierBuilder(); self::assertInstanceOf( JwtVerifierBuilder::class, - $verifier->setDiscovery(new \Okta\JwtVerifier\Discovery\Oauth()), + $verifier->setDiscovery($this->createMock(Oauth::class)), 'Settings discovery does not return self.' ); } /** @test */ - public function building_the_jwt_verifier_throws_exception_if_issuer_not_set(): void + public function when_setting_adaptor_self_is_returned(): void { - $this->expectException(InvalidArgumentException::class); $verifier = new JwtVerifierBuilder(); - $verifier->build(); + self::assertInstanceOf( + JwtVerifierBuilder::class, + $verifier->setAdaptor($this->createMock(Adaptor::class)), + 'Settings adaptor does not return self.' + ); } /** @test */ - public function discovery_defaults_to_oauth_when_building(): void + public function when_setting_audience_self_is_returned(): void { - $this->response - ->method('getBody') - ->willreturn('{"issuer": "https://example.com"}'); - - - $httpClient = new Client; - $httpClient->addResponse($this->response); - $request = new Request($httpClient); + $verifier = new JwtVerifierBuilder(); + self::assertInstanceOf( + JwtVerifierBuilder::class, + $verifier->setAudience('test'), + 'Settings audience does not return self.' + ); + } - $verifier = new JwtVerifierBuilder($request); - $verifier = $verifier->setIssuer('https://my.issuer.com')->setClientId("abc123") - ->setAdaptor(new FirebasePhpJwt())->build(); + /** @test */ + public function when_setting_client_id_self_is_returned(): void + { + $verifier = new JwtVerifierBuilder(); + self::assertInstanceOf( + JwtVerifierBuilder::class, + $verifier->setClientId('test'), + 'Settings client id does not return self.' + ); + } + /** @test */ + public function when_setting_nonce_id_self_is_returned(): void + { + $verifier = new JwtVerifierBuilder(); self::assertInstanceOf( - \Okta\JwtVerifier\Discovery\Oauth::class, - $verifier->getDiscovery(), - 'The builder is not defaulting to oauth2 discovery' + JwtVerifierBuilder::class, + $verifier->setNonce('test'), + 'Settings nonce does not return self.' ); } /** @test */ - public function building_the_verifier_returns_instance_of_jwt_verifier(): void + public function build_throws_exception_if_issuer_not_set(): void { - $this->response - ->method('getBody') - ->willreturn('{"issuer": "https://example.com"}'); + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessageMatches('~^Your Issuer is missing~'); + $verifier = new JwtVerifierBuilder(); + $verifier->build(); + } + /** @test */ + public function build_throws_exception_if_issuer_does_not_start_with_https(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessageMatches('~^Your Issuer must start with https~'); + $verifier = new JwtVerifierBuilder(); + $verifier->setIssuer('http://test'); + $verifier->build(); + } - $httpClient = new Client; - $httpClient->addResponse($this->response); - $request = new Request($httpClient); + /** @test */ + public function build_throws_exception_if_issuer_contains_replacement(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessageMatches('~^Replace \{yourOktaDomain\} with your Okta domain~'); + $verifier = new JwtVerifierBuilder(); + $verifier->setIssuer('https://{yourOktaDomain}/'); + $verifier->build(); + } - $verifier = new JwtVerifierBuilder($request); - $verifier = $verifier->setIssuer('https://my.issuer.com')->setClientId("abc123") - ->setAdaptor(new FirebasePhpJwt())->build(); + /** @test */ + public function build_throws_exception_if_client_id_not_set(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessageMatches('~^Your client ID is missing~'); + $verifier = new JwtVerifierBuilder(); + $verifier->setIssuer('https://issuer/'); + $verifier->build(); + } + + /** @test */ + public function build_throws_exception_if_client_id_contains_replacement(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessageMatches('~^Replace \{clientId\} with the client ID of your Application~'); + $verifier = new JwtVerifierBuilder(); + $verifier->setIssuer('https://issuer/'); + $verifier->setClientId('this is my {clientId}'); + $verifier->build(); + } + + /** @test */ + public function building_the_verifier_returns_instance_of_jwt_verifier(): void + { + $builder = new JwtVerifierBuilder(); + $verifier = $builder + ->setIssuer('https://my.issuer.com') + ->setClientId("abc123") + ->build(); self::assertInstanceOf( JwtVerifier::class, @@ -99,5 +158,19 @@ public function building_the_verifier_returns_instance_of_jwt_verifier(): void ); } + /** @test */ + public function discovery_defaults_to_oauth_when_building(): void + { + $builder = new JwtVerifierBuilder(); + $verifier = $builder + ->setIssuer('https://my.issuer.com') + ->setClientId("abc123") + ->build(); + self::assertInstanceOf( + Oauth::class, + $verifier->getServer()->getDiscovery(), + 'The builder is not defaulting to oauth2 discovery' + ); + } } diff --git a/tests/Unit/JwtVerifierTest.php b/tests/Unit/JwtVerifierTest.php index a6d6731..a37344f 100644 --- a/tests/Unit/JwtVerifierTest.php +++ b/tests/Unit/JwtVerifierTest.php @@ -15,91 +15,196 @@ * limitations under the License. * ******************************************************************************/ +namespace Test\Unit; + +use DomainException; use Http\Mock\Client; -use Okta\JwtVerifier\Adaptors\FirebasePhpJwt; -use Okta\JwtVerifier\Discovery\Oauth; +use Okta\JwtVerifier\Adaptor\Adaptor; +use Okta\JwtVerifier\Adaptor\FirebasePhpJwt; +use Okta\JwtVerifier\Jwt; +use Okta\JwtVerifier\Server\Discovery\Discovery; use Okta\JwtVerifier\JwtVerifier; use Okta\JwtVerifier\Request; +use Okta\JwtVerifier\Server\Server; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Http\Message\ResponseInterface; +use Test\BaseTestCase; class JwtVerifierTest extends BaseTestCase { + /** + * @var MockObject|Server + */ + protected $server; + + /** + * @var MockObject|Request + */ + protected $request; + + /** + * @var MockObject|ResponseInterface + */ + protected $response; + + /** + * @var MockObject|Discovery + */ + protected $discovery; + + /** + * @var MockObject|Adaptor + */ + protected $adaptor; + + public function setUp(): void + { + parent::setUp(); + + $this->server = $this->createMock(Server::class); + $this->request = $this->createMock(Request::class); + $this->response = $this->createMock(ResponseInterface::class); + $this->discovery = $this->createMock(Discovery::class); + $this->adaptor = $this->createMock(Adaptor::class); + } + /** @test */ - public function can_get_issuer_off_object(): void + public function can_get_server_from_constructor(): void { - $this->response - ->method('getBody') - ->willreturn('{"issuer": "https://example.com"}'); + $verifier = new JwtVerifier($this->server); - $httpClient = new Client; - $httpClient->addResponse($this->response); - $request = new Request($httpClient); + self::assertSame( + $this->server, + $verifier->getServer(), + 'Does not return server correctly' + ); + } + /** @test */ + public function can_get_adaptor_from_constructor(): void + { $verifier = new JwtVerifier( - 'https://my.issuer.com', - new Oauth(), - new FirebasePhpJwt(), - $request + $this->server, + $this->adaptor ); - self::assertEquals( - 'https://my.issuer.com', - $verifier->getIssuer(), - 'Does not return issuer correctly' + self::assertSame( + $this->adaptor, + $verifier->getAdaptor(), + 'Does not return adaptor correctly' ); } /** @test */ - public function can_get_discovery_off_object(): void + public function defaults_to_firebase_adaptor(): void { - $this->response - ->method('getBody') - ->willreturn('{"issuer": "https://example.com"}'); - - $httpClient = new Client; - $httpClient->addResponse($this->response); - $request = new Request($httpClient); - - $verifier = new JwtVerifier( - 'https://my.issuer.com', - new Oauth(), - new FirebasePhpJwt(), - $request - ); + $verifier = new JwtVerifier($this->server); self::assertInstanceOf( - Oauth::class, - $verifier->getDiscovery(), - 'Does not return discovery correctly' + FirebasePhpJwt::class, + $verifier->getAdaptor(), + 'Does not default to Firebase adaptor' ); } + protected function configureJwt(array $claims): Jwt + { + $this->server + ->expects(self::once()) + ->method('getKeys') + ->willReturn('my keys'); + + $this->adaptor + ->expects(self::once()) + ->method('parseKeySet') + ->with('my keys') + ->willReturn(['my keys']); + + $jwt = $this->createMock(Jwt::class); + + $this->adaptor + ->expects(self::once()) + ->method('decode') + ->with('my token', ['my keys']) + ->willReturn($jwt); + + $jwt->expects(self::once())->method('getClaims')->willReturn($claims); + + return $jwt; + } + /** @test */ - public function will_get_meta_data_when_verifier_is_constructed(): void + public function can_verify_empty_token_if_no_claims_registered(): void { - $this->response - ->method('getBody') - ->willreturn('{"jwks_uri": "https://example.com"}'); + $jwt = $this->configureJwt([]); - $httpClient = new Client; - $httpClient->addResponse($this->response); - $request = new Request($httpClient); + $verifier = new JwtVerifier($this->server, $this->adaptor); - $verifier = new JwtVerifier( - 'https://my.issuer.com', - new Oauth(), - new FirebasePhpJwt(), - $request + self::assertSame( + $jwt, + $verifier->verify('my token') ); + } + + /** @test */ + public function can_verify_empty_token_with_claims_registered(): void + { + $jwt = $this->configureJwt([]); - $metaData = $verifier->getMetaData(); + $verifier = new JwtVerifier($this->server, $this->adaptor, [ + JwtVerifier::VALIDATE_NONCE => 'my nonce', + JwtVerifier::VALIDATE_AUDIENCE => 'my audience', + JwtVerifier::VALIDATE_CLIENT_ID => 'my client id', + ]); - self::assertEquals( - 'https://example.com', - $metaData->jwks_uri, - 'Metadata was not accessed.' + self::assertSame( + $jwt, + $verifier->verify('my token') ); + } + + /** @test */ + public function can_verify_nonce(): void + { + $this->configureJwt(['nonce' => 'correct nonce']); + + $verifier = new JwtVerifier($this->server, $this->adaptor, [ + JwtVerifier::VALIDATE_NONCE => 'my nonce', + ]); + + $this->expectException(DomainException::class); + $this->expectExceptionMessageMatches('~Nonce does not match what is expected~'); + + $verifier->verify('my token'); + } + + /** @test */ + public function can_verify_audience(): void + { + $this->configureJwt(['aud' => 'correct audience']); + + $verifier = new JwtVerifier($this->server, $this->adaptor, [ + JwtVerifier::VALIDATE_AUDIENCE => 'my audience', + ]); + + $this->expectException(DomainException::class); + $this->expectExceptionMessageMatches('~Audience does not match what is expected~'); + $verifier->verify('my token'); } + /** @test */ + public function can_verify_client_id(): void + { + $this->configureJwt(['cid' => 'correct audience']); + + $verifier = new JwtVerifier($this->server, $this->adaptor, [ + JwtVerifier::VALIDATE_CLIENT_ID => 'my audience', + ]); + $this->expectException(DomainException::class); + $this->expectExceptionMessageMatches('~ClientId does not match what is expected~'); + + $verifier->verify('my token'); + } } diff --git a/tests/Unit/RequestTest.php b/tests/Unit/RequestTest.php index 8b152bb..d08a743 100644 --- a/tests/Unit/RequestTest.php +++ b/tests/Unit/RequestTest.php @@ -15,12 +15,28 @@ * limitations under the License. * ******************************************************************************/ +namespace Test\Unit; + use Http\Mock\Client; use Okta\JwtVerifier\Request; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\ResponseInterface; +use Test\BaseTestCase; class RequestTest extends BaseTestCase { + /** + * @var MockObject|ResponseInterface + */ + protected $response; + + public function setUp(): void + { + parent::setUp(); + + $this->response = $this->createMock(ResponseInterface::class); + } + /** @test */ public function makes_request_to_correct_location(): void { diff --git a/tests/Unit/Server/DefaultServerTest.php b/tests/Unit/Server/DefaultServerTest.php new file mode 100644 index 0000000..43d8d08 --- /dev/null +++ b/tests/Unit/Server/DefaultServerTest.php @@ -0,0 +1,263 @@ +server = $this->createMock(Server::class); + $this->request = $this->createMock(Request::class); + $this->response = $this->createMock(ResponseInterface::class); + $this->discovery = $this->createMock(Discovery::class); + $this->adaptor = $this->createMock(FirebasePhpJwt::class); + } + + /** @test */ + public function can_get_issuer_from_constructor(): void + { + $server = new DefaultServer( + 'test-issuer', + $this->discovery, + $this->request + ); + + self::assertSame( + 'test-issuer', + $server->getIssuer(), + 'Does not return issuer correctly' + ); + } + + /** @test */ + public function can_get_discovery_from_constructor(): void + { + $server = new DefaultServer( + 'test-issuer', + $this->discovery, + $this->request + ); + + self::assertSame( + $this->discovery, + $server->getDiscovery(), + 'Does not return discovery correctly' + ); + } + + protected function configureMetaDataResponse($response): void + { + $this->discovery + ->expects(self::once()) + ->method('getWellKnown') + ->willreturn('/test-well-known'); + + $this->request + ->expects(self::once()) + ->method('setUrl') + ->with('https://my.issuer.com/test-well-known') + ->willReturnSelf(); + + $this->request + ->expects(self::once()) + ->method('get') + ->willReturn($this->response); + + $stream = $this->createMock(StreamInterface::class); + $stream->expects(self::once()) + ->method('getContents') + ->willReturn($response); + + $this->response + ->expects(self::once()) + ->method('getBody') + ->willreturn($stream); + } + + /** @test */ + public function will_get_meta_data_from_issuer_and_well_known(): void + { + $this->configureMetaDataResponse('{"jwks_uri": "https://example.com"}'); + + $server = new DefaultServer( + 'https://my.issuer.com', + $this->discovery, + $this->request + ); + + $metaData = $server->getMetaData(); + + self::assertSame( + 'https://example.com', + $metaData->jwks_uri + ); + } + + /** + * @test + *@dataProvider invalidJsonProvider + */ + public function will_throw_exception_when_meta_data_is_not_json_string( + $response, + string $message + ): void { + $this->configureMetaDataResponse($response); + + $server = new DefaultServer( + 'https://my.issuer.com', + $this->discovery, + $this->request + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches($message); + + $server->getMetaData(); + } + + public function invalidJsonProvider(): array + { + return [ + 'integer' => [ + 123, + '~did not receive json~' + ], + 'null' => [ + null, + '~did not receive json~' + ], + 'array' => [ + ['test'], + '~did not receive json~' + ], + 'object' => [ + new \stdClass, + '~did not receive json~' + ], + 'invalid json' => [ + '{this is not valid}', + '~did not receive a valid json object~' + ], + 'not a json object' => [ + '["this is an array"]', + '~did not receive a valid json object~' + ], + ]; + } + + protected function configureJWKSResponse($response): void + { + $this->discovery + ->expects(self::once()) + ->method('getWellKnown') + ->willreturn('/test-well-known'); + + $this->request + ->expects(self::exactly(2)) + ->method('setUrl') + ->withConsecutive( + ['https://my.issuer.com/test-well-known'], + ['https://my.issuer.com/jwks.json'] + ) + ->willReturnSelf(); + + $metaDataResponse = $this->createMock(ResponseInterface::class); + $metaDataStream = $this->createMock(StreamInterface::class); + $metaDataStream->expects(self::once()) + ->method('getContents') + ->willReturn('{"jwks_uri": "https://my.issuer.com/jwks.json"}'); + $metaDataResponse + ->method('getBody') + ->willreturn($metaDataStream); + + $jwksResponse = $this->createMock(ResponseInterface::class); + $jwksStream = $this->createMock(StreamInterface::class); + $jwksStream->expects(self::once()) + ->method('getContents') + ->willReturn($response); + $jwksResponse + ->expects(self::once()) + ->method('getBody') + ->willreturn($jwksStream); + + $this->request + ->method('get') + ->willReturnOnConsecutiveCalls($metaDataResponse, $jwksResponse); + } + + /** @test */ + public function will_get_jwks_from_well_known(): void + { + $this->configureJWKSResponse('{"keys": ["test"]}'); + + $server = new DefaultServer( + 'https://my.issuer.com', + $this->discovery, + $this->request + ); + + $keys = $server->getKeys(); + + self::assertEquals( + '{"keys": ["test"]}', + $keys + ); + } +}