From 43e62cd5ec35f1727537f7959c6b6ec9de89ed74 Mon Sep 17 00:00:00 2001 From: Eric Schricker Date: Tue, 21 Sep 2021 14:39:44 +0200 Subject: [PATCH] Migrated pull request https://github.com/tymondesigns/jwt-auth/pull/2117 to update lcobucci to version 4 and php 8 --- composer.json | 2 +- src/Providers/AbstractServiceProvider.php | 4 - src/Providers/JWT/Lcobucci.php | 145 +++++++++++++++++----- tests/Providers/JWT/LcobucciTest.php | 141 +++++++++++++++------ 4 files changed, 215 insertions(+), 77 deletions(-) diff --git a/composer.json b/composer.json index 9dd45f9..77ba10b 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "illuminate/database": "^8.61", "illuminate/http": "^5.2|^6|^7|^8", "illuminate/support": "^5.2|^6|^7|^8", - "lcobucci/jwt": "<3.4", + "lcobucci/jwt": "^4.0", "namshi/jose": "^7.0", "nesbot/carbon": "^1.0|^2.0" }, diff --git a/src/Providers/AbstractServiceProvider.php b/src/Providers/AbstractServiceProvider.php index 6f753a5..3994444 100644 --- a/src/Providers/AbstractServiceProvider.php +++ b/src/Providers/AbstractServiceProvider.php @@ -12,8 +12,6 @@ namespace PHPOpenSourceSaver\JWTAuth\Providers; use Illuminate\Support\ServiceProvider; -use Lcobucci\JWT\Builder as JWTBuilder; -use Lcobucci\JWT\Parser as JWTParser; use Namshi\JOSE\JWS; use PHPOpenSourceSaver\JWTAuth\Blacklist; use PHPOpenSourceSaver\JWTAuth\Claims\Factory as ClaimFactory; @@ -167,8 +165,6 @@ protected function registerLcobucciProvider() { $this->app->singleton('tymon.jwt.provider.jwt.lcobucci', function ($app) { return new Lcobucci( - new JWTBuilder(), - new JWTParser(), $this->config('secret'), $this->config('algo'), $this->config('keys') diff --git a/src/Providers/JWT/Lcobucci.php b/src/Providers/JWT/Lcobucci.php index 1a598f9..b22b1f3 100644 --- a/src/Providers/JWT/Lcobucci.php +++ b/src/Providers/JWT/Lcobucci.php @@ -14,7 +14,7 @@ use Exception; use Illuminate\Support\Collection; use Lcobucci\JWT\Builder; -use Lcobucci\JWT\Parser; +use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Ecdsa; use Lcobucci\JWT\Signer\Ecdsa\Sha256 as ES256; use Lcobucci\JWT\Signer\Ecdsa\Sha384 as ES384; @@ -22,11 +22,15 @@ use Lcobucci\JWT\Signer\Hmac\Sha256 as HS256; use Lcobucci\JWT\Signer\Hmac\Sha384 as HS384; use Lcobucci\JWT\Signer\Hmac\Sha512 as HS512; -use Lcobucci\JWT\Signer\Keychain; +use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\Signer; +use Lcobucci\JWT\Signer\Key; use Lcobucci\JWT\Signer\Rsa; use Lcobucci\JWT\Signer\Rsa\Sha256 as RS256; use Lcobucci\JWT\Signer\Rsa\Sha384 as RS384; use Lcobucci\JWT\Signer\Rsa\Sha512 as RS512; +use Lcobucci\JWT\Token\RegisteredClaims; +use Lcobucci\JWT\Validation\Constraint\SignedWith; use ReflectionClass; use PHPOpenSourceSaver\JWTAuth\Contracts\Providers\JWT; use PHPOpenSourceSaver\JWTAuth\Exceptions\JWTException; @@ -35,42 +39,68 @@ class Lcobucci extends Provider implements JWT { /** - * The Builder instance. + * The builder instance. * - * @var \Lcobucci\JWT\Builder + * @var Builder */ protected $builder; /** - * The Parser instance. + * The configuration instance. * - * @var \Lcobucci\JWT\Parser + * @var Configuration */ - protected $parser; + protected $config; + + /** + * The Signer instance. + * + * @var Signer + */ + protected $signer; /** * Create the Lcobucci provider. * - * @param \Lcobucci\JWT\Builder $builder - * @param \Lcobucci\JWT\Parser $parser * @param string $secret * @param string $algo * @param array $keys + * @param Configuration $config Optional, to pass an existing configuration to be used. * * @return void */ public function __construct( - Builder $builder, - Parser $parser, $secret, $algo, - array $keys + array $keys, + $config = null ) { parent::__construct($secret, $algo, $keys); - $this->builder = $builder; - $this->parser = $parser; $this->signer = $this->getSigner(); + + if (!is_null($config)) { + $this->config = $config; + } elseif ($this->isAsymmetric()) { + $this->config = Configuration::forAsymmetricSigner($this->signer, $this->getSigningKey(), $this->getVerificationKey()); + } else { + $this->config = Configuration::forSymmetricSigner($this->signer, InMemory::plainText($this->getSecret())); + } + if (!count($this->config->validationConstraints())) { + $this->config->setValidationConstraints( + new SignedWith($this->signer, $this->getVerificationKey()), + ); + } + } + + /** + * Gets the {@see $config} attribute. + * + * @return Configuration + */ + public function getConfig() + { + return $this->config; } /** @@ -101,19 +131,18 @@ public function __construct( */ public function encode(array $payload) { - // Remove the signature on the builder instance first. - $this->builder->unsign(); + $this->builder = null; + $this->builder = $this->config->builder(); try { foreach ($payload as $key => $value) { - $this->builder->set($key, $value); + $this->addClaim($key, $value); } - $this->builder->sign($this->signer, $this->getSigningKey()); + + return $this->builder->getToken($this->config->signer(), $this->config->signingKey())->toString(); } catch (Exception $e) { - throw new JWTException('Could not create token: '.$e->getMessage(), $e->getCode(), $e); + throw new JWTException('Could not create token: ' . $e->getMessage(), $e->getCode(), $e); } - - return (string) $this->builder->getToken(); } /** @@ -128,20 +157,66 @@ public function encode(array $payload) public function decode($token) { try { - $jwt = $this->parser->parse($token); + $jwt = $this->config->parser()->parse($token); } catch (Exception $e) { - throw new TokenInvalidException('Could not decode token: '.$e->getMessage(), $e->getCode(), $e); + throw new TokenInvalidException('Could not decode token: ' . $e->getMessage(), $e->getCode(), $e); } - if (! $jwt->verify($this->signer, $this->getVerificationKey())) { + if (!$this->config->validator()->validate($jwt, ...$this->config->validationConstraints())) { throw new TokenInvalidException('Token Signature could not be verified.'); } - return (new Collection($jwt->getClaims()))->map(function ($claim) { - return is_object($claim) ? $claim->getValue() : $claim; + return (new Collection($jwt->claims()->all()))->map(function ($claim) { + if (is_a($claim, \DateTimeImmutable::class)) { + return $claim->getTimestamp(); + } + if (is_object($claim) && method_exists($claim, 'getValue')) { + return $claim->getValue(); + } + + return $claim; })->toArray(); } + /** + * Adds a claim to the {@see $config}. + * + * @param string $key + * @param mixed $value + */ + protected function addClaim($key, $value) + { + if (!isset($this->builder)) { + $this->builder = $this->config->builder(); + } + + switch ($key) { + case RegisteredClaims::ID: + $this->builder->identifiedBy($value); + break; + case RegisteredClaims::EXPIRATION_TIME: + $this->builder->expiresAt(\DateTimeImmutable::createFromFormat('U', $value)); + break; + case RegisteredClaims::NOT_BEFORE: + $this->builder->canOnlyBeUsedAfter(\DateTimeImmutable::createFromFormat('U', $value)); + break; + case RegisteredClaims::ISSUED_AT: + $this->builder->issuedAt(\DateTimeImmutable::createFromFormat('U', $value)); + break; + case RegisteredClaims::ISSUER: + $this->builder->issuedBy($value); + break; + case RegisteredClaims::AUDIENCE: + $this->builder->permittedFor($value); + break; + case RegisteredClaims::SUBJECT: + $this->builder->relatedTo($value); + break; + default: + $this->builder->withClaim($key, $value); + } + } + /** * Get the signer instance. * @@ -151,7 +226,7 @@ public function decode($token) */ protected function getSigner() { - if (! array_key_exists($this->algo, $this->signers)) { + if (!array_key_exists($this->algo, $this->signers)) { throw new JWTException('The given algorithm could not be found'); } @@ -169,22 +244,26 @@ protected function isAsymmetric() } /** - * {@inheritdoc} + * Get the key used to sign the tokens. + * + * @return Key|string */ protected function getSigningKey() { return $this->isAsymmetric() ? - (new Keychain())->getPrivateKey($this->getPrivateKey(), $this->getPassphrase()) : - $this->getSecret(); + InMemory::plainText($this->getPrivateKey(), $this->getPassphrase() ?? '') : + InMemory::plainText($this->getSecret()); } /** - * {@inheritdoc} + * Get the key used to verify the tokens. + * + * @return Key|string */ protected function getVerificationKey() { return $this->isAsymmetric() ? - (new Keychain())->getPublicKey($this->getPublicKey()) : - $this->getSecret(); + InMemory::plainText($this->getPublicKey()) : + InMemory::plainText($this->getSecret()); } } diff --git a/tests/Providers/JWT/LcobucciTest.php b/tests/Providers/JWT/LcobucciTest.php index 8e8554a..a182967 100644 --- a/tests/Providers/JWT/LcobucciTest.php +++ b/tests/Providers/JWT/LcobucciTest.php @@ -15,7 +15,12 @@ use InvalidArgumentException; use Lcobucci\JWT\Builder; use Lcobucci\JWT\Parser; +use Lcobucci\JWT\Signer; use Lcobucci\JWT\Signer\Key; +use Lcobucci\JWT\Signer\Rsa\Sha256 as RS256; +use Lcobucci\JWT\Token; +use Lcobucci\JWT\Token\DataSet; +use Lcobucci\JWT\Validation\Constraint; use Mockery; use Mockery\MockInterface; use PHPOpenSourceSaver\JWTAuth\Exceptions\JWTException; @@ -26,6 +31,13 @@ class LcobucciTest extends AbstractTestCase { + /** + * Mocks {@see Configuration}. + * + * @var MockInterface + */ + protected $config; + /** * @var MockInterface */ @@ -37,9 +49,9 @@ class LcobucciTest extends AbstractTestCase protected $builder; /** - * @var Namshi + * @var MockInterface */ - protected $provider; + protected $validator; public function setUp(): void { @@ -54,19 +66,22 @@ public function it_should_return_the_token_when_passing_a_valid_payload_to_encod { $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp + 3600, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - $this->builder->shouldReceive('unsign')->once()->andReturnSelf(); - $this->builder->shouldReceive('set')->times(count($payload)); - $this->builder->shouldReceive('sign')->once()->with(Mockery::any(), 'secret'); - $this->builder->shouldReceive('getToken')->once()->andReturn('foo.bar.baz'); + $dataSet = new DataSet($payload, 'payload'); - $token = $this->getProvider('secret', 'HS256')->encode($payload); + $this->builder->shouldReceive('relatedTo')->once()->andReturnSelf(); // sub + $this->builder->shouldReceive('expiresAt')->once()->andReturnSelf(); // exp + $this->builder->shouldReceive('issuedAt')->once()->andReturnSelf(); // iat + $this->builder->shouldReceive('issuedBy')->once()->andReturnSelf(); // iss + $this->builder + ->shouldReceive('getToken') + ->once() + ->with(\Mockery::type(Signer::class), \Mockery::type(Key::class)) + ->andReturn(new Token\Plain(new DataSet([], 'header'), $dataSet, (new Token\Signature('', 'signature')))); - $this->assertSame('foo.bar.baz', $token); - } + /** @var Token $token */ + $token = $this->getProvider('secret', 'HS256')->encode($payload); - public function getProvider($secret, $algo, array $keys = []) - { - return new Lcobucci($this->builder, $this->parser, $secret, $algo, $keys); + $this->assertSame('header.payload.signature', $token); } /** @test */ @@ -77,9 +92,15 @@ public function it_should_throw_an_invalid_exception_when_the_payload_could_not_ $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - $this->builder->shouldReceive('unsign')->once()->andReturnSelf(); - $this->builder->shouldReceive('set')->times(count($payload)); - $this->builder->shouldReceive('sign')->once()->with(Mockery::any(), 'secret')->andThrow(new Exception); + $this->builder->shouldReceive('relatedTo')->once()->andReturnSelf(); // sub + $this->builder->shouldReceive('expiresAt')->once()->andReturnSelf(); // exp + $this->builder->shouldReceive('issuedAt')->once()->andReturnSelf(); // iat + $this->builder->shouldReceive('issuedBy')->once()->andReturnSelf(); // iss + $this->builder + ->shouldReceive('getToken') + ->once() + ->with(\Mockery::type(Signer::class), \Mockery::type(Key::class)) + ->andThrow(new Exception); $this->getProvider('secret', 'HS256')->encode($payload); } @@ -89,24 +110,36 @@ public function it_should_return_the_payload_when_passing_a_valid_token_to_decod { $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp + 3600, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - $this->parser->shouldReceive('parse')->once()->with('foo.bar.baz')->andReturn(Mockery::self()); - $this->parser->shouldReceive('verify')->once()->with(Mockery::any(), 'secret')->andReturn(true); - $this->parser->shouldReceive('getClaims')->once()->andReturn($payload); + $token = Mockery::mock(Token::class); + $dataSet = Mockery::mock(new DataSet($payload, 'payload')); + + $provider = $this->getProvider('secret', 'HS256'); + + $this->parser->shouldReceive('parse')->once()->with('foo.bar.baz')->andReturn($token); + $this->validator->shouldReceive('validate')->once()->with($token, Mockery::any())->andReturnTrue(); + $token->shouldReceive('claims')->once()->andReturn($dataSet); + $dataSet->shouldReceive('all')->once()->andReturn($payload); - $this->assertSame($payload, $this->getProvider('secret', 'HS256')->decode('foo.bar.baz')); + $this->assertSame($payload, $provider->decode('foo.bar.baz')); } /** @test */ public function it_should_throw_a_token_invalid_exception_when_the_token_could_not_be_decoded_due_to_a_bad_signature() { + $token = Mockery::mock(Token::class); + $dataSet = Mockery::mock(new DataSet(['pay', 'load'], 'payload')); + + $provider = $this->getProvider('secret', 'HS256'); + $this->expectException(TokenInvalidException::class); $this->expectExceptionMessage('Token Signature could not be verified.'); - $this->parser->shouldReceive('parse')->once()->with('foo.bar.baz')->andReturn(Mockery::self()); - $this->parser->shouldReceive('verify')->once()->with(Mockery::any(), 'secret')->andReturn(false); - $this->parser->shouldReceive('getClaims')->never(); + $this->parser->shouldReceive('parse')->once()->with('foo.bar.baz')->andReturn($token); + $this->validator->shouldReceive('validate')->once()->with($token, Mockery::any())->andReturnFalse(); + $token->shouldReceive('claims')->never(); + $dataSet->shouldReceive('all')->never(); - $this->getProvider('secret', 'HS256')->decode('foo.bar.baz'); + $provider->decode('foo.bar.baz'); } /** @test */ @@ -125,32 +158,32 @@ public function it_should_throw_a_token_invalid_exception_when_the_token_could_n /** @test */ public function it_should_generate_a_token_when_using_an_rsa_algorithm() { + $dummyPrivateKey = $this->getDummyPrivateKey(); + $dummyPublicKey = $this->getDummyPublicKey(); + $provider = $this->getProvider( 'does_not_matter', 'RS256', - ['private' => $this->getDummyPrivateKey(), 'public' => $this->getDummyPublicKey()] + ['private' => $dummyPrivateKey, 'public' => $dummyPublicKey] ); $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp + 3600, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - $this->builder->shouldReceive('unsign')->once()->andReturnSelf(); - $this->builder->shouldReceive('set')->times(count($payload)); - $this->builder->shouldReceive('sign')->once()->with(Mockery::any(), Mockery::type(Key::class)); - $this->builder->shouldReceive('getToken')->once()->andReturn('foo.bar.baz'); - - $token = $provider->encode($payload); + $dataSet = new DataSet($payload, 'payload'); - $this->assertSame('foo.bar.baz', $token); - } + $this->builder->shouldReceive('relatedTo')->once()->andReturnSelf(); // sub + $this->builder->shouldReceive('expiresAt')->once()->andReturnSelf(); // exp + $this->builder->shouldReceive('issuedAt')->once()->andReturnSelf(); // iat + $this->builder->shouldReceive('issuedBy')->once()->andReturnSelf(); // iss + $this->builder + ->shouldReceive('getToken') + ->once() + ->with(Mockery::type(RS256::class), Mockery::type(Key::class)) + ->andReturn(new Token\Plain(new DataSet([], 'header'), $dataSet, (new Token\Signature('', 'signature')))); - public function getDummyPrivateKey() - { - return file_get_contents(__DIR__ . '/../Keys/id_rsa'); - } + $token = $provider->encode($payload); - public function getDummyPublicKey() - { - return file_get_contents(__DIR__ . '/../Keys/id_rsa.pub'); + $this->assertSame('header.payload.signature', $token); } /** @test */ @@ -188,4 +221,34 @@ public function it_should_return_the_keys() $this->assertSame($keys, $provider->getKeys()); } + + public function getProvider($secret, $algo, array $keys = []) + { + $provider = new Lcobucci($secret, $algo, $keys); + + $this->validator = Mockery::mock(\Lcobucci\JWT\Validator::class); + $this->config = Mockery::mock($provider->getConfig()); + + $provider = new Lcobucci($secret, $algo, $keys, $this->config); + + $this->config->shouldReceive('builder')->andReturn($this->builder); + $this->config->shouldReceive('parser')->andReturn($this->parser); + $this->config->shouldReceive('validator')->andReturn($this->validator); + + $constraint = Mockery::mock(Constraint::class); + $constraint->shouldReceive('assert')->andReturn(); + $this->config->shouldReceive('validationConstraints')->andReturn([$constraint]); + + return $provider; + } + + public function getDummyPrivateKey() + { + return file_get_contents(__DIR__ . '/../Keys/id_rsa'); + } + + public function getDummyPublicKey() + { + return file_get_contents(__DIR__ . '/../Keys/id_rsa.pub'); + } }