diff --git a/composer.json b/composer.json index a15b39bf3..74efb7560 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "illuminate/contracts": "^5.2|^6|^7|^8", "illuminate/http": "^5.2|^6|^7|^8", "illuminate/support": "^5.2|^6|^7|^8", - "lcobucci/jwt": "<3.4", + "lcobucci/jwt": "^3.4|^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 ca312a40e..45d629969 100644 --- a/src/Providers/AbstractServiceProvider.php +++ b/src/Providers/AbstractServiceProvider.php @@ -12,8 +12,6 @@ namespace Tymon\JWTAuth\Providers; use Illuminate\Support\ServiceProvider; -use Lcobucci\JWT\Builder as JWTBuilder; -use Lcobucci\JWT\Parser as JWTParser; use Namshi\JOSE\JWS; use Tymon\JWTAuth\Blacklist; use Tymon\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 126bdda30..0f2c0d0f0 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,13 @@ 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\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 Tymon\JWTAuth\Contracts\Providers\JWT; use Tymon\JWTAuth\Exceptions\JWTException; @@ -35,42 +37,60 @@ class Lcobucci extends Provider implements JWT { /** - * The Builder instance. + * The Configuration instance. * - * @var \Lcobucci\JWT\Builder + * @var Configuration */ - protected $builder; + protected $config; - /** - * The Parser instance. - * - * @var \Lcobucci\JWT\Parser - */ - protected $parser; + /** @var \Lcobucci\JWT\Signer The signer chosen based on the aglo. */ + protected $signer; + + /** @var Builder */ + protected $builder; /** * Create the Lcobucci provider. * - * @param \Lcobucci\JWT\Builder $builder - * @param \Lcobucci\JWT\Parser $parser - * @param string $secret - * @param string $algo - * @param array $keys + * @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 +121,54 @@ public function __construct( */ public function encode(array $payload) { - // Remove the signature on the builder instance first. - $this->builder->unsign(); - + $this->builder = null; 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); } + } - return (string) $this->builder->getToken(); + /** + * 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); + } } /** @@ -128,17 +183,24 @@ 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); } - 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(); } @@ -174,8 +236,8 @@ protected function isAsymmetric() 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()); } /** @@ -184,7 +246,7 @@ protected function getSigningKey() 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 e3983023c..a1ced4346 100644 --- a/tests/Providers/JWT/LcobucciTest.php +++ b/tests/Providers/JWT/LcobucciTest.php @@ -14,8 +14,14 @@ use Exception; use InvalidArgumentException; use Lcobucci\JWT\Builder; +use Lcobucci\JWT\Configuration; 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 Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Exceptions\TokenInvalidException; @@ -24,6 +30,13 @@ class LcobucciTest extends AbstractTestCase { + /** + * Mocks {@see Configuration}. + * + * @var \Mockery\MockInterface + */ + protected $config; + /** * @var \Mockery\MockInterface */ @@ -35,9 +48,9 @@ class LcobucciTest extends AbstractTestCase protected $builder; /** - * @var \Tymon\JWTAuth\Providers\JWT\Namshi + * @var \Mockery\MockInterface */ - protected $provider; + protected $validator; public function setUp(): void { @@ -52,14 +65,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'); + $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')))); + + /** @var Token $token */ $token = $this->getProvider('secret', 'HS256')->encode($payload); - $this->assertSame('foo.bar.baz', $token); + $this->assertSame('header.payload.signature', $token); } /** @test */ @@ -70,9 +91,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); } @@ -82,24 +109,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 */ @@ -118,22 +157,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'); + $dataSet = new DataSet($payload, '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(RS256::class), Mockery::type(Key::class)) + ->andReturn(new Token\Plain(new DataSet([], 'header'), $dataSet, (new Token\Signature('', 'signature')))); $token = $provider->encode($payload); - $this->assertSame('foo.bar.baz', $token); + $this->assertSame('header.payload.signature', $token); } /** @test */ @@ -174,7 +223,22 @@ public function it_should_return_the_keys() public function getProvider($secret, $algo, array $keys = []) { - return new Lcobucci($this->builder, $this->parser, $secret, $algo, $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()