From 10f6f09ab7c29f268ff85378d64f17831f19cb42 Mon Sep 17 00:00:00 2001 From: Bizley Date: Sun, 26 Feb 2023 13:46:20 +0100 Subject: [PATCH 1/2] v4 --- INSTRUCTION.md | 3 + README.md | 36 ++-- composer.json | 7 +- infection.json.dist | 2 +- src/Jwt.php | 111 +++++-------- src/JwtHttpBearerAuth.php | 7 +- tests/BearerTest.php | 62 +++---- tests/ConstraintsConfigTest.php | 4 +- tests/JwtTest.php | 97 ++++++----- tests/SignerTest.php | 280 +++++++++++++++----------------- 10 files changed, 289 insertions(+), 320 deletions(-) diff --git a/INSTRUCTION.md b/INSTRUCTION.md index 81c0309..ddf96c9 100644 --- a/INSTRUCTION.md +++ b/INSTRUCTION.md @@ -66,6 +66,9 @@ Validation constraints used here are: - `LooseValidAt` - this will make sure that token is not expired yet allowing 10 seconds leeway (in case of some delays between the server and the client), we are here also setting the same time zone that is used in the application. +*NOTE*: The above implementation requires to install `lcobucci/clock` library first (run `composer req lcobucci/clock`). +If you prefer other PSR-20 clock implementation you must change the above `\Lcobucci\Clock\SystemClock()` usage. + You can also add here any other constraint that you find necessary. The available list is at https://github.com/lcobucci/jwt/tree/4.1.x/src/Validation/Constraint, and you can always write your own constraint as long as it implements `Lcobucci\JWT\Validation\Constraint`. diff --git a/README.md b/README.md index fde0c84..d93e58f 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,15 @@ This extension provides the [JWT](https://github.com/lcobucci/jwt) integration f > This is a fork of [sizeg/yii2-jwt](https://github.com/sizeg/yii2-jwt) package -**Version 3.x of this package uses `lcobucci/jwt` [v4](https://github.com/lcobucci/jwt/releases/tag/4.0.0) -and introduces critical BC changes, [see v4 lcobucci/jwt Upgrade Guide](https://lcobucci-jwt.readthedocs.io/en/latest/upgrading/). -For 2.x (and `lcobucci/jwt` v3) install `^2.0`.** +# Available versions + +| bizley/yii2-jwt | lcobucci/jwt | php | +|:---------------:|:------------:|:-------:| +| `^4.0` | `^5.0` | `>=8.1` | +| `^3.0` | `^4.0` | `>=7.4` | +| `^2.0` | `^3.0` | `>=7.1` | + +See [lcobucci/jwt](https://github.com/lcobucci/jwt) repo for details about the version. ## Installation @@ -20,12 +26,12 @@ Add the package to your `composer.json`: ```json { "require": { - "bizley/jwt": "^3.0" + "bizley/jwt": "^4.0" } } ``` -and run `composer update` or alternatively run `composer require bizley/jwt:^3.0` +and run `composer update` or alternatively run `composer require bizley/jwt:^4.0` ## Basic usage @@ -68,17 +74,7 @@ and `algorithmTypes` and using its ID for `signer`. ### Note on signers and minimum bits requirement Since `lcobucci/jwt 4.2.0` signers require the minimum key length to make sure those are properly secured, otherwise -the `InvalidKeyProvided` is thrown. If for any reason (**and on your own risk**) you would still like to use the less -secure key (for example HS256 with fewer than 256 bits length) you can wire it through this library by using the -`Unsafe` version of that signer (for example `Lcobucci\JWT\Signer\Hmac\Sha256` has the unsafe version -`Lcobucci\JWT\Signer\Hmac\UnsafeSha256`). Unsafe versions are using the same algorithm ID, so you don't have to add them -on the `Jwt::$algorithmTypes` list, but you need to configure them manually for your signer configuration like: - -```php -[ - 'signer' => [\Lcobucci\JWT\Signer\Hmac\UnsafeSha256::class], -] -``` +the `InvalidKeyProvided` is thrown. ### Keys @@ -91,16 +87,12 @@ Configuration array can be as the following: [ 'key' => /* key content */, 'passphrase' => /* key passphrase */, - 'store' => /* storage type */, 'method' => /* method type */, ] ``` - key (Jwt::KEY) - _string_, default `''`, - passphrase (Jwt::PASSPHRASE) - _string_, default `''`, -- store (Jwt::STORE) - _string_, default `Jwt::STORE_IN_MEMORY`, - available: `Jwt::STORE_IN_MEMORY`, `Jwt::STORE_LOCAL_FILE_REFERENCE` (deprecated since 3.2.0, will be removed in 4.0.0) - (see https://lcobucci-jwt.readthedocs.io/en/latest/configuration/) - method (Jwt::METHOD) - _string_, default `Jwt::METHOD_PLAIN`, available: `Jwt::METHOD_PLAIN`, `Jwt::METHOD_BASE64`, `Jwt::METHOD_FILE` (see https://lcobucci-jwt.readthedocs.io/en/latest/configuration/) @@ -111,18 +103,16 @@ Simple string keys are shortcuts to the following array configs: [ 'key' => /* given key itself */, 'passphrase' => '', - 'store' => Jwt::STORE_IN_MEMORY, 'method' => Jwt::METHOD_FILE, ] ``` - Detecting `@` at the beginning assumes Yii alias has been provided so it will be resolved with `Yii::getAlias()`. + Detecting `@` at the beginning assumes Yii alias has been provided, so it will be resolved with `Yii::getAlias()`. - key doesn't start with `@` nor `file://`: ```php [ 'key' => /* given key itself */, 'passphrase' => '', - 'store' => Jwt::STORE_IN_MEMORY, 'method' => Jwt::METHOD_PLAIN, ] ``` diff --git a/composer.json b/composer.json index 3ec3be4..f5305cf 100644 --- a/composer.json +++ b/composer.json @@ -18,14 +18,15 @@ "source": "https://github.com/bizley/yii2-jwt" }, "require": { - "php": ">=7.4", - "lcobucci/jwt": "^4.1", + "php": ">=8.1", + "lcobucci/jwt": "^5.0", "yiisoft/yii2": ">=2.0.14 <2.1" }, "require-dev": { "infection/infection": "*", + "lcobucci/clock": "^3.0", "phpstan/phpstan": "*", - "phpunit/phpunit": "^9.3", + "phpunit/phpunit": "^9.6", "roave/security-advisories": "dev-latest" }, "autoload": { diff --git a/infection.json.dist b/infection.json.dist index 3a55dc5..dca1b08 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -14,7 +14,7 @@ "@default": true, "MethodCallRemoval": { "ignore": [ - "bizley\\jwt\\Jwt::init::193", + "bizley\\jwt\\Jwt::init::186", "bizley\\jwt\\JwtHttpBearerAuth::init::77" ] } diff --git a/src/Jwt.php b/src/Jwt.php index 875ad72..c647ada 100644 --- a/src/Jwt.php +++ b/src/Jwt.php @@ -10,6 +10,7 @@ use Lcobucci\JWT\Decoder; use Lcobucci\JWT\Encoder; use Lcobucci\JWT\Encoding\CannotDecodeContent; +use Lcobucci\JWT\Encoding\JoseEncoder; use Lcobucci\JWT\Parser; use Lcobucci\JWT\Signer; use Lcobucci\JWT\Token; @@ -26,10 +27,10 @@ use function is_callable; use function is_string; use function reset; -use function strpos; +use function str_starts_with; /** - * JSON Web Token implementation based on lcobucci/jwt library v4. + * JSON Web Token implementation based on lcobucci/jwt library v5. * @see https://github.com/lcobucci/jwt * * @author Paweł Bizley Brzozowski since 2.0 (fork) @@ -50,7 +51,6 @@ class Jwt extends Component public const BLAKE2B = 'BLAKE2B'; public const STORE_IN_MEMORY = 'in_memory'; - public const STORE_LOCAL_FILE_REFERENCE = 'local_file_reference'; // deprecated since 3.2.0, will be removed in 4.0.0 public const METHOD_PLAIN = 'plain'; public const METHOD_BASE64 = 'base64'; @@ -69,18 +69,13 @@ class Jwt extends Component * This can be a simple string, an instance of Key, or a configuration array. * The configuration takes the following array keys: * - 'key' => Key's value or path to the key file. - * - 'store' => Either `Jwt::STORE_IN_MEMORY` or `Jwt::STORE_LOCAL_FILE_REFERENCE` (deprecated) - - * whether to keep the key in the memory or as a reference to a local file. * - 'method' => `Jwt::METHOD_PLAIN`, `Jwt::METHOD_BASE64`, or `Jwt::METHOD_FILE` - whether the key is a plain * text, base64 encoded text, or a file. - * In case the 'store' is set to `Jwt::STORE_LOCAL_FILE_REFERENCE` (deprecated), only - * `Jwt::METHOD_FILE` method is available. * - 'passphrase' => Key's passphrase. * In case a simple string is provided (and it does not start with 'file://' or '@') the following configuration * is assumed: * [ * 'key' => // the original given value, - * 'store' => Jwt::STORE_IN_MEMORY, * 'method' => Jwt::METHOD_PLAIN, * 'passphrase' => '', * ] @@ -88,7 +83,6 @@ class Jwt extends Component * the following configuration is assumed: * [ * 'key' => // the original given value, - * 'store' => Jwt::STORE_IN_MEMORY, * 'method' => Jwt::METHOD_FILE, * 'passphrase' => '', * ] @@ -107,12 +101,11 @@ class Jwt extends Component public $verifyingKey = ''; /** - * @var string|Signer|null Signer ID or Signer instance to be used for signing/verifying. - * See $signers for available values. In case it's not set, no algorithm will be used, which may be handy if you - * want to do some testing, but it's NOT recommended for production environments. + * @var string|Signer Signer ID or Signer instance to be used for signing/verifying. + * See $signers for available values. Since 4.0.0 it cannot be empty anymore. * @since 3.0.0 */ - public $signer; + public $signer = ''; /** * @var array Default signers configuration. When instantiated it will use selected array to @@ -192,31 +185,27 @@ public function init(): void { parent::init(); - if ($this->signer === null) { - $this->configuration = Configuration::forUnsecuredSigner($this->prepareEncoder(), $this->prepareDecoder()); + $signerId = $this->signer; + if ($this->signer instanceof Signer) { + $signerId = $this->signer->algorithmId(); + } + if (in_array($signerId, $this->algorithmTypes[self::SYMMETRIC], true)) { + $this->configuration = Configuration::forSymmetricSigner( + $this->prepareSigner($this->signer), + $this->prepareKey($this->signingKey), + $this->prepareEncoder(), + $this->prepareDecoder() + ); + } elseif (in_array($signerId, $this->algorithmTypes[self::ASYMMETRIC], true)) { + $this->configuration = Configuration::forAsymmetricSigner( + $this->prepareSigner($this->signer), + $this->prepareKey($this->signingKey), + $this->prepareKey($this->verifyingKey), + $this->prepareEncoder(), + $this->prepareDecoder() + ); } else { - $signerId = $this->signer; - if ($this->signer instanceof Signer) { - $signerId = $this->signer->algorithmId(); - } - if (in_array($signerId, $this->algorithmTypes[self::SYMMETRIC], true)) { - $this->configuration = Configuration::forSymmetricSigner( - $this->prepareSigner($this->signer), - $this->prepareKey($this->signingKey), - $this->prepareEncoder(), - $this->prepareDecoder() - ); - } elseif (in_array($signerId, $this->algorithmTypes[self::ASYMMETRIC], true)) { - $this->configuration = Configuration::forAsymmetricSigner( - $this->prepareSigner($this->signer), - $this->prepareKey($this->signingKey), - $this->prepareKey($this->verifyingKey), - $this->prepareEncoder(), - $this->prepareDecoder() - ); - } else { - throw new InvalidConfigException('Invalid signer ID!'); - } + throw new InvalidConfigException('Invalid signer ID!'); } } @@ -251,6 +240,7 @@ public function getConfiguration(): Configuration /** * Since 3.0.0 this method is using different signature. + * Please note that since 4.0.0 Builder object is immutable. * @see https://lcobucci-jwt.readthedocs.io/en/latest/issuing-tokens/ for details of using the builder. * @throws InvalidConfigException */ @@ -270,6 +260,7 @@ public function getParser(): Parser } /** + * @param non-empty-string $jwt * @throws CannotDecodeContent When something goes wrong while decoding. * @throws Token\InvalidTokenStructure When token string structure is invalid. * @throws Token\UnsupportedHeaderFound When parsed token has an unsupported header. @@ -284,7 +275,7 @@ public function parse(string $jwt): Token /** * This method goes through every single constraint in the set, groups all the violations, and throws an exception * with the grouped violations. - * @param string|Token $jwt JWT string or instance of Token + * @param non-empty-string|Token $jwt JWT string or instance of Token * @throws Validation\RequiredConstraintsViolated When constraint is violated * @throws Validation\NoConstraintsGiven When no constraints are provided * @throws InvalidConfigException @@ -300,7 +291,7 @@ public function assert($jwt): void /** * This method return false on first constraint violation - * @param string|Token $jwt JWT string or instance of Token + * @param non-empty-string|Token $jwt JWT string or instance of Token * @throws InvalidConfigException * @since 3.0.0 */ @@ -331,22 +322,19 @@ private function prepareKey($key): Signer\Key if ($key === '') { throw new InvalidConfigException('Empty string used as a key configuration!'); } - if (strpos($key, '@') === 0) { + if (str_starts_with($key, '@')) { $keyConfig = [ self::KEY => Yii::getAlias($key), - self::STORE => self::STORE_IN_MEMORY, self::METHOD => self::METHOD_FILE, ]; - } elseif (strpos($key, 'file://') === 0) { + } elseif (str_starts_with($key, 'file://')) { $keyConfig = [ self::KEY => $key, - self::STORE => self::STORE_IN_MEMORY, self::METHOD => self::METHOD_FILE, ]; } else { $keyConfig = [ self::KEY => $key, - self::STORE => self::STORE_IN_MEMORY, self::METHOD => self::METHOD_PLAIN, ]; } @@ -357,16 +345,12 @@ private function prepareKey($key): Signer\Key } $value = $keyConfig[self::KEY] ?? ''; - $store = $keyConfig[self::STORE] ?? self::STORE_IN_MEMORY; $method = $keyConfig[self::METHOD] ?? self::METHOD_PLAIN; $passphrase = $keyConfig[self::PASSPHRASE] ?? ''; - if (!is_string($value)) { + if (!is_string($value) || $value === '') { throw new InvalidConfigException('Invalid key value!'); } - if (!in_array($store, [self::STORE_IN_MEMORY, self::STORE_LOCAL_FILE_REFERENCE], true)) { - throw new InvalidConfigException('Invalid key store!'); - } if (!in_array($method, [self::METHOD_PLAIN, self::METHOD_BASE64, self::METHOD_FILE], true)) { throw new InvalidConfigException('Invalid key method!'); } @@ -374,25 +358,14 @@ private function prepareKey($key): Signer\Key throw new InvalidConfigException('Invalid key passphrase!'); } - if ($store === self::STORE_IN_MEMORY) { - if ($value === '') { - return Signer\Key\InMemory::empty(); - } - if ($method === self::METHOD_BASE64) { - return Signer\Key\InMemory::base64Encoded($value, $passphrase); - } - if ($method === self::METHOD_FILE) { - return Signer\Key\InMemory::file($value, $passphrase); - } - - return Signer\Key\InMemory::plainText($value, $passphrase); + if ($method === self::METHOD_BASE64) { + return Signer\Key\InMemory::base64Encoded($value, $passphrase); } - - if ($method !== self::METHOD_FILE) { - throw new InvalidConfigException('Invalid key store and method combination!'); + if ($method === self::METHOD_FILE) { + return Signer\Key\InMemory::file($value, $passphrase); } - return Signer\Key\InMemory::file($value, $passphrase); + return Signer\Key\InMemory::plainText($value, $passphrase); } /** @@ -454,10 +427,10 @@ private function prepareValidationConstraints(): array /** * @throws InvalidConfigException */ - private function prepareEncoder(): ?Encoder + private function prepareEncoder(): Encoder { if ($this->encoder === null) { - return null; + return new JoseEncoder(); } /** @var Encoder $encoder */ @@ -469,10 +442,10 @@ private function prepareEncoder(): ?Encoder /** * @throws InvalidConfigException */ - private function prepareDecoder(): ?Decoder + private function prepareDecoder(): Decoder { if ($this->decoder === null) { - return null; + return new JoseEncoder(); } /** @var Decoder $decoder */ diff --git a/src/JwtHttpBearerAuth.php b/src/JwtHttpBearerAuth.php index 9eafb77..6ccc51c 100644 --- a/src/JwtHttpBearerAuth.php +++ b/src/JwtHttpBearerAuth.php @@ -120,9 +120,12 @@ public function authenticate($user, $request, $response): ?IdentityInterface // $identity = null; $token = null; + $data = $matches[1]; try { - $token = $this->processToken($matches[1]); + if (!empty($data)) { + $token = $this->processToken($data); + } } catch (Throwable $exception) { Yii::warning($exception->getMessage(), 'JwtHttpBearerAuth'); if (!$this->throwException) { @@ -148,7 +151,7 @@ public function authenticate($user, $request, $response): ?IdentityInterface // /** * Parses and validates the JWT token. - * @param string $data data provided in HTTP header, presumably JWT + * @param non-empty-string $data data provided in HTTP header, presumably JWT * @throws InvalidConfigException */ public function processToken(string $data): ?Token diff --git a/tests/BearerTest.php b/tests/BearerTest.php index 5e6d679..2cf8ec5 100644 --- a/tests/BearerTest.php +++ b/tests/BearerTest.php @@ -31,32 +31,34 @@ class BearerTest extends TestCase { protected function setUp(): void { - new Application([ - 'id' => 'test', - 'basePath' => __DIR__, - 'vendorPath' => __DIR__ . '/../vendor', - 'components' => [ - 'user' => [ - 'identityClass' => UserIdentity::class, - 'enableSession' => false, + new Application( + [ + 'id' => 'test', + 'basePath' => __DIR__, + 'vendorPath' => __DIR__ . '/../vendor', + 'components' => [ + 'user' => [ + 'identityClass' => UserIdentity::class, + 'enableSession' => false, + ], + 'request' => [ + 'enableCookieValidation' => false, + 'scriptFile' => __DIR__ . '/index.php', + 'scriptUrl' => '/index.php', + ], + 'jwt' => [ + 'class' => Jwt::class, + 'signer' => Jwt::HS256, + 'signingKey' => 'c2VjcmV0MXNlY3JldDFzZWNyZXQxc2VjcmV0M', + ], ], - 'request' => [ - 'enableCookieValidation' => false, - 'scriptFile' => __DIR__ . '/index.php', - 'scriptUrl' => '/index.php', + 'controllerMap' => [ + 'test-auth' => TestAuthController::class, + 'test-stub' => TestStubController::class, + 'test-stub2' => TestStub2Controller::class, ], - 'jwt' => [ - 'class' => Jwt::class, - 'signer' => Jwt::HS256, - 'signingKey' => 'c2VjcmV0MXNlY3JldDFzZWNyZXQxc2VjcmV0M', - ], - ], - 'controllerMap' => [ - 'test-auth' => TestAuthController::class, - 'test-stub' => TestStubController::class, - 'test-stub2' => TestStub2Controller::class, - ], - ]); + ] + ); } protected function getJwt(): Jwt @@ -112,7 +114,9 @@ public function testHttpBearerAuthExpiredToken(): void $now = new DateTimeImmutable(); - $this->getJwt()->getConfiguration()->setValidationConstraints(new LooseValidAt(SystemClock::fromSystemTimezone())); + $this->getJwt()->getConfiguration()->setValidationConstraints( + new LooseValidAt(SystemClock::fromSystemTimezone()) + ); $token = $this->getJwt()->getBuilder() ->issuedAt($now->modify('-10 minutes')) @@ -157,7 +161,9 @@ public function testHttpBearerAuthCustom(): void { $now = new DateTimeImmutable(); - $this->getJwt()->getConfiguration()->setValidationConstraints(new LooseValidAt(SystemClock::fromSystemTimezone())); + $this->getJwt()->getConfiguration()->setValidationConstraints( + new LooseValidAt(SystemClock::fromSystemTimezone()) + ); $token = $this->getJwt()->getBuilder() ->relatedTo('test') @@ -236,7 +242,7 @@ public function testHttpBearerAuthCustomNotIdentityInterface(): void public function testMethodsVisibility(): void { - $filter = new JwtHttpBearerAuth(['jwt' => new Jwt()]); + $filter = new JwtHttpBearerAuth(['jwt' => $this->getJwt()]); $jwt = $filter->getJwtComponent(); $jwt->getConfiguration()->setValidationConstraints(new IssuedBy('test')); @@ -250,7 +256,7 @@ public function testMethodsVisibility(): void public function testFailVisibility(): void { - $filter = new TestJwtHttpBearerAuth(['jwt' => new Jwt()]); + $filter = new TestJwtHttpBearerAuth(['jwt' => $this->getJwt()]); $filter->fail($this->createMock(Response::class)); self::assertSame(2, $filter->flag); diff --git a/tests/ConstraintsConfigTest.php b/tests/ConstraintsConfigTest.php index 8881cb7..57017fa 100644 --- a/tests/ConstraintsConfigTest.php +++ b/tests/ConstraintsConfigTest.php @@ -27,7 +27,9 @@ private function getJwt($validationConstraints): Jwt return Yii::createObject( [ 'class' => Jwt::class, - 'validationConstraints' => $validationConstraints + 'signer' => Jwt::HS256, + 'signingKey' => 'c2VjcmV0MXNlY3JldDFzZWNyZXQxc2VjcmV0M', + 'validationConstraints' => $validationConstraints, ] ); } diff --git a/tests/JwtTest.php b/tests/JwtTest.php index a9429bc..fff3d61 100644 --- a/tests/JwtTest.php +++ b/tests/JwtTest.php @@ -13,10 +13,21 @@ use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use PHPUnit\Framework\TestCase; use stdClass; +use Yii; use yii\base\InvalidConfigException; class JwtTest extends TestCase { + private function getJwt(): Jwt + { + return new Jwt( + [ + 'signer' => Jwt::HS256, + 'signingKey' => 'c2VjcmV0MXNlY3JldDFzZWNyZXQxc2VjcmV0M', + ] + ); + } + public function testAvailableSigners(): void { self::assertSame( @@ -33,7 +44,7 @@ public function testAvailableSigners(): void Jwt::EDDSA => [Signer\Eddsa::class], Jwt::BLAKE2B => [Signer\Blake2b::class], ], - (new Jwt())->signers, + $this->getJwt()->signers, ); } @@ -57,7 +68,7 @@ public function testAvailableAlgorithmTypes(): void Jwt::BLAKE2B, ], ], - (new Jwt())->algorithmTypes, + $this->getJwt()->algorithmTypes, ); } @@ -70,7 +81,7 @@ public function testNoInit(): void public function testValidateSuccess(): void { - $jwt = new Jwt(); + $jwt = $this->getJwt(); $config = $jwt->getConfiguration(); $config->setValidationConstraints(new IdentifiedBy('abc')); $token = $jwt->getBuilder()->identifiedBy('abc')->getToken($config->signer(), $config->signingKey()); @@ -79,7 +90,7 @@ public function testValidateSuccess(): void public function testValidateSuccessWithStringToken(): void { - $jwt = new Jwt(); + $jwt = $this->getJwt(); $config = $jwt->getConfiguration(); $config->setValidationConstraints(new IdentifiedBy('abc')); $token = $jwt->getBuilder()->identifiedBy('abc')->getToken( @@ -91,7 +102,7 @@ public function testValidateSuccessWithStringToken(): void public function testValidateFail(): void { - $jwt = new Jwt(); + $jwt = $this->getJwt(); $config = $jwt->getConfiguration(); $config->setValidationConstraints(new IdentifiedBy('abc')); $token = $jwt->getBuilder()->identifiedBy('def')->getToken($config->signer(), $config->signingKey()); @@ -103,7 +114,7 @@ public function testValidateFail(): void */ public function testAssertSuccess(): void { - $jwt = new Jwt(); + $jwt = $this->getJwt(); $config = $jwt->getConfiguration(); $config->setValidationConstraints(new IdentifiedBy('abc')); $token = $jwt->getBuilder()->identifiedBy('abc')->getToken($config->signer(), $config->signingKey()); @@ -115,7 +126,7 @@ public function testAssertSuccess(): void */ public function testAssertSuccessWithStringToken(): void { - $jwt = new Jwt(); + $jwt = $this->getJwt(); $config = $jwt->getConfiguration(); $config->setValidationConstraints(new IdentifiedBy('abc')); $token = $jwt->getBuilder()->identifiedBy('abc')->getToken( @@ -127,7 +138,7 @@ public function testAssertSuccessWithStringToken(): void public function testAssertFail(): void { - $jwt = new Jwt(); + $jwt = $this->getJwt(); $config = $jwt->getConfiguration(); $config->setValidationConstraints(new IdentifiedBy('abc')); $token = $jwt->getBuilder()->identifiedBy('def')->getToken($config->signer(), $config->signingKey()); @@ -135,20 +146,16 @@ public function testAssertFail(): void $jwt->assert($token); } - public function providerForInvalidKey(): array + public static function providerForInvalidKey(): iterable { - return [ - 'empty' => ['', 'Empty string used as a key configuration!'], - 'object' => [new stdClass(), 'Invalid key configuration!'], - 'int value' => [[Jwt::KEY => 1], 'Invalid key value!'], - 'array value' => [[Jwt::KEY => []], 'Invalid key value!'], - 'object value' => [[Jwt::KEY => new stdClass()], 'Invalid key value!'], - 'store' => [[Jwt::STORE => ''], 'Invalid key store!'], - 'method' => [[Jwt::METHOD => ''], 'Invalid key method!'], - 'int pass' => [[Jwt::PASSPHRASE => 1], 'Invalid key passphrase!'], - 'array pass' => [[Jwt::PASSPHRASE => []], 'Invalid key passphrase!'], - 'object pass' => [[Jwt::PASSPHRASE => new stdClass()], 'Invalid key passphrase!'], - ]; + yield 'object' => [new stdClass(), 'Invalid key configuration!']; + yield 'int value' => [[Jwt::KEY => 1], 'Invalid key value!']; + yield 'array value' => [[Jwt::KEY => []], 'Invalid key value!']; + yield 'object value' => [[Jwt::KEY => new stdClass()], 'Invalid key value!']; + yield 'method' => [[Jwt::KEY => 'k', Jwt::METHOD => ''], 'Invalid key method!']; + yield 'int pass' => [[Jwt::KEY => 'k', Jwt::PASSPHRASE => 1], 'Invalid key passphrase!']; + yield 'array pass' => [[Jwt::KEY => 'k', Jwt::PASSPHRASE => []], 'Invalid key passphrase!']; + yield 'object pass' => [[Jwt::KEY => 'k', Jwt::PASSPHRASE => new stdClass()], 'Invalid key passphrase!']; } /** @@ -171,7 +178,13 @@ public function testCustomEncoder(): void $encoder = $this->createMock(Encoder::class); $encoder->expects(self::exactly(3))->method('base64UrlEncode'); - $jwt = new Jwt(['encoder' => $encoder]); + $jwt = new Jwt( + [ + 'signer' => Jwt::HS256, + 'signingKey' => 'c2VjcmV0MXNlY3JldDFzZWNyZXQxc2VjcmV0M', + 'encoder' => $encoder, + ] + ); $jwt->getBuilder()->getToken($jwt->getConfiguration()->signer(), $jwt->getConfiguration()->signingKey()); } @@ -179,9 +192,15 @@ public function testCustomDecoder(): void { $decoder = $this->createMock(Decoder::class); $decoder->method('jsonDecode')->willReturn([]); - $decoder->expects(self::exactly(2))->method('base64UrlDecode'); + $decoder->expects(self::exactly(3))->method('base64UrlDecode'); - $jwt = new Jwt(['decoder' => $decoder]); + $jwt = new Jwt( + [ + 'signer' => Jwt::HS256, + 'signingKey' => 'c2VjcmV0MXNlY3JldDFzZWNyZXQxc2VjcmV0M', + 'decoder' => $decoder, + ] + ); $jwt->parse( $jwt->getBuilder()->getToken( $jwt->getConfiguration()->signer(), @@ -192,18 +211,16 @@ public function testCustomDecoder(): void public function testMethodsVisibility(): void { - $jwt = new Jwt(); + $jwt = $this->getJwt(); self::assertNotEmpty($jwt->getConfiguration()); self::assertNotEmpty($jwt->getBuilder()); self::assertNotEmpty($jwt->getParser()); } - public function providerForWrongKeyNames(): array + public static function providerForWrongKeyNames(): iterable { - return [ - '@' => ['_@bizley/tests/data/rs256.key'], - 'file://' => ['_file://' . __DIR__ . '/data/rs256.key'], - ]; + yield '@' => ['_@bizley/tests/data/rs256.key']; + yield 'file://' => ['_file://' . __DIR__ . '/data/rs256.key']; } /** @@ -222,12 +239,10 @@ public function testWrongFileKeyNameStartingCharacters(string $key): void self::assertSame($key, $jwt->getConfiguration()->signingKey()->contents()); } - public function providerForRightKeyNames(): array + public static function providerForRightKeyNames(): iterable { - return [ - '@' => ['@bizley/tests/data/rs256.key'], - 'file://' => ['file://' . __DIR__ . '/data/rs256.key'], - ]; + yield '@' => ['@bizley/tests/data/rs256.key']; + yield 'file://' => ['file://' . __DIR__ . '/data/rs256.key']; } /** @@ -300,13 +315,11 @@ public function testRightFileKeyNameStartingCharacters(string $key): void ); } - public function providerForSignerSignatureConverter(): array + public static function providerForSignerSignatureConverter(): iterable { - return [ - Jwt::ES256 => [Jwt::ES256], - Jwt::ES384 => [Jwt::ES384], - Jwt::ES512 => [Jwt::ES512], - ]; + yield Jwt::ES256 => [Jwt::ES256]; + yield Jwt::ES384 => [Jwt::ES384]; + yield Jwt::ES512 => [Jwt::ES512]; } /** @@ -317,7 +330,7 @@ public function testPrepareSignatureConverter(string $signerId): void new Jwt(['signer' => $signerId, 'signingKey' => ' ', 'verifyingKey' => ' ']); $this->assertInstanceOf( Signer\Ecdsa\MultibyteStringConverter::class, - \Yii::$container->get(Signer\Ecdsa\SignatureConverter::class) + Yii::$container->get(Signer\Ecdsa\SignatureConverter::class) ); } } diff --git a/tests/SignerTest.php b/tests/SignerTest.php index 7deb282..3391585 100644 --- a/tests/SignerTest.php +++ b/tests/SignerTest.php @@ -31,170 +31,150 @@ public function getJwt(array $config = []): Jwt return $jwt; } - public function providerForSigners(): array + public static function providerForSigners(): iterable { - return [ - 'No signer' => [ - [ - 'signer' => null, - ], - 'none' + yield 'Direct signer provided' => [ + [ + 'signer' => new Sha256(), + 'signingKey' => 'secret1secret1secret1secret1secret1secret1', ], - 'Direct signer provided' => [ - [ - 'signer' => new Sha256(), - 'signingKey' => 'secret1secret1secret1secret1secret1secret1', - ], - Jwt::HS256 + Jwt::HS256 + ]; + yield 'Direct key provided' => [ + [ + 'signer' => Jwt::HS256, + 'signingKey' => InMemory::plainText('secret1secret1secret1secret1secret1secret1') ], - 'Direct key provided' => [ - [ - 'signer' => Jwt::HS256, - 'signingKey' => InMemory::plainText('secret1secret1secret1secret1secret1secret1') - ], - Jwt::HS256 + Jwt::HS256 + ]; + yield 'HS256' => [ + [ + 'signer' => Jwt::HS256, + 'signingKey' => 'secret1secret1secret1secret1secret1secret1', ], - 'HS256' => [ - [ - 'signer' => Jwt::HS256, - 'signingKey' => 'secret1secret1secret1secret1secret1secret1', - ], - Jwt::HS256 - ], - 'HS256 base64' => [ - [ - 'signer' => Jwt::HS256, - 'signingKey' => [ - Jwt::KEY => 'c2VjcmV0MXNlY3JldDFzZWNyZXQxc2VjcmV0MXNlY3JldDFzZWNyZXQx', - Jwt::METHOD => JWT::METHOD_BASE64 - ], + Jwt::HS256 + ]; + yield 'HS256 base64' => [ + [ + 'signer' => Jwt::HS256, + 'signingKey' => [ + Jwt::KEY => 'c2VjcmV0MXNlY3JldDFzZWNyZXQxc2VjcmV0MXNlY3JldDFzZWNyZXQx', + Jwt::METHOD => JWT::METHOD_BASE64 ], - Jwt::HS256 ], - 'HS384' => [ - [ - 'signer' => Jwt::HS384, - 'signingKey' => 'secret1secret1secret1secret1secret1secret1secret1', - ], - Jwt::HS384 + Jwt::HS256 + ]; + yield 'HS384' => [ + [ + 'signer' => Jwt::HS384, + 'signingKey' => 'secret1secret1secret1secret1secret1secret1secret1', ], - 'HS512' => [ - [ - 'signer' => Jwt::HS512, - 'signingKey' => 'secret1secret1secret1secret1secret1secret1secret1secret1secret1secret1', - ], - Jwt::HS512 - ], - 'HS256 pass' => [ - [ - 'signer' => Jwt::HS256, - 'signingKey' => [ - Jwt::KEY => 'secret1secret1secret1secret1secret1secret1secret1', - Jwt::PASSPHRASE => 'passphrase' - ], - ], - Jwt::HS256 - ], - 'HS384 pass' => [ - [ - 'signer' => Jwt::HS384, - 'signingKey' => [ - Jwt::KEY => 'secret1secret1secret1secret1secret1secret1secret1', - Jwt::PASSPHRASE => 'passphrase' - ], - ], - Jwt::HS384 - ], - 'HS512 pass' => [ - [ - 'signer' => Jwt::HS512, - 'signingKey' => [ - Jwt::KEY => 'secret1secret1secret1secret1secret1secret1secret1secret1secret1secret1', - Jwt::PASSPHRASE => 'passphrase' - ], - ], - Jwt::HS512 + Jwt::HS384 + ]; + yield 'HS512' => [ + [ + 'signer' => Jwt::HS512, + 'signingKey' => 'secret1secret1secret1secret1secret1secret1secret1secret1secret1secret1', ], - 'RS256' => [ - [ - 'signer' => Jwt::RS256, - 'signingKey' => '@bizley/tests/data/rs256.key', - 'verifyingKey' => '@bizley/tests/data/rs256.key.pub', + Jwt::HS512 + ]; + yield 'HS256 pass' => [ + [ + 'signer' => Jwt::HS256, + 'signingKey' => [ + Jwt::KEY => 'secret1secret1secret1secret1secret1secret1secret1', + Jwt::PASSPHRASE => 'passphrase' ], - Jwt::RS256 ], - 'RS256 with file handler' => [ - [ - 'signer' => Jwt::RS256, - 'signingKey' => 'file://' . __DIR__ . '/data/rs256.key', - 'verifyingKey' => 'file://' . __DIR__ . '/data/rs256.key.pub', - ], - Jwt::RS256 - ], - 'RS256 with in-memory file' => [ - [ - 'signer' => Jwt::RS256, - 'signingKey' => [ - Jwt::KEY => 'file://' . __DIR__ . '/data/rs256.key', - Jwt::STORE => Jwt::STORE_IN_MEMORY, - Jwt::METHOD => Jwt::METHOD_FILE, - ], - 'verifyingKey' => 'file://' . __DIR__ . '/data/rs256.key.pub', - ], - Jwt::RS256 - ], - 'RS256 with local file' => [ - [ - 'signer' => Jwt::RS256, - 'signingKey' => [ - Jwt::KEY => 'file://' . __DIR__ . '/data/rs256.key', - Jwt::STORE => Jwt::STORE_LOCAL_FILE_REFERENCE, - Jwt::METHOD => Jwt::METHOD_FILE, - ], - 'verifyingKey' => 'file://' . __DIR__ . '/data/rs256.key.pub', + Jwt::HS256 + ]; + yield 'HS384 pass' => [ + [ + 'signer' => Jwt::HS384, + 'signingKey' => [ + Jwt::KEY => 'secret1secret1secret1secret1secret1secret1secret1', + Jwt::PASSPHRASE => 'passphrase' ], - Jwt::RS256 ], - 'RS384' => [ - [ - 'signer' => Jwt::RS384, - 'signingKey' => '@bizley/tests/data/rs384.key', - 'verifyingKey' => '@bizley/tests/data/rs384.key.pub', + Jwt::HS384 + ]; + yield 'HS512 pass' => [ + [ + 'signer' => Jwt::HS512, + 'signingKey' => [ + Jwt::KEY => 'secret1secret1secret1secret1secret1secret1secret1secret1secret1secret1', + Jwt::PASSPHRASE => 'passphrase' ], - Jwt::RS384 ], - 'RS512' => [ - [ - 'signer' => Jwt::RS512, - 'signingKey' => '@bizley/tests/data/rs512.key', - 'verifyingKey' => '@bizley/tests/data/rs512.key.pub', - ], - Jwt::RS512 + Jwt::HS512 + ]; + yield 'RS256' => [ + [ + 'signer' => Jwt::RS256, + 'signingKey' => '@bizley/tests/data/rs256.key', + 'verifyingKey' => '@bizley/tests/data/rs256.key.pub', ], - 'ES256' => [ - [ - 'signer' => Jwt::ES256, - 'signingKey' => '@bizley/tests/data/es256.key', - 'verifyingKey' => '@bizley/tests/data/es256.key.pub', - ], - Jwt::ES256 + Jwt::RS256 + ]; + yield 'RS256 with file handler' => [ + [ + 'signer' => Jwt::RS256, + 'signingKey' => 'file://' . __DIR__ . '/data/rs256.key', + 'verifyingKey' => 'file://' . __DIR__ . '/data/rs256.key.pub', ], - 'ES384' => [ - [ - 'signer' => Jwt::ES384, - 'signingKey' => '@bizley/tests/data/es384.key', - 'verifyingKey' => '@bizley/tests/data/es384.key.pub', + Jwt::RS256 + ]; + yield 'RS256 with in-memory file' => [ + [ + 'signer' => Jwt::RS256, + 'signingKey' => [ + Jwt::KEY => 'file://' . __DIR__ . '/data/rs256.key', + Jwt::STORE => Jwt::STORE_IN_MEMORY, + Jwt::METHOD => Jwt::METHOD_FILE, ], - Jwt::ES384 + 'verifyingKey' => 'file://' . __DIR__ . '/data/rs256.key.pub', ], - 'ES512' => [ - [ - 'signer' => Jwt::ES512, - 'signingKey' => '@bizley/tests/data/es512.key', - 'verifyingKey' => '@bizley/tests/data/es512.key.pub', - ], - Jwt::ES512 + Jwt::RS256 + ]; + yield 'RS384' => [ + [ + 'signer' => Jwt::RS384, + 'signingKey' => '@bizley/tests/data/rs384.key', + 'verifyingKey' => '@bizley/tests/data/rs384.key.pub', + ], + Jwt::RS384 + ]; + yield 'RS512' => [ + [ + 'signer' => Jwt::RS512, + 'signingKey' => '@bizley/tests/data/rs512.key', + 'verifyingKey' => '@bizley/tests/data/rs512.key.pub', + ], + Jwt::RS512 + ]; + yield 'ES256' => [ + [ + 'signer' => Jwt::ES256, + 'signingKey' => '@bizley/tests/data/es256.key', + 'verifyingKey' => '@bizley/tests/data/es256.key.pub', + ], + Jwt::ES256 + ]; + yield 'ES384' => [ + [ + 'signer' => Jwt::ES384, + 'signingKey' => '@bizley/tests/data/es384.key', + 'verifyingKey' => '@bizley/tests/data/es384.key.pub', + ], + Jwt::ES384 + ]; + yield 'ES512' => [ + [ + 'signer' => Jwt::ES512, + 'signingKey' => '@bizley/tests/data/es512.key', + 'verifyingKey' => '@bizley/tests/data/es512.key.pub', ], + Jwt::ES512 ]; } @@ -220,20 +200,18 @@ public function testParseTokenWithSignature(array $config, string $algorithm): v public function testInvalidSignerId(): void { $this->expectException(InvalidConfigException::class); + $this->expectExceptionMessage('Invalid signer ID!'); $this->getJwt(['signer' => 'Invalid']); } - public function testInvalidKeyConfigCombination(): void + public function testEmptyKey(): void { $this->expectException(InvalidConfigException::class); + $this->expectExceptionMessage('Empty string used as a key configuration!'); $this->getJwt( [ 'signer' => Jwt::HS256, - 'signingKey' => [ - Jwt::KEY => 'file://' . __DIR__ . '/data/rs256.key', - Jwt::STORE => Jwt::STORE_LOCAL_FILE_REFERENCE, - Jwt::METHOD => Jwt::METHOD_PLAIN, - ], + 'signingKey' => '', ] ); } From 3da68b176021293a59bc15cec41cf0b6a467ae48 Mon Sep 17 00:00:00 2001 From: Bizley Date: Sun, 26 Feb 2023 13:48:41 +0100 Subject: [PATCH 2/2] v4 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6c8afcb..0194b85 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,7 +49,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['7.4', '8.0', '8.1'] + php: ['8.1', '8.2'] steps: - name: Checkout