From cc324026969108b7b6a8ac46b28428cb5d8581ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sat, 21 Nov 2020 02:53:40 +0100 Subject: [PATCH 01/17] Backport new key implementations Also adjusting public interface of the `Key` object. --- src/Signer/Key.php | 22 +++++- src/Signer/Key/InMemory.php | 52 +++++++++++++ src/Signer/Key/LocalFileReference.php | 30 +++++++ test/functional/CompatibilityLayerTest.php | 91 ++++++++++++++++++++++ 4 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 src/Signer/Key/InMemory.php create mode 100644 src/Signer/Key/LocalFileReference.php create mode 100644 test/functional/CompatibilityLayerTest.php diff --git a/src/Signer/Key.php b/src/Signer/Key.php index f6130ca7..433e63cd 100644 --- a/src/Signer/Key.php +++ b/src/Signer/Key.php @@ -19,12 +19,12 @@ * @author Luís Otávio Cobucci Oblonczyk * @since 3.0.4 */ -final class Key +class Key { /** * @var string */ - private $content; + protected $content; /** * @var string @@ -81,7 +81,22 @@ private function readFile($content) return $content; } + /** @return string */ + public function contents() + { + return $this->content; + } + + /** @return string */ + public function passphrase() + { + return $this->passphrase; + } + /** + * @deprecated This method is no longer part of the public interface + * @see Key::contents() + * * @return string */ public function getContent() @@ -90,6 +105,9 @@ public function getContent() } /** + * @deprecated This method is no longer part of the public interface + * @see Key::passphrase() + * * @return string */ public function getPassphrase() diff --git a/src/Signer/Key/InMemory.php b/src/Signer/Key/InMemory.php new file mode 100644 index 00000000..a40f4bfd --- /dev/null +++ b/src/Signer/Key/InMemory.php @@ -0,0 +1,52 @@ +content = 'file://' . $path; + + return $key; + } +} diff --git a/test/functional/CompatibilityLayerTest.php b/test/functional/CompatibilityLayerTest.php new file mode 100644 index 00000000..f09aeb30 --- /dev/null +++ b/test/functional/CompatibilityLayerTest.php @@ -0,0 +1,91 @@ +issuedBy('me') + ->relatedTo('user123') + ->getToken($signer, $key); + + self::assertTrue($token->verify($signer, self::$rsaKeys['public'])); + } + + public function possibleKeys() + { + $rsaKey = <<<'RSA' +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTvwE87MtgREYL +TL4aHhQo3ZzogmxxvMUsKnPzyxRs1YrXOSOpwN0npsXarBKKVIUMNLfFODp/vnQn +2Zp06N8XG59WAOKwvC4MfxLDQkA+JXggzHlkbVoTN+dUkdYIFqSKuAPGwiWToRK2 +SxEhij3rE2FON8jQZvDxZkiP9a4vxJO3OTPQwKredXFiObsXD/c3RtLFhKctjCyH +OIrP0bQEsee/m7JNtG4ry6BPusN6wb+vJo5ieBYPa3c19akNq6q/nYWhplhkkJSu +aOrL5xXEFzI5TvcvnXR568GVcxK8YLfFkdxpsXGt5rAbeh0h/U5kILEAqv8P9PGT +ZpicKbrnAgMBAAECggEAd3yTQEQHR91/ASVfKPHMQns77eCbPVtekFusbugsMHYY +EPdHbqVMpvFvOMRc+f5Tzd15ziq6qBdbCJm8lThLm4iU0z1QrpaiDZ8vgUvDYM5Y +CXoZDli+uZWUTp60/n94fmb0ipZIChScsI2PrzOJWTvobvD/uso8MJydWc8zafQm +uqYzygOfjFZvU4lSfgzpefhpquy0JUy5TiKRmGUnwLb3TtcsVavjsn4QmNwLYgOF +2OE+R12ex3pAKTiRE6FcnE1xFIo1GKhBa2Otgw3MDO6Gg+kn8Q4alKz6C6RRlgaH +R7sYzEfJhsk/GGFTYOzXKQz2lSaStKt9wKCor04RcQKBgQDzPOu5jCTfayUo7xY2 +jHtiogHyKLLObt9l3qbwgXnaD6rnxYNvCrA0OMvT+iZXsFZKJkYzJr8ZOxOpPROk +10WdOaefiwUyL5dypueSwlIDwVm+hI4Bs82MajHtzOozh+73wA+aw5rPs84Uix9w +VbbwaVR6qP/BV09yJYS5kQ7fmwKBgQDe2xjywX2d2MC+qzRr+LfU+1+gq0jjhBCX +WHqRN6IECB0xTnXUf9WL/VCoI1/55BhdbbEja+4btYgcXSPmlXBIRKQ4VtFfVmYB +kPXeD8oZ7LyuNdCsbKNe+x1IHXDe6Wfs3L9ulCfXxeIE84wy3fd66mQahyXV9iD9 +CkuifMqUpQKBgQCiydHlY1LGJ/o9tA2Ewm5Na6mrvOs2V2Ox1NqbObwoYbX62eiF +53xX5u8bVl5U75JAm+79it/4bd5RtKux9dUETbLOhwcaOFm+hM+VG/IxyzRZ2nMD +1qcpY2U5BpxzknUvYF3RMTop6edxPk7zKpp9ubCtSu+oINvtxAhY/SkcIwKBgGP1 +upcImyO2GZ5shLL5eNubdSVILwV+M0LveOqyHYXZbd6z5r5OKKcGFKuWUnJwEU22 +6gGNY9wh7M9sJ7JBzX9c6pwqtPcidda2AtJ8GpbOTUOG9/afNBhiYpv6OKqD3w2r +ZmJfKg/qvpqh83zNezgy8nvDqwDxyZI2j/5uIx/RAoGBAMWRmxtv6H2cKhibI/aI +MTJM4QRjyPNxQqvAQsv+oHUbid06VK3JE+9iQyithjcfNOwnCaoO7I7qAj9QEfJS +MZQc/W/4DHJebo2kd11yoXPVTXXOuEwLSKCejBXABBY0MPNuPUmiXeU0O3Tyi37J +TUKzrgcd7NvlA41Y4xKcOqEA +-----END PRIVATE KEY----- +RSA; + + return [ + [Key\InMemory::plainText($rsaKey)], + [Key\InMemory::base64Encoded(base64_encode($rsaKey))], + [Key\InMemory::file(__DIR__ . '/rsa/private.key')], + [Key\LocalFileReference::file(__DIR__ . '/rsa/private.key')], + ]; + } +} From 29417a19f12941b0570b81c1f17560f91a24ae91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sat, 21 Nov 2020 03:03:20 +0100 Subject: [PATCH 02/17] Backport token dataset --- src/Token/DataSet.php | 56 +++++++++++++++++ test/unit/Token/DataSetTest.php | 107 ++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 src/Token/DataSet.php create mode 100644 test/unit/Token/DataSetTest.php diff --git a/src/Token/DataSet.php b/src/Token/DataSet.php new file mode 100644 index 00000000..696a4e6a --- /dev/null +++ b/src/Token/DataSet.php @@ -0,0 +1,56 @@ + */ + private $data; + /** @var string */ + private $encoded; + + /** + * @param array $data + * @param string $encoded + */ + public function __construct(array $data, $encoded) + { + $this->data = $data; + $this->encoded = $encoded; + } + + /** + * @param string $name + * @param mixed|null $default + * + * @return mixed|null + */ + public function get($name, $default = null) + { + return $this->has($name) ? $this->data[$name] : $default; + } + + /** + * @param string $name + * + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->data); + } + + /** @return array */ + public function all() + { + return $this->data; + } + + /** @return string */ + public function toString() + { + return $this->encoded; + } +} diff --git a/test/unit/Token/DataSetTest.php b/test/unit/Token/DataSetTest.php new file mode 100644 index 00000000..7d9dcfd3 --- /dev/null +++ b/test/unit/Token/DataSetTest.php @@ -0,0 +1,107 @@ + 1], 'one=1'); + + self::assertSame(1, $set->get('one')); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::get + * + * @uses \Lcobucci\JWT\Token\DataSet::has + */ + public function getShouldReturnTheFallbackValueWhenItWasGiven() + { + $set = new DataSet(['one' => 1], 'one=1'); + + self::assertSame(2, $set->get('two', 2)); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::get + * + * @uses \Lcobucci\JWT\Token\DataSet::has + */ + public function getShouldReturnNullWhenFallbackValueWasNotGiven() + { + $set = new DataSet(['one' => 1], 'one=1'); + + self::assertNull($set->get('two')); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::has + */ + public function hasShouldReturnTrueWhenItemWasConfigured() + { + $set = new DataSet(['one' => 1], 'one=1'); + + self::assertTrue($set->has('one')); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::has + */ + public function hasShouldReturnFalseWhenItemWasNotConfigured() + { + $set = new DataSet(['one' => 1], 'one=1'); + + self::assertFalse($set->has('two')); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::all + */ + public function allShouldReturnAllConfiguredItems() + { + $items = ['one' => 1, 'two' => 2]; + $set = new DataSet($items, 'one=1'); + + self::assertSame($items, $set->all()); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::toString + */ + public function toStringShouldReturnTheEncodedData() + { + $set = new DataSet(['one' => 1], 'one=1'); + + self::assertSame('one=1', $set->toString()); + } +} From 951128fe357fd181fcf791323b590fab4db55382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sat, 21 Nov 2020 03:26:07 +0100 Subject: [PATCH 03/17] Expose token signature --- src/Token.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Token.php b/src/Token.php index a9bdc7ad..c6b91e1f 100644 --- a/src/Token.php +++ b/src/Token.php @@ -267,6 +267,12 @@ public function getPayload() return $this->payload[0] . '.' . $this->payload[1]; } + /** @return Signature|null */ + public function signature() + { + return $this->signature; + } + /** * Returns an encoded representation of the token * From 9b46118513c1b9d2bc58e122a9d89ed6fc4b3d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sat, 21 Nov 2020 03:27:02 +0100 Subject: [PATCH 04/17] Remove assertions on token attribute values These are anti-patterns that we should have already removed. --- test/unit/ParserTest.php | 29 ++++++++++++++--------------- test/unit/TokenTest.php | 12 ++++++++---- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/test/unit/ParserTest.php b/test/unit/ParserTest.php index 62e0d01c..7354a28a 100644 --- a/test/unit/ParserTest.php +++ b/test/unit/ParserTest.php @@ -148,7 +148,7 @@ public function parseMustRaiseExceptionWhenHeaderIsFromAnEncryptedToken() * @test * * @uses Lcobucci\JWT\Parser::__construct - * @uses Lcobucci\JWT\Token::__construct + * @uses Lcobucci\JWT\Token * * @covers Lcobucci\JWT\Parser::parse * @covers Lcobucci\JWT\Parser::splitJwt @@ -170,16 +170,16 @@ public function parseMustReturnANonSignedTokenWhenSignatureIsNotInformed() $parser = $this->createParser(); $token = $parser->parse('a.a.'); - $this->assertAttributeEquals(['typ' => 'JWT', 'alg' => 'none'], 'headers', $token); - $this->assertAttributeEquals(['aud' => $this->defaultClaim], 'claims', $token); - $this->assertAttributeEquals(null, 'signature', $token); + $this->assertEquals(['typ' => 'JWT', 'alg' => 'none'], $token->getHeaders()); + $this->assertEquals(['aud' => $this->defaultClaim], $token->getClaims()); + $this->assertNull($token->signature()); } /** * @test * * @uses Lcobucci\JWT\Parser::__construct - * @uses Lcobucci\JWT\Token::__construct + * @uses Lcobucci\JWT\Token * * @covers Lcobucci\JWT\Parser::parse * @covers Lcobucci\JWT\Parser::splitJwt @@ -200,22 +200,21 @@ public function parseShouldReplicateClaimValueOnHeaderWhenNeeded() $parser = $this->createParser(); $token = $parser->parse('a.a.'); - $this->assertAttributeEquals( + $this->assertEquals( ['typ' => 'JWT', 'alg' => 'none', 'aud' => $this->defaultClaim], - 'headers', - $token + $token->getHeaders() ); - $this->assertAttributeEquals(['aud' => $this->defaultClaim], 'claims', $token); - $this->assertAttributeEquals(null, 'signature', $token); + $this->assertEquals(['aud' => $this->defaultClaim], $token->getClaims()); + $this->assertNull($token->signature()); } /** * @test * * @uses Lcobucci\JWT\Parser::__construct - * @uses Lcobucci\JWT\Token::__construct - * @uses Lcobucci\JWT\Signature::__construct + * @uses Lcobucci\JWT\Token + * @uses Lcobucci\JWT\Signature * * @covers Lcobucci\JWT\Parser::parse * @covers Lcobucci\JWT\Parser::splitJwt @@ -240,8 +239,8 @@ public function parseMustReturnASignedTokenWhenSignatureIsInformed() $parser = $this->createParser(); $token = $parser->parse('a.a.a'); - $this->assertAttributeEquals(['typ' => 'JWT', 'alg' => 'HS256'], 'headers', $token); - $this->assertAttributeEquals(['aud' => $this->defaultClaim], 'claims', $token); - $this->assertAttributeEquals(new Signature('aaa'), 'signature', $token); + $this->assertEquals(['typ' => 'JWT', 'alg' => 'HS256'], $token->getHeaders()); + $this->assertEquals(['aud' => $this->defaultClaim], $token->getClaims()); + $this->assertEquals(new Signature('aaa'), $token->signature()); } } diff --git a/test/unit/TokenTest.php b/test/unit/TokenTest.php index 2ec3bbab..ad8ac673 100644 --- a/test/unit/TokenTest.php +++ b/test/unit/TokenTest.php @@ -24,15 +24,19 @@ class TokenTest extends \PHPUnit\Framework\TestCase * @test * * @covers Lcobucci\JWT\Token::__construct + * @covers Lcobucci\JWT\Token::getHeaders + * @covers Lcobucci\JWT\Token::getClaims + * @covers Lcobucci\JWT\Token::signature + * @covers Lcobucci\JWT\Token::getPayload */ public function constructMustInitializeAnEmptyPlainTextTokenWhenNoArgumentsArePassed() { $token = new Token(); - $this->assertAttributeEquals(['alg' => 'none'], 'headers', $token); - $this->assertAttributeEquals([], 'claims', $token); - $this->assertAttributeEquals(null, 'signature', $token); - $this->assertAttributeEquals(['', ''], 'payload', $token); + $this->assertEquals(['alg' => 'none'], $token->getHeaders()); + $this->assertEquals([], $token->getClaims()); + $this->assertNull($token->signature()); + $this->assertEquals('.', $token->getPayload()); } /** From 386d09e53a9384614e50c0b51d31160f5280658b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sat, 21 Nov 2020 03:29:32 +0100 Subject: [PATCH 05/17] Fix docblock for token signature --- src/Token.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Token.php b/src/Token.php index c6b91e1f..0c0e90b0 100644 --- a/src/Token.php +++ b/src/Token.php @@ -40,7 +40,7 @@ class Token /** * The token signature * - * @var Signature + * @var Signature|null */ private $signature; @@ -57,7 +57,7 @@ class Token * @param array $headers * @param array $claims * @param array $payload - * @param Signature $signature + * @param Signature|null $signature */ public function __construct( array $headers = ['alg' => 'none'], From 3751cd0cdc73944b10537e2b851dfc4a68dfe483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sat, 21 Nov 2020 03:31:01 +0100 Subject: [PATCH 06/17] Use DataSet for Token headers --- src/Token.php | 60 +++++++++++++--------- test/functional/CompatibilityLayerTest.php | 1 + test/functional/EcdsaTokenTest.php | 1 + test/functional/HmacTokenTest.php | 2 + test/functional/RFC6978VectorTest.php | 1 + test/functional/RsaTokenTest.php | 1 + test/functional/UnsignedTokenTest.php | 2 + test/unit/BuilderTest.php | 2 + test/unit/ParserTest.php | 1 + test/unit/TokenTest.php | 4 +- 10 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/Token.php b/src/Token.php index 0c0e90b0..8e229617 100644 --- a/src/Token.php +++ b/src/Token.php @@ -13,7 +13,9 @@ use Generator; use Lcobucci\JWT\Claim\Validatable; use Lcobucci\JWT\Signer\Key; +use Lcobucci\JWT\Token\DataSet; use OutOfBoundsException; +use function func_num_args; /** * Basic structure of the JWT @@ -23,10 +25,13 @@ */ class Token { + /** @internal */ + const FAKE_DEFAULT_VALUE = '~~~WEIRD~DEFAULT~VALUE~~~'; + /** * The token headers * - * @var array + * @var DataSet */ private $headers; @@ -65,37 +70,54 @@ public function __construct( Signature $signature = null, array $payload = ['', ''] ) { - $this->headers = $headers; + $this->headers = new DataSet($headers, $payload[0]); $this->claims = $claims; $this->signature = $signature; $this->payload = $payload; } + /** @return DataSet */ + public function headers() + { + return $this->headers; + } + /** * Returns the token headers * + * @deprecated This method has been removed from the interface in v4.0 + * @see Token::headers() + * * @return array */ public function getHeaders() { - return $this->headers; + return $this->headers->all(); } /** * Returns if the header is configured * + * @deprecated This method has been removed from the interface in v4.0 + * @see Token::headers() + * @see DataSet::has() + * * @param string $name * * @return boolean */ public function hasHeader($name) { - return array_key_exists($name, $this->headers); + return $this->headers->has($name); } /** * Returns the value of a token header * + * @deprecated This method has been removed from the interface in v4.0 + * @see Token::headers() + * @see DataSet::has() + * * @param string $name * @param mixed $default * @@ -105,33 +127,21 @@ public function hasHeader($name) */ public function getHeader($name, $default = null) { - if ($this->hasHeader($name)) { - return $this->getHeaderValue($name); + if (func_num_args() === 1) { + $default = self::FAKE_DEFAULT_VALUE; } - if ($default === null) { + $value = $this->headers->get($name, $default); + + if ($value === self::FAKE_DEFAULT_VALUE) { throw new OutOfBoundsException(sprintf('Requested header "%s" is not configured', $name)); } - return $default; - } - - /** - * Returns the value stored in header - * - * @param string $name - * - * @return mixed - */ - private function getHeaderValue($name) - { - $header = $this->headers[$name]; - - if ($header instanceof Claim) { - return $header->getValue(); + if ($value instanceof Claim) { + return $value->getValue(); } - return $header; + return $value; } /** @@ -195,7 +205,7 @@ public function verify(Signer $signer, $key) throw new BadMethodCallException('This token is not signed'); } - if ($this->headers['alg'] !== $signer->getAlgorithmId()) { + if ($this->headers->get('alg') !== $signer->getAlgorithmId()) { return false; } diff --git a/test/functional/CompatibilityLayerTest.php b/test/functional/CompatibilityLayerTest.php index f09aeb30..8b6e52f9 100644 --- a/test/functional/CompatibilityLayerTest.php +++ b/test/functional/CompatibilityLayerTest.php @@ -24,6 +24,7 @@ * @covers \Lcobucci\JWT\Signer\Rsa\Sha256 * @covers \Lcobucci\JWT\Signature * @covers \Lcobucci\JWT\Token + * @covers \Lcobucci\JWT\Token\DataSet */ final class CompatibilityLayerTest extends TestCase { diff --git a/test/functional/EcdsaTokenTest.php b/test/functional/EcdsaTokenTest.php index 5043bb63..9d5ff7a5 100644 --- a/test/functional/EcdsaTokenTest.php +++ b/test/functional/EcdsaTokenTest.php @@ -22,6 +22,7 @@ * @since 2.1.0 * * @covers \Lcobucci\JWT\Signer\InvalidKeyProvided + * @covers \Lcobucci\JWT\Token\DataSet */ class EcdsaTokenTest extends \PHPUnit\Framework\TestCase { diff --git a/test/functional/HmacTokenTest.php b/test/functional/HmacTokenTest.php index 20101dd9..6f30b575 100644 --- a/test/functional/HmacTokenTest.php +++ b/test/functional/HmacTokenTest.php @@ -17,6 +17,8 @@ /** * @author Luís Otávio Cobucci Oblonczyk * @since 2.1.0 + * + * @covers \Lcobucci\JWT\Token\DataSet */ class HmacTokenTest extends \PHPUnit\Framework\TestCase { diff --git a/test/functional/RFC6978VectorTest.php b/test/functional/RFC6978VectorTest.php index 0076b6d8..69ef72ab 100644 --- a/test/functional/RFC6978VectorTest.php +++ b/test/functional/RFC6978VectorTest.php @@ -30,6 +30,7 @@ final class RFC6978VectorTest extends TestCase * @covers \Lcobucci\JWT\Signer\Ecdsa\Sha512 * @covers \Lcobucci\JWT\Signer\OpenSSL * @covers \Lcobucci\JWT\Signer\BaseSigner + * @covers \Lcobucci\JWT\Token\DataSet */ public function theVectorsFromRFC6978CanBeVerified( Ecdsa $signer, diff --git a/test/functional/RsaTokenTest.php b/test/functional/RsaTokenTest.php index 8a9cd699..a14b4bbc 100644 --- a/test/functional/RsaTokenTest.php +++ b/test/functional/RsaTokenTest.php @@ -21,6 +21,7 @@ * @since 2.1.0 * * @covers \Lcobucci\JWT\Signer\InvalidKeyProvided + * @covers \Lcobucci\JWT\Token\DataSet */ class RsaTokenTest extends \PHPUnit\Framework\TestCase { diff --git a/test/functional/UnsignedTokenTest.php b/test/functional/UnsignedTokenTest.php index 6e26fdff..4144025e 100644 --- a/test/functional/UnsignedTokenTest.php +++ b/test/functional/UnsignedTokenTest.php @@ -15,6 +15,8 @@ /** * @author Luís Otávio Cobucci Oblonczyk * @since 2.1.0 + * + * @covers \Lcobucci\JWT\Token\DataSet */ class UnsignedTokenTest extends \PHPUnit\Framework\TestCase { diff --git a/test/unit/BuilderTest.php b/test/unit/BuilderTest.php index 9e9aa932..ea7fa918 100644 --- a/test/unit/BuilderTest.php +++ b/test/unit/BuilderTest.php @@ -15,6 +15,8 @@ /** * @author Luís Otávio Cobucci Oblonczyk * @since 0.1.0 + * + * @covers \Lcobucci\JWT\Token\DataSet */ class BuilderTest extends \PHPUnit\Framework\TestCase { diff --git a/test/unit/ParserTest.php b/test/unit/ParserTest.php index 7354a28a..4d56fbbe 100644 --- a/test/unit/ParserTest.php +++ b/test/unit/ParserTest.php @@ -15,6 +15,7 @@ * @author Luís Otávio Cobucci Oblonczyk * @since 0.1.0 * + * @covers \Lcobucci\JWT\Token\DataSet * @covers \Lcobucci\JWT\Token\InvalidTokenStructure * @covers \Lcobucci\JWT\Token\UnsupportedHeaderFound */ diff --git a/test/unit/TokenTest.php b/test/unit/TokenTest.php index ad8ac673..d241db96 100644 --- a/test/unit/TokenTest.php +++ b/test/unit/TokenTest.php @@ -17,6 +17,8 @@ /** * @author Luís Otávio Cobucci Oblonczyk * @since 0.1.0 + * + * @covers \Lcobucci\JWT\Token\DataSet */ class TokenTest extends \PHPUnit\Framework\TestCase { @@ -107,7 +109,6 @@ public function getHeaderMustReturnTheDefaultValueWhenIsNotConfigured() * @uses Lcobucci\JWT\Token::hasHeader * * @covers Lcobucci\JWT\Token::getHeader - * @covers Lcobucci\JWT\Token::getHeaderValue */ public function getHeaderMustReturnTheRequestedHeader() { @@ -124,7 +125,6 @@ public function getHeaderMustReturnTheRequestedHeader() * @uses Lcobucci\JWT\Claim\Basic * * @covers Lcobucci\JWT\Token::getHeader - * @covers Lcobucci\JWT\Token::getHeaderValue */ public function getHeaderMustReturnValueWhenItIsAReplicatedClaim() { From da4bf270fc23e507b7c0b1259b97bee1e28ed92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sat, 21 Nov 2020 03:50:45 +0100 Subject: [PATCH 07/17] Use DataSet for Token claims --- src/Token.php | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/Token.php b/src/Token.php index 8e229617..0083da36 100644 --- a/src/Token.php +++ b/src/Token.php @@ -16,6 +16,7 @@ use Lcobucci\JWT\Token\DataSet; use OutOfBoundsException; use function func_num_args; +use function sprintf; /** * Basic structure of the JWT @@ -38,7 +39,7 @@ class Token /** * The token claim set * - * @var array + * @var DataSet */ private $claims; @@ -71,7 +72,7 @@ public function __construct( array $payload = ['', ''] ) { $this->headers = new DataSet($headers, $payload[0]); - $this->claims = $claims; + $this->claims = new DataSet($claims, $payload[1]); $this->signature = $signature; $this->payload = $payload; } @@ -144,31 +145,48 @@ public function getHeader($name, $default = null) return $value; } + /** @return DataSet */ + public function claims() + { + return $this->claims; + } + /** * Returns the token claim set * + * @deprecated This method has been removed from the interface in v4.0 + * @see Token::claims() + * * @return array */ public function getClaims() { - return $this->claims; + return $this->claims->all(); } /** * Returns if the claim is configured * + * @deprecated This method has been removed from the interface in v4.0 + * @see Token::claims() + * @see DataSet::has() + * * @param string $name * * @return boolean */ public function hasClaim($name) { - return array_key_exists($name, $this->claims); + return $this->claims->has($name); } /** * Returns the value of a token claim * + * @deprecated This method has been removed from the interface in v4.0 + * @see Token::claims() + * @see DataSet::get() + * * @param string $name * @param mixed $default * @@ -178,15 +196,21 @@ public function hasClaim($name) */ public function getClaim($name, $default = null) { - if ($this->hasClaim($name)) { - return $this->claims[$name]->getValue(); + if (func_num_args() === 1) { + $default = self::FAKE_DEFAULT_VALUE; } - if (func_num_args() === 1) { - throw new OutOfBoundsException(sprintf('Requested claim "%s" is not configured', $name)); + $value = $this->claims->get($name, $default); + + if ($value === self::FAKE_DEFAULT_VALUE) { + throw new OutOfBoundsException(sprintf('Requested header "%s" is not configured', $name)); + } + + if ($value instanceof Claim) { + return $value->getValue(); } - return $default; + return $value; } /** @@ -260,7 +284,7 @@ public function isExpired(DateTimeInterface $now = null) */ private function getValidatableClaims() { - foreach ($this->claims as $claim) { + foreach ($this->claims->all() as $claim) { if ($claim instanceof Validatable) { yield $claim; } From 88c07c15a73a80d20c74b38408198b442c92bef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sat, 21 Nov 2020 03:54:25 +0100 Subject: [PATCH 08/17] Backport Token#toString() --- src/Token.php | 10 ++++++++++ test/unit/TokenTest.php | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/Token.php b/src/Token.php index 0083da36..29e926c8 100644 --- a/src/Token.php +++ b/src/Token.php @@ -16,6 +16,7 @@ use Lcobucci\JWT\Token\DataSet; use OutOfBoundsException; use function func_num_args; +use function implode; use function sprintf; /** @@ -310,9 +311,18 @@ public function signature() /** * Returns an encoded representation of the token * + * @deprecated This method has been removed from the interface in v4.0 + * @see Token::toString() + * * @return string */ public function __toString() + { + return $this->toString(); + } + + /** @return string */ + public function toString() { $data = implode('.', $this->payload); diff --git a/test/unit/TokenTest.php b/test/unit/TokenTest.php index d241db96..f0b8f332 100644 --- a/test/unit/TokenTest.php +++ b/test/unit/TokenTest.php @@ -567,6 +567,7 @@ public function getPayloadShouldReturnAStringWithTheTwoEncodePartsThatGeneratedT * @uses Lcobucci\JWT\Token::getPayload * * @covers Lcobucci\JWT\Token::__toString + * @covers Lcobucci\JWT\Token::toString */ public function toStringMustReturnEncodedDataWithEmptySignature() { @@ -582,6 +583,7 @@ public function toStringMustReturnEncodedDataWithEmptySignature() * @uses Lcobucci\JWT\Token::getPayload * * @covers Lcobucci\JWT\Token::__toString + * @covers Lcobucci\JWT\Token::toString */ public function toStringMustReturnEncodedData() { From ddb7f2817d22ce055bd1c9157315f37a40404d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sat, 21 Nov 2020 04:09:21 +0100 Subject: [PATCH 09/17] Don't wrap header values on claim objects --- src/Builder.php | 4 ++-- src/Token.php | 22 +++++++++++++++++----- test/unit/BuilderTest.php | 6 +++++- test/unit/ParserTest.php | 27 +++++++++++---------------- test/unit/TokenTest.php | 6 +++++- 5 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/Builder.php b/src/Builder.php index 88d2a475..8a08b556 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -309,7 +309,7 @@ protected function setRegisteredClaim($name, $value, $replicate) $this->configureClaim($name, $value); if ($replicate) { - $this->headers[$name] = $this->claims[$name]; + $this->headers[$name] = $value; } return $this; @@ -325,7 +325,7 @@ protected function setRegisteredClaim($name, $value, $replicate) */ public function withHeader($name, $value) { - $this->headers[(string) $name] = $this->claimFactory->create($name, $value); + $this->headers[(string) $name] = $value; return $this; } diff --git a/src/Token.php b/src/Token.php index 29e926c8..99dec3b2 100644 --- a/src/Token.php +++ b/src/Token.php @@ -11,12 +11,16 @@ use DateTime; use DateTimeInterface; use Generator; +use Lcobucci\JWT\Claim\Factory; use Lcobucci\JWT\Claim\Validatable; use Lcobucci\JWT\Signer\Key; use Lcobucci\JWT\Token\DataSet; +use Lcobucci\JWT\Token\RegisteredClaimGiven; +use Lcobucci\JWT\Token\RegisteredClaims; use OutOfBoundsException; use function func_num_args; use function implode; +use function in_array; use function sprintf; /** @@ -94,7 +98,19 @@ public function headers() */ public function getHeaders() { - return $this->headers->all(); + $claimFactory = new Factory(); + $items = []; + + foreach ($this->headers->all() as $name => $value) { + if (! in_array($name, RegisteredClaims::ALL, true) || ! $this->claims->has($name)) { + $items[$name] = $value; + continue; + } + + $items[$name] = $claimFactory->create($name, $value); + } + + return $items; } /** @@ -139,10 +155,6 @@ public function getHeader($name, $default = null) throw new OutOfBoundsException(sprintf('Requested header "%s" is not configured', $name)); } - if ($value instanceof Claim) { - return $value->getValue(); - } - return $value; } diff --git a/test/unit/BuilderTest.php b/test/unit/BuilderTest.php index ea7fa918..b7dc684c 100644 --- a/test/unit/BuilderTest.php +++ b/test/unit/BuilderTest.php @@ -17,6 +17,10 @@ * @since 0.1.0 * * @covers \Lcobucci\JWT\Token\DataSet + * + * @uses \Lcobucci\JWT\Claim\Factory + * @uses \Lcobucci\JWT\Claim\EqualsTo + * @uses \Lcobucci\JWT\Claim\Basic */ class BuilderTest extends \PHPUnit\Framework\TestCase { @@ -532,7 +536,7 @@ public function withHeaderMustConfigureTheGivenClaim() $builder->withHeader('userId', 2); $this->assertAttributeEquals( - ['alg' => 'none', 'typ' => 'JWT', 'userId' => $this->defaultClaim], + ['alg' => 'none', 'typ' => 'JWT', 'userId' => 2], 'headers', $builder ); diff --git a/test/unit/ParserTest.php b/test/unit/ParserTest.php index 4d56fbbe..a9db7768 100644 --- a/test/unit/ParserTest.php +++ b/test/unit/ParserTest.php @@ -7,6 +7,7 @@ namespace Lcobucci\JWT; +use Lcobucci\JWT\Claim\EqualsTo; use Lcobucci\JWT\Claim\Factory as ClaimFactory; use Lcobucci\JWT\Parsing\Decoder; use RuntimeException; @@ -18,6 +19,10 @@ * @covers \Lcobucci\JWT\Token\DataSet * @covers \Lcobucci\JWT\Token\InvalidTokenStructure * @covers \Lcobucci\JWT\Token\UnsupportedHeaderFound + * + * @uses \Lcobucci\JWT\Claim\Factory + * @uses \Lcobucci\JWT\Claim\EqualsTo + * @uses \Lcobucci\JWT\Claim\Basic */ class ParserTest extends \PHPUnit\Framework\TestCase { @@ -27,27 +32,17 @@ class ParserTest extends \PHPUnit\Framework\TestCase protected $decoder; /** - * @var ClaimFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ClaimFactory */ protected $claimFactory; - /** - * @var Claim|\PHPUnit_Framework_MockObject_MockObject - */ - protected $defaultClaim; - /** * {@inheritdoc} */ protected function setUp() { $this->decoder = $this->createMock(Decoder::class); - $this->claimFactory = $this->createMock(ClaimFactory::class, [], [], '', false); - $this->defaultClaim = $this->createMock(Claim::class); - - $this->claimFactory->expects($this->any()) - ->method('create') - ->willReturn($this->defaultClaim); + $this->claimFactory = new ClaimFactory(); } /** @@ -172,7 +167,7 @@ public function parseMustReturnANonSignedTokenWhenSignatureIsNotInformed() $token = $parser->parse('a.a.'); $this->assertEquals(['typ' => 'JWT', 'alg' => 'none'], $token->getHeaders()); - $this->assertEquals(['aud' => $this->defaultClaim], $token->getClaims()); + $this->assertEquals(['aud' => new EqualsTo('aud', 'test')], $token->getClaims()); $this->assertNull($token->signature()); } @@ -202,11 +197,11 @@ public function parseShouldReplicateClaimValueOnHeaderWhenNeeded() $token = $parser->parse('a.a.'); $this->assertEquals( - ['typ' => 'JWT', 'alg' => 'none', 'aud' => $this->defaultClaim], + ['typ' => 'JWT', 'alg' => 'none', 'aud' => new EqualsTo('aud', 'test')], $token->getHeaders() ); - $this->assertEquals(['aud' => $this->defaultClaim], $token->getClaims()); + $this->assertEquals(['aud' => new EqualsTo('aud', 'test')], $token->getClaims()); $this->assertNull($token->signature()); } @@ -241,7 +236,7 @@ public function parseMustReturnASignedTokenWhenSignatureIsInformed() $token = $parser->parse('a.a.a'); $this->assertEquals(['typ' => 'JWT', 'alg' => 'HS256'], $token->getHeaders()); - $this->assertEquals(['aud' => $this->defaultClaim], $token->getClaims()); + $this->assertEquals(['aud' => new EqualsTo('aud', 'test')], $token->getClaims()); $this->assertEquals(new Signature('aaa'), $token->signature()); } } diff --git a/test/unit/TokenTest.php b/test/unit/TokenTest.php index f0b8f332..943f5864 100644 --- a/test/unit/TokenTest.php +++ b/test/unit/TokenTest.php @@ -19,6 +19,10 @@ * @since 0.1.0 * * @covers \Lcobucci\JWT\Token\DataSet + * + * @uses \Lcobucci\JWT\Claim\Factory + * @uses \Lcobucci\JWT\Claim\EqualsTo + * @uses \Lcobucci\JWT\Claim\Basic */ class TokenTest extends \PHPUnit\Framework\TestCase { @@ -128,7 +132,7 @@ public function getHeaderMustReturnTheRequestedHeader() */ public function getHeaderMustReturnValueWhenItIsAReplicatedClaim() { - $token = new Token(['jti' => new EqualsTo('jti', 1)]); + $token = new Token(['jti' => 1]); $this->assertEquals(1, $token->getHeader('jti')); } From a87a9bf9639172258dd63f60c48f259c3fa869ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Mon, 23 Nov 2020 15:10:28 +0100 Subject: [PATCH 10/17] Remove more attribute-based testing --- test/unit/BuilderTest.php | 469 ++++++++++++++++++-------------------- 1 file changed, 227 insertions(+), 242 deletions(-) diff --git a/test/unit/BuilderTest.php b/test/unit/BuilderTest.php index b7dc684c..db7eb5fd 100644 --- a/test/unit/BuilderTest.php +++ b/test/unit/BuilderTest.php @@ -7,7 +7,11 @@ namespace Lcobucci\JWT; +use Lcobucci\JWT\Claim\Basic; +use Lcobucci\JWT\Claim\EqualsTo; use Lcobucci\JWT\Claim\Factory as ClaimFactory; +use Lcobucci\JWT\Claim\GreaterOrEqualsTo; +use Lcobucci\JWT\Claim\LesserOrEqualsTo; use Lcobucci\JWT\Parsing\Encoder; use Lcobucci\JWT\Signer\Key; use Lcobucci\JWT\Token\RegisteredClaimGiven; @@ -16,11 +20,15 @@ * @author Luís Otávio Cobucci Oblonczyk * @since 0.1.0 * + * @coversDefaultClass \Lcobucci\JWT\Builder + * * @covers \Lcobucci\JWT\Token\DataSet * * @uses \Lcobucci\JWT\Claim\Factory * @uses \Lcobucci\JWT\Claim\EqualsTo * @uses \Lcobucci\JWT\Claim\Basic + * @uses \Lcobucci\JWT\Token + * @uses \Lcobucci\JWT\Signer\Key */ class BuilderTest extends \PHPUnit\Framework\TestCase { @@ -29,28 +37,12 @@ class BuilderTest extends \PHPUnit\Framework\TestCase */ protected $encoder; - /** - * @var ClaimFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $claimFactory; - - /** - * @var Claim|\PHPUnit_Framework_MockObject_MockObject - */ - protected $defaultClaim; - /** * {@inheritdoc} */ protected function setUp() { $this->encoder = $this->createMock(Encoder::class); - $this->claimFactory = $this->createMock(ClaimFactory::class); - $this->defaultClaim = $this->createMock(Claim::class); - - $this->claimFactory->expects($this->any()) - ->method('create') - ->willReturn($this->defaultClaim); } /** @@ -58,461 +50,462 @@ protected function setUp() */ private function createBuilder() { - return new Builder($this->encoder, $this->claimFactory); + return new Builder($this->encoder, new ClaimFactory()); } /** * @test * - * @covers Lcobucci\JWT\Builder::__construct - */ - public function constructMustInitializeTheAttributes() - { - $builder = $this->createBuilder(); - - $this->assertAttributeEquals(['alg' => 'none', 'typ' => 'JWT'], 'headers', $builder); - $this->assertAttributeEquals([], 'claims', $builder); - $this->assertAttributeSame($this->encoder, 'encoder', $builder); - $this->assertAttributeSame($this->claimFactory, 'claimFactory', $builder); - } - - /** - * @test - * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::permittedFor + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::permittedFor - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function permittedForMustChangeTheAudClaim() { $builder = $this->createBuilder(); $builder->permittedFor('test'); - $this->assertAttributeEquals(['alg' => 'none', 'typ' => 'JWT'], 'headers', $builder); - $this->assertAttributeEquals(['aud' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); + + self::assertEquals(['typ' => 'JWT', 'alg' => 'none'], $token->getHeaders()); + self::assertEquals(['aud' => new EqualsTo('aud', 'test')], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::permittedFor + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::permittedFor - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function permittedForCanReplicateItemOnHeader() { $builder = $this->createBuilder(); $builder->permittedFor('test', true); - $this->assertAttributeEquals(['aud' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); - $this->assertAttributeEquals( - ['alg' => 'none', 'typ' => 'JWT', 'aud' => $this->defaultClaim], - 'headers', - $builder - ); + self::assertEquals(['typ' => 'JWT', 'alg' => 'none', 'aud' => new EqualsTo('aud', 'test')], $token->getHeaders()); + self::assertEquals(['aud' => new EqualsTo('aud', 'test')], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim - * - * @covers Lcobucci\JWT\Builder::permittedFor - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @covers ::__construct + * @covers ::permittedFor + * @covers ::setRegisteredClaim + * @covers ::configureClaim */ public function permittedForMustKeepAFluentInterface() { $builder = $this->createBuilder(); - $this->assertSame($builder, $builder->permittedFor('test')); + self::assertSame($builder, $builder->permittedFor('test')); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::expiresAt + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::expiresAt - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function expiresAtMustChangeTheExpClaim() { $builder = $this->createBuilder(); $builder->expiresAt('2'); - $this->assertAttributeEquals(['alg' => 'none', 'typ' => 'JWT'], 'headers', $builder); - $this->assertAttributeEquals(['exp' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); + + self::assertEquals(['typ' => 'JWT', 'alg' => 'none'], $token->getHeaders()); + self::assertEquals(['exp' => new GreaterOrEqualsTo('exp', 2)], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::expiresAt + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::expiresAt - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function expiresAtCanReplicateItemOnHeader() { $builder = $this->createBuilder(); $builder->expiresAt('2', true); - $this->assertAttributeEquals(['exp' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); - $this->assertAttributeEquals( - ['alg' => 'none', 'typ' => 'JWT', 'exp' => $this->defaultClaim], - 'headers', - $builder - ); + self::assertEquals(['typ' => 'JWT', 'alg' => 'none', 'exp' => new GreaterOrEqualsTo('exp', 2)], $token->getHeaders()); + self::assertEquals(['exp' => new GreaterOrEqualsTo('exp', 2)], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim - * - * @covers Lcobucci\JWT\Builder::expiresAt - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @covers ::__construct + * @covers ::expiresAt + * @covers ::setRegisteredClaim + * @covers ::configureClaim */ public function expiresAtMustKeepAFluentInterface() { $builder = $this->createBuilder(); - $this->assertSame($builder, $builder->expiresAt('2')); + self::assertSame($builder, $builder->expiresAt('2')); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::identifiedBy + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::identifiedBy - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function identifiedByMustChangeTheJtiClaim() { $builder = $this->createBuilder(); $builder->identifiedBy('2'); - $this->assertAttributeEquals(['alg' => 'none', 'typ' => 'JWT'], 'headers', $builder); - $this->assertAttributeEquals(['jti' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); + + self::assertEquals(['typ' => 'JWT', 'alg' => 'none'], $token->getHeaders()); + self::assertEquals(['jti' => new EqualsTo('jti', 2)], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::identifiedBy + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::identifiedBy - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function identifiedByCanReplicateItemOnHeader() { $builder = $this->createBuilder(); $builder->identifiedBy('2', true); - $this->assertAttributeEquals(['jti' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); - $this->assertAttributeEquals( - ['alg' => 'none', 'typ' => 'JWT', 'jti' => $this->defaultClaim], - 'headers', - $builder - ); + self::assertEquals(['typ' => 'JWT', 'alg' => 'none', 'jti' => new EqualsTo('jti', 2)], $token->getHeaders()); + self::assertEquals(['jti' => new EqualsTo('jti', 2)], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim - * - * @covers Lcobucci\JWT\Builder::identifiedBy - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @covers ::__construct + * @covers ::identifiedBy + * @covers ::setRegisteredClaim + * @covers ::configureClaim */ public function identifiedByMustKeepAFluentInterface() { $builder = $this->createBuilder(); - $this->assertSame($builder, $builder->identifiedBy('2')); + self::assertSame($builder, $builder->identifiedBy('2')); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::issuedAt + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::issuedAt - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function issuedAtMustChangeTheIatClaim() { $builder = $this->createBuilder(); $builder->issuedAt('2'); - $this->assertAttributeEquals(['alg' => 'none', 'typ' => 'JWT'], 'headers', $builder); - $this->assertAttributeEquals(['iat' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); + + self::assertEquals(['typ' => 'JWT', 'alg' => 'none'], $token->getHeaders()); + self::assertEquals(['iat' => new LesserOrEqualsTo('iat', 2)], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::issuedAt + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::issuedAt - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function issuedAtCanReplicateItemOnHeader() { $builder = $this->createBuilder(); $builder->issuedAt('2', true); - $this->assertAttributeEquals(['iat' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); - $this->assertAttributeEquals( - ['alg' => 'none', 'typ' => 'JWT', 'iat' => $this->defaultClaim], - 'headers', - $builder - ); + self::assertEquals(['typ' => 'JWT', 'alg' => 'none', 'iat' => new LesserOrEqualsTo('iat', 2)], $token->getHeaders()); + self::assertEquals(['iat' => new LesserOrEqualsTo('iat', 2)], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim - * - * @covers Lcobucci\JWT\Builder::issuedAt - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @covers ::__construct + * @covers ::issuedAt + * @covers ::setRegisteredClaim + * @covers ::configureClaim */ public function issuedAtMustKeepAFluentInterface() { $builder = $this->createBuilder(); - $this->assertSame($builder, $builder->issuedAt('2')); + self::assertSame($builder, $builder->issuedAt('2')); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::issuedBy + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::issuedBy - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function issuedByMustChangeTheIssClaim() { $builder = $this->createBuilder(); $builder->issuedBy('2'); - $this->assertAttributeEquals(['alg' => 'none', 'typ' => 'JWT'], 'headers', $builder); - $this->assertAttributeEquals(['iss' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); + + self::assertEquals(['typ' => 'JWT', 'alg' => 'none'], $token->getHeaders()); + self::assertEquals(['iss' => new EqualsTo('iss', '2')], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::issuedBy + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::issuedBy - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function issuedByCanReplicateItemOnHeader() { $builder = $this->createBuilder(); $builder->issuedBy('2', true); - $this->assertAttributeEquals(['iss' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); - $this->assertAttributeEquals( - ['alg' => 'none', 'typ' => 'JWT', 'iss' => $this->defaultClaim], - 'headers', - $builder - ); + self::assertEquals(['typ' => 'JWT', 'alg' => 'none', 'iss' => new EqualsTo('iss', '2')], $token->getHeaders()); + self::assertEquals(['iss' => new EqualsTo('iss', '2')], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim - * - * @covers Lcobucci\JWT\Builder::issuedBy - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @covers ::__construct + * @covers ::issuedBy + * @covers ::setRegisteredClaim + * @covers ::configureClaim */ public function issuedByMustKeepAFluentInterface() { $builder = $this->createBuilder(); - $this->assertSame($builder, $builder->issuedBy('2')); + self::assertSame($builder, $builder->issuedBy('2')); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::canOnlyBeUsedAfter + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::canOnlyBeUsedAfter - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function canOnlyBeUsedAfterMustChangeTheNbfClaim() { $builder = $this->createBuilder(); $builder->canOnlyBeUsedAfter('2'); - $this->assertAttributeEquals(['alg' => 'none', 'typ' => 'JWT'], 'headers', $builder); - $this->assertAttributeEquals(['nbf' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); + + self::assertEquals(['typ' => 'JWT', 'alg' => 'none'], $token->getHeaders()); + self::assertEquals(['nbf' => new LesserOrEqualsTo('nbf', 2)], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::canOnlyBeUsedAfter + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::canOnlyBeUsedAfter - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function canOnlyBeUsedAfterCanReplicateItemOnHeader() { $builder = $this->createBuilder(); $builder->canOnlyBeUsedAfter('2', true); - $this->assertAttributeEquals(['nbf' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); - $this->assertAttributeEquals( - ['alg' => 'none', 'typ' => 'JWT', 'nbf' => $this->defaultClaim], - 'headers', - $builder - ); + self::assertEquals(['typ' => 'JWT', 'alg' => 'none', 'nbf' => new LesserOrEqualsTo('nbf', 2)], $token->getHeaders()); + self::assertEquals(['nbf' => new LesserOrEqualsTo('nbf', 2)], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim - * - * @covers Lcobucci\JWT\Builder::canOnlyBeUsedAfter - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @covers ::__construct + * @covers ::canOnlyBeUsedAfter + * @covers ::setRegisteredClaim + * @covers ::configureClaim */ public function canOnlyBeUsedAfterMustKeepAFluentInterface() { $builder = $this->createBuilder(); - $this->assertSame($builder, $builder->canOnlyBeUsedAfter('2')); + self::assertSame($builder, $builder->canOnlyBeUsedAfter('2')); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::relatedTo + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::relatedTo - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function relatedToMustChangeTheSubClaim() { $builder = $this->createBuilder(); $builder->relatedTo('2'); - $this->assertAttributeEquals(['alg' => 'none', 'typ' => 'JWT'], 'headers', $builder); - $this->assertAttributeEquals(['sub' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); + + self::assertEquals(['typ' => 'JWT', 'alg' => 'none'], $token->getHeaders()); + self::assertEquals(['sub' => new EqualsTo('sub', '2')], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::relatedTo + * @covers ::setRegisteredClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::relatedTo - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function relatedToCanReplicateItemOnHeader() { $builder = $this->createBuilder(); $builder->relatedTo('2', true); - $this->assertAttributeEquals(['sub' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); - $this->assertAttributeEquals( - ['alg' => 'none', 'typ' => 'JWT', 'sub' => $this->defaultClaim], - 'headers', - $builder - ); + self::assertEquals(['typ' => 'JWT', 'alg' => 'none', 'sub' => new EqualsTo('sub', '2')], $token->getHeaders()); + self::assertEquals(['sub' => new EqualsTo('sub', '2')], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim - * - * @covers Lcobucci\JWT\Builder::relatedTo - * @covers Lcobucci\JWT\Builder::setRegisteredClaim + * @covers ::__construct + * @covers ::relatedTo + * @covers ::setRegisteredClaim + * @covers ::configureClaim */ public function relatedToMustKeepAFluentInterface() { $builder = $this->createBuilder(); - $this->assertSame($builder, $builder->relatedTo('2')); + self::assertSame($builder, $builder->relatedTo('2')); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct + * @covers ::__construct + * @covers ::withClaim + * @covers ::configureClaim + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::withClaim - * @covers Lcobucci\JWT\Builder::configureClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function withClaimMustConfigureTheGivenClaim() { $builder = $this->createBuilder(); $builder->withClaim('userId', 2); - $this->assertAttributeEquals(['userId' => $this->defaultClaim], 'claims', $builder); + $token = $builder->getToken(); + + self::assertEquals(['typ' => 'JWT', 'alg' => 'none'], $token->getHeaders()); + self::assertEquals(['userId' => new Basic('userId', 2)], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * - * @covers Lcobucci\JWT\Builder::withClaim - * @covers Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::withClaim + * @covers ::configureClaim */ public function withClaimMustKeepAFluentInterface() { $builder = $this->createBuilder(); - $this->assertSame($builder, $builder->withClaim('userId', 2)); + self::assertSame($builder, $builder->withClaim('userId', 2)); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * - * @covers Lcobucci\JWT\Builder::withClaim - * @covers Lcobucci\JWT\Builder::configureClaim + * @covers ::__construct + * @covers ::withClaim * @covers \Lcobucci\JWT\Token\RegisteredClaimGiven */ public function withClaimShouldThrowExceptionWhenTryingToConfigureARegisteredClaim() @@ -525,46 +518,44 @@ public function withClaimShouldThrowExceptionWhenTryingToConfigureARegisteredCla /** * @test * - * @uses Lcobucci\JWT\Builder::__construct + * @covers ::__construct + * @covers ::withHeader + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::withHeader - * @covers Lcobucci\JWT\Builder::configureClaim + * @uses \Lcobucci\JWT\Builder::getToken */ public function withHeaderMustConfigureTheGivenClaim() { $builder = $this->createBuilder(); $builder->withHeader('userId', 2); - $this->assertAttributeEquals( - ['alg' => 'none', 'typ' => 'JWT', 'userId' => 2], - 'headers', - $builder - ); + $token = $builder->getToken(); + + self::assertEquals(['typ' => 'JWT', 'alg' => 'none', 'userId' => 2], $token->getHeaders()); + self::assertEquals([], $token->getClaims()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * - * @covers Lcobucci\JWT\Builder::withHeader + * @covers ::__construct + * @covers ::withHeader */ public function withHeaderMustKeepAFluentInterface() { $builder = $this->createBuilder(); - $this->assertSame($builder, $builder->withHeader('userId', 2)); + self::assertSame($builder, $builder->withHeader('userId', 2)); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::getToken - * @uses Lcobucci\JWT\Token - * @uses Lcobucci\JWT\Signer\Key + * @covers ::__construct + * @covers ::sign + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::sign + * @uses \Lcobucci\JWT\Builder::getToken */ public function signMustConfigureSignerAndKey() { @@ -573,26 +564,22 @@ public function signMustConfigureSignerAndKey() $builder = $this->createBuilder(); $builder->sign($signer, 'test'); - $this->assertAttributeSame($signer, 'signer', $builder); - $this->assertAttributeEquals(new Key('test'), 'key', $builder); + self::assertAttributeSame($signer, 'signer', $builder); + self::assertAttributeEquals(new Key('test'), 'key', $builder); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::getToken - * @uses Lcobucci\JWT\Token - * @uses Lcobucci\JWT\Signer\Key - * - * @covers Lcobucci\JWT\Builder::sign + * @covers ::__construct + * @covers ::sign */ public function signMustKeepAFluentInterface() { $signer = $this->createMock(Signer::class); $builder = $this->createBuilder(); - $this->assertSame($builder, $builder->sign($signer, 'test')); + self::assertSame($builder, $builder->sign($signer, 'test')); return $builder; } @@ -602,14 +589,14 @@ public function signMustKeepAFluentInterface() * * @depends signMustKeepAFluentInterface * - * @covers Lcobucci\JWT\Builder::unsign + * @covers ::unsign */ public function unsignMustRemoveTheSignerAndKey(Builder $builder) { $builder->unsign(); - $this->assertAttributeSame(null, 'signer', $builder); - $this->assertAttributeSame(null, 'key', $builder); + self::assertAttributeSame(null, 'signer', $builder); + self::assertAttributeSame(null, 'key', $builder); } /** @@ -617,24 +604,22 @@ public function unsignMustRemoveTheSignerAndKey(Builder $builder) * * @depends signMustKeepAFluentInterface * - * @covers Lcobucci\JWT\Builder::unsign + * @covers ::unsign */ public function unsignMustKeepAFluentInterface(Builder $builder) { - $this->assertSame($builder, $builder->unsign()); + self::assertSame($builder, $builder->unsign()); } /** * @test * - * @uses Lcobucci\JWT\Builder::__construct - * @uses Lcobucci\JWT\Builder::configureClaim - * @uses Lcobucci\JWT\Builder::createSignature - * @uses Lcobucci\JWT\Builder::withClaim - * @uses Lcobucci\JWT\Token - * @uses Lcobucci\JWT\Signer\Key + * @covers ::getToken + * @covers ::createSignature * - * @covers Lcobucci\JWT\Builder::getToken + * @uses \Lcobucci\JWT\Builder::__construct + * @uses \Lcobucci\JWT\Builder::configureClaim + * @uses \Lcobucci\JWT\Builder::withClaim */ public function getTokenMustReturnANewTokenWithCurrentConfiguration() { @@ -645,7 +630,7 @@ public function getTokenMustReturnANewTokenWithCurrentConfiguration() $this->encoder->expects($this->exactly(2)) ->method('jsonEncode') - ->withConsecutive([['typ'=> 'JWT', 'alg' => 'none']], [['test' => $this->defaultClaim]]) + ->withConsecutive([['typ'=> 'JWT', 'alg' => 'none']], [['test' => new Basic('test', 123)]]) ->willReturnOnConsecutiveCalls('1', '2'); $this->encoder->expects($this->exactly(3)) @@ -656,9 +641,9 @@ public function getTokenMustReturnANewTokenWithCurrentConfiguration() $builder = $this->createBuilder()->withClaim('test', 123); $token = $builder->getToken($signer, new Key('testing')); - $this->assertAttributeEquals(['1', '2', '3'], 'payload', $token); - $this->assertAttributeEquals($token->getHeaders(), 'headers', $builder); - $this->assertAttributeEquals($token->getClaims(), 'claims', $builder); - $this->assertAttributeSame($signature, 'signature', $token); + self::assertEquals(['typ' => 'JWT', 'alg' => 'none'], $token->getHeaders()); + self::assertEquals(['test' => new Basic('test', 123)], $token->getClaims()); + self::assertSame($signature, $token->signature()); + self::assertSame('1.2.3', $token->toString()); } } From 2b280decc631881be64058da23adf9c8511ea9ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Mon, 23 Nov 2020 15:42:54 +0100 Subject: [PATCH 11/17] Don't wrap claim values with claim objects This makes the legacy validation API the only place the requires the deprecated claim objects. --- src/Builder.php | 5 ++-- src/Parser.php | 23 +++-------------- src/Token.php | 32 +++++++++++++++--------- test/unit/BuilderTest.php | 2 +- test/unit/ParserTest.php | 23 ++++++++--------- test/unit/TokenTest.php | 52 ++++++++++++++++++++++----------------- 6 files changed, 66 insertions(+), 71 deletions(-) diff --git a/src/Builder.php b/src/Builder.php index 8a08b556..63bdd065 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -370,8 +370,7 @@ public function with($name, $value) */ private function configureClaim($name, $value) { - $name = (string) $name; - $this->claims[$name] = $this->claimFactory->create($name, $value); + $this->claims[(string) $name] = $value; return $this; } @@ -475,7 +474,7 @@ public function getToken(Signer $signer = null, Key $key = null) $payload[] = $this->encoder->base64UrlEncode($signature); } - return new Token($this->headers, $this->claims, $signature, $payload); + return new Token($this->headers, $this->claims, $signature, $payload, $this->claimFactory); } /** diff --git a/src/Parser.php b/src/Parser.php index e4c0d2f7..bc005ecb 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -29,25 +29,14 @@ class Parser */ private $decoder; - /** - * The claims factory - * - * @var ClaimFactory - */ - private $claimFactory; - /** * Initializes the object * * @param Decoder $decoder - * @param ClaimFactory $claimFactory */ - public function __construct( - Decoder $decoder = null, - ClaimFactory $claimFactory = null - ) { + public function __construct(Decoder $decoder = null) + { $this->decoder = $decoder ?: new Decoder(); - $this->claimFactory = $claimFactory ?: new ClaimFactory(); } /** @@ -133,13 +122,7 @@ protected function parseHeader($data) */ protected function parseClaims($data) { - $claims = (array) $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); - - foreach ($claims as $name => &$value) { - $value = $this->claimFactory->create($name, $value); - } - - return $claims; + return (array) $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); } /** diff --git a/src/Token.php b/src/Token.php index 99dec3b2..e0cd291f 100644 --- a/src/Token.php +++ b/src/Token.php @@ -15,7 +15,6 @@ use Lcobucci\JWT\Claim\Validatable; use Lcobucci\JWT\Signer\Key; use Lcobucci\JWT\Token\DataSet; -use Lcobucci\JWT\Token\RegisteredClaimGiven; use Lcobucci\JWT\Token\RegisteredClaims; use OutOfBoundsException; use function func_num_args; @@ -62,6 +61,13 @@ class Token */ private $payload; + /** + * @internal This serves just as compatibility layer + * + * @var Factory + */ + private $claimFactory; + /** * Initializes the object * @@ -69,17 +75,20 @@ class Token * @param array $claims * @param array $payload * @param Signature|null $signature + * @param Factory|null $claimFactory */ public function __construct( array $headers = ['alg' => 'none'], array $claims = [], Signature $signature = null, - array $payload = ['', ''] + array $payload = ['', ''], + Factory $claimFactory = null ) { $this->headers = new DataSet($headers, $payload[0]); $this->claims = new DataSet($claims, $payload[1]); $this->signature = $signature; $this->payload = $payload; + $this->claimFactory = $claimFactory ?: new Factory(); } /** @return DataSet */ @@ -98,8 +107,7 @@ public function headers() */ public function getHeaders() { - $claimFactory = new Factory(); - $items = []; + $items = []; foreach ($this->headers->all() as $name => $value) { if (! in_array($name, RegisteredClaims::ALL, true) || ! $this->claims->has($name)) { @@ -107,7 +115,7 @@ public function getHeaders() continue; } - $items[$name] = $claimFactory->create($name, $value); + $items[$name] = $this->claimFactory->create($name, $value); } return $items; @@ -174,7 +182,13 @@ public function claims() */ public function getClaims() { - return $this->claims->all(); + $items = []; + + foreach ($this->claims->all() as $name => $value) { + $items[$name] = $this->claimFactory->create($name, $value); + } + + return $items; } /** @@ -219,10 +233,6 @@ public function getClaim($name, $default = null) throw new OutOfBoundsException(sprintf('Requested header "%s" is not configured', $name)); } - if ($value instanceof Claim) { - return $value->getValue(); - } - return $value; } @@ -297,7 +307,7 @@ public function isExpired(DateTimeInterface $now = null) */ private function getValidatableClaims() { - foreach ($this->claims->all() as $claim) { + foreach ($this->getClaims() as $claim) { if ($claim instanceof Validatable) { yield $claim; } diff --git a/test/unit/BuilderTest.php b/test/unit/BuilderTest.php index db7eb5fd..877347f5 100644 --- a/test/unit/BuilderTest.php +++ b/test/unit/BuilderTest.php @@ -630,7 +630,7 @@ public function getTokenMustReturnANewTokenWithCurrentConfiguration() $this->encoder->expects($this->exactly(2)) ->method('jsonEncode') - ->withConsecutive([['typ'=> 'JWT', 'alg' => 'none']], [['test' => new Basic('test', 123)]]) + ->withConsecutive([['typ'=> 'JWT', 'alg' => 'none']], [['test' => 123]]) ->willReturnOnConsecutiveCalls('1', '2'); $this->encoder->expects($this->exactly(3)) diff --git a/test/unit/ParserTest.php b/test/unit/ParserTest.php index a9db7768..d8cf87dd 100644 --- a/test/unit/ParserTest.php +++ b/test/unit/ParserTest.php @@ -8,7 +8,6 @@ namespace Lcobucci\JWT; use Lcobucci\JWT\Claim\EqualsTo; -use Lcobucci\JWT\Claim\Factory as ClaimFactory; use Lcobucci\JWT\Parsing\Decoder; use RuntimeException; @@ -19,10 +18,6 @@ * @covers \Lcobucci\JWT\Token\DataSet * @covers \Lcobucci\JWT\Token\InvalidTokenStructure * @covers \Lcobucci\JWT\Token\UnsupportedHeaderFound - * - * @uses \Lcobucci\JWT\Claim\Factory - * @uses \Lcobucci\JWT\Claim\EqualsTo - * @uses \Lcobucci\JWT\Claim\Basic */ class ParserTest extends \PHPUnit\Framework\TestCase { @@ -31,18 +26,12 @@ class ParserTest extends \PHPUnit\Framework\TestCase */ protected $decoder; - /** - * @var ClaimFactory - */ - protected $claimFactory; - /** * {@inheritdoc} */ protected function setUp() { $this->decoder = $this->createMock(Decoder::class); - $this->claimFactory = new ClaimFactory(); } /** @@ -50,7 +39,7 @@ protected function setUp() */ private function createParser() { - return new Parser($this->decoder, $this->claimFactory); + return new Parser($this->decoder); } /** @@ -63,7 +52,6 @@ public function constructMustConfigureTheAttributes() $parser = $this->createParser(); $this->assertAttributeSame($this->decoder, 'decoder', $parser); - $this->assertAttributeSame($this->claimFactory, 'claimFactory', $parser); } /** @@ -151,6 +139,9 @@ public function parseMustRaiseExceptionWhenHeaderIsFromAnEncryptedToken() * @covers Lcobucci\JWT\Parser::parseHeader * @covers Lcobucci\JWT\Parser::parseClaims * @covers Lcobucci\JWT\Parser::parseSignature + * @covers \Lcobucci\JWT\Claim\Factory + * @covers \Lcobucci\JWT\Claim\Basic + * @covers \Lcobucci\JWT\Claim\EqualsTo * */ public function parseMustReturnANonSignedTokenWhenSignatureIsNotInformed() @@ -182,6 +173,9 @@ public function parseMustReturnANonSignedTokenWhenSignatureIsNotInformed() * @covers Lcobucci\JWT\Parser::parseHeader * @covers Lcobucci\JWT\Parser::parseClaims * @covers Lcobucci\JWT\Parser::parseSignature + * @covers \Lcobucci\JWT\Claim\Factory + * @covers \Lcobucci\JWT\Claim\Basic + * @covers \Lcobucci\JWT\Claim\EqualsTo */ public function parseShouldReplicateClaimValueOnHeaderWhenNeeded() { @@ -217,6 +211,9 @@ public function parseShouldReplicateClaimValueOnHeaderWhenNeeded() * @covers Lcobucci\JWT\Parser::parseHeader * @covers Lcobucci\JWT\Parser::parseClaims * @covers Lcobucci\JWT\Parser::parseSignature + * @covers \Lcobucci\JWT\Claim\Factory + * @covers \Lcobucci\JWT\Claim\Basic + * @covers \Lcobucci\JWT\Claim\EqualsTo */ public function parseMustReturnASignedTokenWhenSignatureIsInformed() { diff --git a/test/unit/TokenTest.php b/test/unit/TokenTest.php index 943f5864..68d888b5 100644 --- a/test/unit/TokenTest.php +++ b/test/unit/TokenTest.php @@ -175,7 +175,7 @@ public function getClaimsMustReturnTheConfiguredClaims() */ public function hasClaimMustReturnTrueWhenItIsConfigured() { - $token = new Token([], ['test' => new Basic('test', 'testing')]); + $token = new Token([], ['test' => 'testing']); $this->assertTrue($token->hasClaim('test')); } @@ -190,7 +190,7 @@ public function hasClaimMustReturnTrueWhenItIsConfigured() */ public function hasClaimMustReturnFalseWhenItIsNotConfigured() { - $token = new Token([], ['test' => new Basic('test', 'testing')]); + $token = new Token([], ['test' => 'testing']); $this->assertFalse($token->hasClaim('testing')); } @@ -206,7 +206,7 @@ public function hasClaimMustReturnFalseWhenItIsNotConfigured() */ public function getClaimMustReturnTheDefaultValueWhenIsNotConfigured() { - $token = new Token([], ['test' => new Basic('test', 'testing')]); + $token = new Token([], ['test' => 'testing']); $this->assertEquals('blah', $token->getClaim('testing', 'blah')); } @@ -255,7 +255,7 @@ public function getClaimShouldReturnNullValueWhenDefaultParameterIsPassed() */ public function getClaimShouldReturnTheClaimValueWhenItExists() { - $token = new Token([], ['testing' => new Basic('testing', 'test')]); + $token = new Token([], ['testing' => 'test']); $this->assertEquals('test', $token->getClaim('testing')); } @@ -337,6 +337,7 @@ public function verifyMustDelegateTheValidationToSignature() * @uses Lcobucci\JWT\ValidationData::setCurrentTime * * @covers Lcobucci\JWT\Token::validate + * @covers Lcobucci\JWT\Token::getClaims * @covers Lcobucci\JWT\Token::getValidatableClaims */ public function validateShouldReturnTrueWhenClaimsAreEmpty() @@ -355,11 +356,12 @@ public function validateShouldReturnTrueWhenClaimsAreEmpty() * @uses Lcobucci\JWT\Claim\Basic::__construct * * @covers Lcobucci\JWT\Token::validate + * @covers Lcobucci\JWT\Token::getClaims * @covers Lcobucci\JWT\Token::getValidatableClaims */ public function validateShouldReturnTrueWhenThereAreNoValidatableClaims() { - $token = new Token([], ['testing' => new Basic('testing', 'test')]); + $token = new Token([], ['testing' => 'test']); $this->assertTrue($token->validate(new ValidationData())); } @@ -373,6 +375,7 @@ public function validateShouldReturnTrueWhenThereAreNoValidatableClaims() * @uses Lcobucci\JWT\Claim\EqualsTo * * @covers Lcobucci\JWT\Token::validate + * @covers Lcobucci\JWT\Token::getClaims * @covers Lcobucci\JWT\Token::getValidatableClaims */ public function validateShouldReturnFalseWhenThereIsAtLeastOneFailedValidatableClaim() @@ -380,8 +383,8 @@ public function validateShouldReturnFalseWhenThereIsAtLeastOneFailedValidatableC $token = new Token( [], [ - 'iss' => new EqualsTo('iss', 'test'), - 'testing' => new Basic('testing', 'test') + 'iss' => 'test', + 'testing' => 'test', ] ); @@ -402,6 +405,7 @@ public function validateShouldReturnFalseWhenThereIsAtLeastOneFailedValidatableC * @uses Lcobucci\JWT\Claim\GreaterOrEqualsTo * * @covers Lcobucci\JWT\Token::validate + * @covers Lcobucci\JWT\Token::getClaims * @covers Lcobucci\JWT\Token::getValidatableClaims */ public function validateShouldReturnFalseWhenATimeBasedClaimFails() @@ -411,11 +415,11 @@ public function validateShouldReturnFalseWhenATimeBasedClaimFails() $token = new Token( [], [ - 'iss' => new EqualsTo('iss', 'test'), - 'iat' => new LesserOrEqualsTo('iat', $now), - 'nbf' => new LesserOrEqualsTo('nbf', $now + 20), - 'exp' => new GreaterOrEqualsTo('exp', $now + 500), - 'testing' => new Basic('testing', 'test') + 'iss' => 'test', + 'iat' => $now, + 'nbf' => $now + 20, + 'exp' => $now + 500, + 'testing' => 'test', ] ); @@ -436,6 +440,7 @@ public function validateShouldReturnFalseWhenATimeBasedClaimFails() * @uses Lcobucci\JWT\Claim\GreaterOrEqualsTo * * @covers Lcobucci\JWT\Token::validate + * @covers Lcobucci\JWT\Token::getClaims * @covers Lcobucci\JWT\Token::getValidatableClaims */ public function validateShouldReturnTrueWhenThereAreNoFailedValidatableClaims() @@ -445,10 +450,10 @@ public function validateShouldReturnTrueWhenThereAreNoFailedValidatableClaims() $token = new Token( [], [ - 'iss' => new EqualsTo('iss', 'test'), - 'iat' => new LesserOrEqualsTo('iat', $now), - 'exp' => new GreaterOrEqualsTo('exp', $now + 500), - 'testing' => new Basic('testing', 'test') + 'iss' => 'test', + 'iat' => $now, + 'exp' => $now + 500, + 'testing' => 'test', ] ); @@ -469,6 +474,7 @@ public function validateShouldReturnTrueWhenThereAreNoFailedValidatableClaims() * @uses Lcobucci\JWT\Claim\GreaterOrEqualsTo * * @covers Lcobucci\JWT\Token::validate + * @covers Lcobucci\JWT\Token::getClaims * @covers Lcobucci\JWT\Token::getValidatableClaims */ public function validateShouldReturnTrueWhenLeewayMakesAllTimeBasedClaimsTrueAndOtherClaimsAreTrue() @@ -478,11 +484,11 @@ public function validateShouldReturnTrueWhenLeewayMakesAllTimeBasedClaimsTrueAnd $token = new Token( [], [ - 'iss' => new EqualsTo('iss', 'test'), - 'iat' => new LesserOrEqualsTo('iat', $now), - 'nbf' => new LesserOrEqualsTo('nbf', $now + 20), - 'exp' => new GreaterOrEqualsTo('exp', $now + 500), - 'testing' => new Basic('testing', 'test') + 'iss' => 'test', + 'iat' => $now, + 'nbf' => $now + 20, + 'exp' => $now + 500, + 'testing' => 'test' ] ); @@ -523,7 +529,7 @@ public function isExpiredShouldReturnFalseWhenTokenIsNotExpired() { $token = new Token( ['alg' => 'none'], - ['exp' => new GreaterOrEqualsTo('exp', time() + 500)] + ['exp' => time() + 500] ); $this->assertFalse($token->isExpired()); @@ -544,7 +550,7 @@ public function isExpiredShouldReturnTrueAfterTokenExpires() { $token = new Token( ['alg' => 'none'], - ['exp' => new GreaterOrEqualsTo('exp', time())] + ['exp' => time()] ); $this->assertTrue($token->isExpired(new DateTime('+10 days'))); From df83ac6b3f28e9750978d535a471eb462021832f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Mon, 23 Nov 2020 16:07:32 +0100 Subject: [PATCH 12/17] Backport the usage of date objects --- src/Builder.php | 59 +++++++++++++++++----- src/Claim/Factory.php | 7 +++ src/Parser.php | 27 +++++++++- src/Token.php | 18 +++---- test/functional/CompatibilityLayerTest.php | 32 ++++++++++++ test/unit/BuilderTest.php | 28 ++++++++++ test/unit/ParserTest.php | 3 ++ test/unit/TokenTest.php | 27 +++++----- 8 files changed, 165 insertions(+), 36 deletions(-) diff --git a/src/Builder.php b/src/Builder.php index 63bdd065..3f2d1e69 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -7,12 +7,15 @@ namespace Lcobucci\JWT; +use DateTimeImmutable; use Lcobucci\JWT\Claim\Factory as ClaimFactory; use Lcobucci\JWT\Parsing\Encoder; use Lcobucci\JWT\Signer\Key; use Lcobucci\JWT\Token\RegisteredClaimGiven; use Lcobucci\JWT\Token\RegisteredClaims; +use function array_key_exists; +use function assert; use function implode; use function in_array; @@ -124,14 +127,28 @@ public function setAudience($audience, $replicateAsHeader = false) /** * Configures the expiration time * - * @param int $expiration + * @param int|DateTimeImmutable $expiration * @param boolean $replicateAsHeader * * @return Builder */ public function expiresAt($expiration, $replicateAsHeader = false) { - return $this->setRegisteredClaim('exp', (int) $expiration, $replicateAsHeader); + return $this->setRegisteredClaim('exp', $this->convertToDate($expiration), $replicateAsHeader); + } + + /** + * @param int|DateTimeImmutable $value + * + * @return DateTimeImmutable + */ + private function convertToDate($value) + { + if (! $value instanceof DateTimeImmutable) { + return new DateTimeImmutable('@' . $value); + } + + return $value; } /** @@ -140,14 +157,14 @@ public function expiresAt($expiration, $replicateAsHeader = false) * @deprecated This method will be removed on v4 * @see Builder::expiresAt() * - * @param int $expiration + * @param int|DateTimeImmutable $expiration * @param boolean $replicateAsHeader * * @return Builder */ public function setExpiration($expiration, $replicateAsHeader = false) { - return $this->setRegisteredClaim('exp', (int) $expiration, $replicateAsHeader); + return $this->expiresAt($expiration, $replicateAsHeader); } /** @@ -182,14 +199,14 @@ public function setId($id, $replicateAsHeader = false) /** * Configures the time that the token was issued * - * @param int $issuedAt + * @param int|DateTimeImmutable $issuedAt * @param boolean $replicateAsHeader * * @return Builder */ public function issuedAt($issuedAt, $replicateAsHeader = false) { - return $this->setRegisteredClaim('iat', (int) $issuedAt, $replicateAsHeader); + return $this->setRegisteredClaim('iat', $this->convertToDate($issuedAt), $replicateAsHeader); } /** @@ -198,7 +215,7 @@ public function issuedAt($issuedAt, $replicateAsHeader = false) * @deprecated This method will be removed on v4 * @see Builder::issuedAt() * - * @param int $issuedAt + * @param int|DateTimeImmutable $issuedAt * @param boolean $replicateAsHeader * * @return Builder @@ -240,14 +257,14 @@ public function setIssuer($issuer, $replicateAsHeader = false) /** * Configures the time before which the token cannot be accepted * - * @param int $notBefore + * @param int|DateTimeImmutable $notBefore * @param boolean $replicateAsHeader * * @return Builder */ public function canOnlyBeUsedAfter($notBefore, $replicateAsHeader = false) { - return $this->setRegisteredClaim('nbf', (int) $notBefore, $replicateAsHeader); + return $this->setRegisteredClaim('nbf', $this->convertToDate($notBefore), $replicateAsHeader); } /** @@ -256,7 +273,7 @@ public function canOnlyBeUsedAfter($notBefore, $replicateAsHeader = false) * @deprecated This method will be removed on v4 * @see Builder::canOnlyBeUsedAfter() * - * @param int $notBefore + * @param int|DateTimeImmutable $notBefore * @param boolean $replicateAsHeader * * @return Builder @@ -464,8 +481,8 @@ public function getToken(Signer $signer = null, Key $key = null) } $payload = [ - $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->headers)), - $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->claims)) + $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->convertDatesToInt($this->headers))), + $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->convertDatesToInt($this->claims))) ]; $signature = $this->createSignature($payload, $signer, $key); @@ -477,6 +494,24 @@ public function getToken(Signer $signer = null, Key $key = null) return new Token($this->headers, $this->claims, $signature, $payload, $this->claimFactory); } + /** + * @param array $items + * + * @return array + */ + private function convertDatesToInt(array $items) + { + foreach (RegisteredClaims::DATE_CLAIMS as $name) { + if (! array_key_exists($name, $items) || ! $items[$name] instanceof DateTimeImmutable) { + continue; + } + + $items[$name] = $items[$name]->getTimestamp(); + } + + return $items; + } + /** * @param string[] $payload * diff --git a/src/Claim/Factory.php b/src/Claim/Factory.php index 6a941057..d34d2a8b 100644 --- a/src/Claim/Factory.php +++ b/src/Claim/Factory.php @@ -7,7 +7,10 @@ namespace Lcobucci\JWT\Claim; +use DateTimeImmutable; use Lcobucci\JWT\Claim; +use Lcobucci\JWT\Token\RegisteredClaims; +use function in_array; /** * Class that create claims @@ -57,6 +60,10 @@ public function __construct(array $callbacks = []) */ public function create($name, $value) { + if ($value instanceof DateTimeImmutable && in_array($name, RegisteredClaims::DATE_CLAIMS, true)) { + $value = $value->getTimestamp(); + } + if (!empty($this->callbacks[$name])) { return call_user_func($this->callbacks[$name], $name, $value); } diff --git a/src/Parser.php b/src/Parser.php index bc005ecb..b3970979 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -7,12 +7,15 @@ namespace Lcobucci\JWT; +use DateTimeImmutable; use InvalidArgumentException; use Lcobucci\JWT\Claim\Factory as ClaimFactory; use Lcobucci\JWT\Parsing\Decoder; use Lcobucci\JWT\Token\InvalidTokenStructure; +use Lcobucci\JWT\Token\RegisteredClaims; use Lcobucci\JWT\Token\UnsupportedHeaderFound; use RuntimeException; +use function array_key_exists; /** * This class parses the JWT strings and convert them into tokens @@ -110,7 +113,7 @@ protected function parseHeader($data) throw UnsupportedHeaderFound::encryption(); } - return $header; + return $this->convertToDateObjects($header); } /** @@ -122,7 +125,27 @@ protected function parseHeader($data) */ protected function parseClaims($data) { - return (array) $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); + $claims = (array) $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); + + return $this->convertToDateObjects($claims); + } + + /** + * @param array $items + * + * @return array + */ + private function convertToDateObjects(array $items) + { + foreach (RegisteredClaims::DATE_CLAIMS as $name) { + if (! array_key_exists($name, $items)) { + continue; + } + + $items[$name] = new DateTimeImmutable('@' . ((int) $items[$name])); + } + + return $items; } /** diff --git a/src/Token.php b/src/Token.php index e0cd291f..47d58ed9 100644 --- a/src/Token.php +++ b/src/Token.php @@ -9,6 +9,7 @@ use BadMethodCallException; use DateTime; +use DateTimeImmutable; use DateTimeInterface; use Generator; use Lcobucci\JWT\Claim\Factory; @@ -233,6 +234,10 @@ public function getClaim($name, $default = null) throw new OutOfBoundsException(sprintf('Requested header "%s" is not configured', $name)); } + if ($value instanceof DateTimeImmutable && in_array($name, RegisteredClaims::DATE_CLAIMS, true)) { + $value = $value->getTimestamp(); + } + return $value; } @@ -280,24 +285,19 @@ public function validate(ValidationData $data) /** * Determine if the token is expired. * - * @param DateTimeInterface $now Defaults to the current time. + * @param DateTimeInterface|null $now Defaults to the current time. * * @return bool */ public function isExpired(DateTimeInterface $now = null) { - $exp = $this->getClaim('exp', false); - - if ($exp === false) { + if (! $this->claims->has('exp')) { return false; } - $now = $now ?: new DateTime(); - - $expiresAt = new DateTime(); - $expiresAt->setTimestamp($exp); + $now = $now ?: new DateTimeImmutable(); - return $now > $expiresAt; + return $now > $this->claims->get(RegisteredClaims::EXPIRATION_TIME); } /** diff --git a/test/functional/CompatibilityLayerTest.php b/test/functional/CompatibilityLayerTest.php index 8b6e52f9..f5edf31f 100644 --- a/test/functional/CompatibilityLayerTest.php +++ b/test/functional/CompatibilityLayerTest.php @@ -2,24 +2,32 @@ namespace Lcobucci\JWT\FunctionalTests; +use DateTimeImmutable; use Lcobucci\JWT\Builder; use Lcobucci\JWT\Keys; +use Lcobucci\JWT\Parser; +use Lcobucci\JWT\Signer\Hmac\Sha256 as HmacSha256; use Lcobucci\JWT\Signer\Key; use Lcobucci\JWT\Signer\Rsa\Sha256; use PHPUnit\Framework\TestCase; use function base64_encode; +use function time; /** * @covers \Lcobucci\JWT\Builder * @covers \Lcobucci\JWT\Claim\Factory * @covers \Lcobucci\JWT\Claim\Basic + * @covers \Lcobucci\JWT\Parser * @covers \Lcobucci\JWT\Parsing\Encoder + * @covers \Lcobucci\JWT\Parsing\Decoder * @covers \Lcobucci\JWT\Signer\Key * @covers \Lcobucci\JWT\Signer\Key\InMemory * @covers \Lcobucci\JWT\Signer\Key\LocalFileReference * @covers \Lcobucci\JWT\Signer\BaseSigner * @covers \Lcobucci\JWT\Signer\OpenSSL + * @covers \Lcobucci\JWT\Signer\Hmac + * @covers \Lcobucci\JWT\Signer\Hmac\Sha256 * @covers \Lcobucci\JWT\Signer\Rsa * @covers \Lcobucci\JWT\Signer\Rsa\Sha256 * @covers \Lcobucci\JWT\Signature @@ -30,6 +38,30 @@ final class CompatibilityLayerTest extends TestCase { use Keys; + /** @test */ + public function registeredDateClaimsShouldBeConvertedToDateObjects() + { + $now = time(); + + $token = (new Builder()) + ->issuedAt($now) + ->canOnlyBeUsedAfter($now + 5) + ->expiresAt($now + 3600) + ->getToken(new HmacSha256(), Key\InMemory::plainText('testing')); + + $expectedNow = new DateTimeImmutable('@' . $now); + + self::assertEquals($expectedNow, $token->claims()->get('iat')); + self::assertEquals($expectedNow->modify('+5 seconds'), $token->claims()->get('nbf')); + self::assertEquals($expectedNow->modify('+1 hour'), $token->claims()->get('exp')); + + $token2 = (new Parser())->parse($token->toString()); + + self::assertEquals($expectedNow, $token2->claims()->get('iat')); + self::assertEquals($expectedNow->modify('+5 seconds'), $token2->claims()->get('nbf')); + self::assertEquals($expectedNow->modify('+1 hour'), $token2->claims()->get('exp')); + } + /** * @test * diff --git a/test/unit/BuilderTest.php b/test/unit/BuilderTest.php index 877347f5..4ea8e0eb 100644 --- a/test/unit/BuilderTest.php +++ b/test/unit/BuilderTest.php @@ -61,6 +61,7 @@ private function createBuilder() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -83,6 +84,7 @@ public function permittedForMustChangeTheAudClaim() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -120,6 +122,8 @@ public function permittedForMustKeepAFluentInterface() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertToDate + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -142,6 +146,9 @@ public function expiresAtMustChangeTheExpClaim() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertToDate + * @covers ::convertToDate + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -163,6 +170,7 @@ public function expiresAtCanReplicateItemOnHeader() * @covers ::expiresAt * @covers ::setRegisteredClaim * @covers ::configureClaim + * @covers ::convertToDate */ public function expiresAtMustKeepAFluentInterface() { @@ -179,6 +187,7 @@ public function expiresAtMustKeepAFluentInterface() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -201,6 +210,7 @@ public function identifiedByMustChangeTheJtiClaim() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -238,6 +248,8 @@ public function identifiedByMustKeepAFluentInterface() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertToDate + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -260,6 +272,8 @@ public function issuedAtMustChangeTheIatClaim() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertToDate + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -281,6 +295,7 @@ public function issuedAtCanReplicateItemOnHeader() * @covers ::issuedAt * @covers ::setRegisteredClaim * @covers ::configureClaim + * @covers ::convertToDate */ public function issuedAtMustKeepAFluentInterface() { @@ -297,6 +312,7 @@ public function issuedAtMustKeepAFluentInterface() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -319,6 +335,7 @@ public function issuedByMustChangeTheIssClaim() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -356,6 +373,8 @@ public function issuedByMustKeepAFluentInterface() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertToDate + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -378,6 +397,8 @@ public function canOnlyBeUsedAfterMustChangeTheNbfClaim() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertToDate + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -399,6 +420,7 @@ public function canOnlyBeUsedAfterCanReplicateItemOnHeader() * @covers ::canOnlyBeUsedAfter * @covers ::setRegisteredClaim * @covers ::configureClaim + * @covers ::convertToDate */ public function canOnlyBeUsedAfterMustKeepAFluentInterface() { @@ -415,6 +437,7 @@ public function canOnlyBeUsedAfterMustKeepAFluentInterface() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -437,6 +460,7 @@ public function relatedToMustChangeTheSubClaim() * @covers ::setRegisteredClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -473,6 +497,7 @@ public function relatedToMustKeepAFluentInterface() * @covers ::withClaim * @covers ::configureClaim * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -521,6 +546,7 @@ public function withClaimShouldThrowExceptionWhenTryingToConfigureARegisteredCla * @covers ::__construct * @covers ::withHeader * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -554,6 +580,7 @@ public function withHeaderMustKeepAFluentInterface() * @covers ::__construct * @covers ::sign * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::getToken */ @@ -616,6 +643,7 @@ public function unsignMustKeepAFluentInterface(Builder $builder) * * @covers ::getToken * @covers ::createSignature + * @covers ::convertDatesToInt * * @uses \Lcobucci\JWT\Builder::__construct * @uses \Lcobucci\JWT\Builder::configureClaim diff --git a/test/unit/ParserTest.php b/test/unit/ParserTest.php index d8cf87dd..08263864 100644 --- a/test/unit/ParserTest.php +++ b/test/unit/ParserTest.php @@ -139,6 +139,7 @@ public function parseMustRaiseExceptionWhenHeaderIsFromAnEncryptedToken() * @covers Lcobucci\JWT\Parser::parseHeader * @covers Lcobucci\JWT\Parser::parseClaims * @covers Lcobucci\JWT\Parser::parseSignature + * @covers Lcobucci\JWT\Parser::convertToDateObjects * @covers \Lcobucci\JWT\Claim\Factory * @covers \Lcobucci\JWT\Claim\Basic * @covers \Lcobucci\JWT\Claim\EqualsTo @@ -173,6 +174,7 @@ public function parseMustReturnANonSignedTokenWhenSignatureIsNotInformed() * @covers Lcobucci\JWT\Parser::parseHeader * @covers Lcobucci\JWT\Parser::parseClaims * @covers Lcobucci\JWT\Parser::parseSignature + * @covers Lcobucci\JWT\Parser::convertToDateObjects * @covers \Lcobucci\JWT\Claim\Factory * @covers \Lcobucci\JWT\Claim\Basic * @covers \Lcobucci\JWT\Claim\EqualsTo @@ -211,6 +213,7 @@ public function parseShouldReplicateClaimValueOnHeaderWhenNeeded() * @covers Lcobucci\JWT\Parser::parseHeader * @covers Lcobucci\JWT\Parser::parseClaims * @covers Lcobucci\JWT\Parser::parseSignature + * @covers Lcobucci\JWT\Parser::convertToDateObjects * @covers \Lcobucci\JWT\Claim\Factory * @covers \Lcobucci\JWT\Claim\Basic * @covers \Lcobucci\JWT\Claim\EqualsTo diff --git a/test/unit/TokenTest.php b/test/unit/TokenTest.php index 68d888b5..b58690f0 100644 --- a/test/unit/TokenTest.php +++ b/test/unit/TokenTest.php @@ -9,6 +9,7 @@ use DateInterval; use DateTime; +use DateTimeImmutable; use Lcobucci\JWT\Claim\Basic; use Lcobucci\JWT\Claim\EqualsTo; use Lcobucci\JWT\Claim\GreaterOrEqualsTo; @@ -410,20 +411,20 @@ public function validateShouldReturnFalseWhenThereIsAtLeastOneFailedValidatableC */ public function validateShouldReturnFalseWhenATimeBasedClaimFails() { - $now = time(); + $now = new DateTimeImmutable(); $token = new Token( [], [ 'iss' => 'test', 'iat' => $now, - 'nbf' => $now + 20, - 'exp' => $now + 500, + 'nbf' => $now->modify('+20 seconds'), + 'exp' => $now->modify('+500 seconds'), 'testing' => 'test', ] ); - $data = new ValidationData($now + 10); + $data = new ValidationData($now->modify('+10 seconds')->getTimestamp()); $data->setIssuer('test'); $this->assertFalse($token->validate($data)); @@ -445,19 +446,19 @@ public function validateShouldReturnFalseWhenATimeBasedClaimFails() */ public function validateShouldReturnTrueWhenThereAreNoFailedValidatableClaims() { - $now = time(); + $now = new DateTimeImmutable(); $token = new Token( [], [ 'iss' => 'test', 'iat' => $now, - 'exp' => $now + 500, + 'exp' => $now->modify('+500 seconds'), 'testing' => 'test', ] ); - $data = new ValidationData($now + 10); + $data = new ValidationData($now->modify('+10 seconds')->getTimestamp()); $data->setIssuer('test'); $this->assertTrue($token->validate($data)); @@ -479,20 +480,20 @@ public function validateShouldReturnTrueWhenThereAreNoFailedValidatableClaims() */ public function validateShouldReturnTrueWhenLeewayMakesAllTimeBasedClaimsTrueAndOtherClaimsAreTrue() { - $now = time(); + $now = new DateTimeImmutable(); $token = new Token( [], [ 'iss' => 'test', 'iat' => $now, - 'nbf' => $now + 20, - 'exp' => $now + 500, + 'nbf' => $now->modify('+20 seconds'), + 'exp' => $now->modify('+500 seconds'), 'testing' => 'test' ] ); - $data = new ValidationData($now + 10, 20); + $data = new ValidationData($now->modify('+10 seconds')->getTimestamp(), 20); $data->setIssuer('test'); $this->assertTrue($token->validate($data)); @@ -529,7 +530,7 @@ public function isExpiredShouldReturnFalseWhenTokenIsNotExpired() { $token = new Token( ['alg' => 'none'], - ['exp' => time() + 500] + ['exp' => new DateTimeImmutable('+500 seconds')] ); $this->assertFalse($token->isExpired()); @@ -550,7 +551,7 @@ public function isExpiredShouldReturnTrueAfterTokenExpires() { $token = new Token( ['alg' => 'none'], - ['exp' => time()] + ['exp' => new DateTimeImmutable()] ); $this->assertTrue($token->isExpired(new DateTime('+10 days'))); From 60324a241e31322ff8f706244e19996175672af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Mon, 23 Nov 2020 16:20:22 +0100 Subject: [PATCH 13/17] Add support methods for new validation API --- src/Token.php | 60 +++++++ test/unit/TokenTest.php | 369 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 425 insertions(+), 4 deletions(-) diff --git a/src/Token.php b/src/Token.php index 47d58ed9..d377c0aa 100644 --- a/src/Token.php +++ b/src/Token.php @@ -300,6 +300,66 @@ public function isExpired(DateTimeInterface $now = null) return $now > $this->claims->get(RegisteredClaims::EXPIRATION_TIME); } + /** + * @param string $audience + * + * @return bool + */ + public function isPermittedFor($audience) + { + return $this->claims->get(RegisteredClaims::AUDIENCE) === $audience; + } + + /** + * @param string $id + * + * @return bool + */ + public function isIdentifiedBy($id) + { + return $this->claims->get(RegisteredClaims::ID) === $id; + } + + /** + * @param string $subject + * + * @return bool + */ + public function isRelatedTo($subject) + { + return $this->claims->get(RegisteredClaims::SUBJECT) === $subject; + } + + /** + * @param list $issuers + * + * @return bool + */ + public function hasBeenIssuedBy(...$issuers) + { + return in_array($this->claims->get(RegisteredClaims::ISSUER), $issuers, true); + } + + /** + * @param DateTimeInterface $now + * + * @return bool + */ + public function hasBeenIssuedBefore(DateTimeInterface $now) + { + return $now >= $this->claims->get(RegisteredClaims::ISSUED_AT); + } + + /** + * @param DateTimeInterface $now + * + * @return bool + */ + public function isMinimumTimeBefore(DateTimeInterface $now) + { + return $now >= $this->claims->get(RegisteredClaims::NOT_BEFORE); + } + /** * Yields the validatable claims * diff --git a/test/unit/TokenTest.php b/test/unit/TokenTest.php index b58690f0..d97e227f 100644 --- a/test/unit/TokenTest.php +++ b/test/unit/TokenTest.php @@ -7,18 +7,17 @@ namespace Lcobucci\JWT; -use DateInterval; use DateTime; use DateTimeImmutable; use Lcobucci\JWT\Claim\Basic; -use Lcobucci\JWT\Claim\EqualsTo; -use Lcobucci\JWT\Claim\GreaterOrEqualsTo; -use Lcobucci\JWT\Claim\LesserOrEqualsTo; +use Lcobucci\JWT\Token\RegisteredClaims; /** * @author Luís Otávio Cobucci Oblonczyk * @since 0.1.0 * + * @coversDefaultClass \Lcobucci\JWT\Token + * * @covers \Lcobucci\JWT\Token\DataSet * * @uses \Lcobucci\JWT\Claim\Factory @@ -499,6 +498,368 @@ public function validateShouldReturnTrueWhenLeewayMakesAllTimeBasedClaimsTrueAnd $this->assertTrue($token->validate($data)); } + /** + * @test + * + * @covers ::isPermittedFor + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isPermittedForShouldReturnFalseWhenNoAudienceIsConfigured() + { + $token = new Token(); + + self::assertFalse($token->isPermittedFor('testing')); + } + + /** + * @test + * + * @covers ::isPermittedFor + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isPermittedForShouldReturnFalseWhenAudienceDoesNotMatch() + { + $token = new Token( + [], + [RegisteredClaims::AUDIENCE => 'test'] + ); + + self::assertFalse($token->isPermittedFor('testing')); + } + + /** + * @test + * + * @covers ::isPermittedFor + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isPermittedForShouldReturnFalseWhenAudienceTypeDoesNotMatch() + { + $token = new Token( + [], + [RegisteredClaims::AUDIENCE => 10] + ); + + self::assertFalse($token->isPermittedFor('10')); + } + + /** + * @test + * + * @covers ::isPermittedFor + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isPermittedForShouldReturnTrueWhenAudienceMatches() + { + $token = new Token( + [], + [RegisteredClaims::AUDIENCE => 'testing'] + ); + + self::assertTrue($token->isPermittedFor('testing')); + } + + /** + * @test + * + * @covers ::isIdentifiedBy + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isIdentifiedByShouldReturnFalseWhenNoIdWasConfigured() + { + $token = new Token(); + + self::assertFalse($token->isIdentifiedBy('test')); + } + + /** + * @test + * + * @covers ::isIdentifiedBy + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isIdentifiedByShouldReturnFalseWhenIdDoesNotMatch() + { + $token = new Token( + [], + [RegisteredClaims::ID => 'testing'] + ); + + self::assertFalse($token->isIdentifiedBy('test')); + } + + /** + * @test + * + * @covers ::isIdentifiedBy + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isIdentifiedByShouldReturnTrueWhenIdMatches() + { + $token = new Token( + [], + [RegisteredClaims::ID => 'test'] + ); + + self::assertTrue($token->isIdentifiedBy('test')); + } + + /** + * @test + * + * @covers ::isRelatedTo + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isRelatedToShouldReturnFalseWhenNoSubjectWasConfigured() + { + $token = new Token(); + + self::assertFalse($token->isRelatedTo('test')); + } + + /** + * @test + * + * @covers ::isRelatedTo + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isRelatedToShouldReturnFalseWhenSubjectDoesNotMatch() + { + $token = new Token( + [], + [RegisteredClaims::SUBJECT => 'testing'] + ); + + self::assertFalse($token->isRelatedTo('test')); + } + + /** + * @test + * + * @covers ::isRelatedTo + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isRelatedToShouldReturnTrueWhenSubjectMatches() + { + $token = new Token( + [], + [RegisteredClaims::SUBJECT => 'test'] + ); + + self::assertTrue($token->isRelatedTo('test')); + } + + /** + * @test + * + * @covers ::hasBeenIssuedBy + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function hasBeenIssuedByShouldReturnFalseWhenIssuerIsNotConfigured() + { + $token = new Token(); + + self::assertFalse($token->hasBeenIssuedBy('test')); + } + + /** + * @test + * + * @covers ::hasBeenIssuedBy + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function hasBeenIssuedByShouldReturnFalseWhenIssuerTypeDoesNotMatches() + { + $token = new Token( + [], + [RegisteredClaims::ISSUER => 10] + ); + + self::assertFalse($token->hasBeenIssuedBy('10')); + } + + /** + * @test + * + * @covers ::hasBeenIssuedBy + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function hasBeenIssuedByShouldReturnFalseWhenIssuerIsNotInTheGivenList() + { + $token = new Token( + [], + [RegisteredClaims::ISSUER => 'test'] + ); + + self::assertFalse($token->hasBeenIssuedBy('testing1', 'testing2')); + } + + /** + * @test + * + * @covers ::hasBeenIssuedBy + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function hasBeenIssuedByShouldReturnTrueWhenIssuerIsInTheGivenList() + { + $token = new Token( + [], + [RegisteredClaims::ISSUER => 'test'] + ); + + self::assertTrue($token->hasBeenIssuedBy('testing1', 'testing2', 'test')); + } + + /** + * @test + * + * @covers ::hasBeenIssuedBefore + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function hasBeenIssuedBeforeShouldReturnTrueWhenIssueTimeIsNotConfigured() + { + $token = new Token(); + + self::assertTrue($token->hasBeenIssuedBefore(new DateTimeImmutable())); + } + + /** + * @test + * + * @covers ::hasBeenIssuedBefore + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function hasBeenIssuedBeforeShouldReturnTrueWhenIssueTimeIsBeforeThanNow() + { + $now = new DateTimeImmutable(); + $token = new Token( + [], + [RegisteredClaims::ISSUED_AT => $now->modify('-100 seconds')] + ); + + self::assertTrue($token->hasBeenIssuedBefore($now)); + } + + /** + * @test + * + * @covers ::hasBeenIssuedBefore + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function hasBeenIssuedBeforeShouldReturnTrueWhenIssueTimeIsEqualsToNow() + { + $now = new DateTimeImmutable(); + $token = new Token( + [], + [RegisteredClaims::ISSUED_AT => $now] + ); + + self::assertTrue($token->hasBeenIssuedBefore($now)); + } + + /** + * @test + * + * @covers ::hasBeenIssuedBefore + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function hasBeenIssuedBeforeShouldReturnFalseWhenIssueTimeIsGreaterThanNow() + { + $now = new DateTimeImmutable(); + $token = new Token( + [], + [RegisteredClaims::ISSUED_AT => $now->modify('+100 seconds')] + ); + + self::assertFalse($token->hasBeenIssuedBefore($now)); + } + + /** + * @test + * + * @covers ::isMinimumTimeBefore + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isMinimumTimeBeforeShouldReturnTrueWhenIssueTimeIsNotConfigured() + { + $token = new Token(); + + self::assertTrue($token->isMinimumTimeBefore(new DateTimeImmutable())); + } + + /** + * @test + * + * @covers ::isMinimumTimeBefore + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isMinimumTimeBeforeShouldReturnTrueWhenNotBeforeClaimIsBeforeThanNow() + { + $now = new DateTimeImmutable(); + $token = new Token( + [], + [RegisteredClaims::NOT_BEFORE => $now->modify('-100 seconds')] + ); + + self::assertTrue($token->isMinimumTimeBefore($now)); + } + + /** + * @test + * + * @covers ::isMinimumTimeBefore + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isMinimumTimeBeforeShouldReturnTrueWhenNotBeforeClaimIsEqualsToNow() + { + $now = new DateTimeImmutable(); + $token = new Token( + [], + [RegisteredClaims::NOT_BEFORE => $now] + ); + + self::assertTrue($token->isMinimumTimeBefore($now)); + } + + /** + * @test + * + * @covers ::isMinimumTimeBefore + * + * @uses \Lcobucci\JWT\Token::__construct + */ + public function isMinimumTimeBeforeShouldReturnFalseWhenNotBeforeClaimIsGreaterThanNow() + { + $now = new DateTimeImmutable(); + $token = new Token( + [], + [RegisteredClaims::NOT_BEFORE => $now->modify('100 seconds')] + ); + + self::assertFalse($token->isMinimumTimeBefore($now)); + } + /** * @test * From e0818f0b80d7e7d9397f0fcddf68b7d647896ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Mon, 23 Nov 2020 16:34:41 +0100 Subject: [PATCH 14/17] Add polyfill for lcobucci/clock The minimum supported PHP version there is 7.1, so we have to do this for PHP 5.6 and 7.0. --- compat/lcobucci-clock-polyfill.php | 70 ++++++++++++++++++++++++++++++ composer.json | 6 ++- 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 compat/lcobucci-clock-polyfill.php diff --git a/compat/lcobucci-clock-polyfill.php b/compat/lcobucci-clock-polyfill.php new file mode 100644 index 00000000..ad932cfd --- /dev/null +++ b/compat/lcobucci-clock-polyfill.php @@ -0,0 +1,70 @@ +now = $now; + } + + /** @return self */ + public static function fromUTC() + { + return new self(new DateTimeImmutable('now', new DateTimeZone('UTC'))); + } + + public function setTo(DateTimeImmutable $now) + { + $this->now = $now; + } + + public function now() + { + return $this->now; + } + } + + final class SystemClock implements Clock + { + /** @var DateTimeZone */ + private $timezone; + + public function __construct(DateTimeZone $timezone) + { + $this->timezone = $timezone; + } + + /** @return self */ + public static function fromUTC() + { + return new self(new DateTimeZone('UTC')); + } + + /** @return self */ + public static function fromSystemTimezone() + { + return new self(new DateTimeZone(date_default_timezone_get())); + } + + public function now() + { + return new DateTimeImmutable('now', $this->timezone); + } + } +} diff --git a/composer.json b/composer.json index 666c3e45..8482ad6f 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,8 @@ "Lcobucci\\JWT\\": "src" }, "files": [ - "compat/json-exception-polyfill.php" + "compat/json-exception-polyfill.php", + "compat/lcobucci-clock-polyfill.php" ] }, "autoload-dev": { @@ -44,6 +45,9 @@ ] } }, + "suggest": { + "lcobucci/clock": "*" + }, "extra": { "branch-alias": { "dev-master": "3.1-dev" From e860c426f498e2b0c956f53686bd58c0667e6847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Mon, 23 Nov 2020 16:55:19 +0100 Subject: [PATCH 15/17] Backport new validation API --- src/Token.php | 6 + src/Validation/Constraint.php | 11 + src/Validation/Constraint/IdentifiedBy.php | 28 +++ src/Validation/Constraint/IssuedBy.php | 28 +++ .../Constraint/LeewayCannotBeNegative.php | 15 ++ src/Validation/Constraint/PermittedFor.php | 27 +++ src/Validation/Constraint/RelatedTo.php | 27 +++ src/Validation/Constraint/SignedWith.php | 34 +++ src/Validation/Constraint/ValidAt.php | 72 ++++++ src/Validation/ConstraintViolation.php | 10 + src/Validation/NoConstraintsGiven.php | 10 + .../RequiredConstraintsViolated.php | 53 +++++ src/Validation/Validator.php | 55 +++++ src/Validator.php | 23 ++ .../Constraint/ConstraintTestCase.php | 29 +++ .../Constraint/IdentifiedByTest.php | 63 ++++++ .../Validation/Constraint/IssuedByTest.php | 77 +++++++ .../Constraint/PermittedForTest.php | 77 +++++++ .../Validation/Constraint/RelatedToTest.php | 62 ++++++ .../Validation/Constraint/SignedWithTest.php | 103 +++++++++ .../Validation/Constraint/ValidAtTest.php | 209 ++++++++++++++++++ .../RequiredConstraintsViolatedTest.php | 29 +++ test/unit/Validation/ValidatorTest.php | 139 ++++++++++++ 23 files changed, 1187 insertions(+) create mode 100644 src/Validation/Constraint.php create mode 100644 src/Validation/Constraint/IdentifiedBy.php create mode 100644 src/Validation/Constraint/IssuedBy.php create mode 100644 src/Validation/Constraint/LeewayCannotBeNegative.php create mode 100644 src/Validation/Constraint/PermittedFor.php create mode 100644 src/Validation/Constraint/RelatedTo.php create mode 100644 src/Validation/Constraint/SignedWith.php create mode 100644 src/Validation/Constraint/ValidAt.php create mode 100644 src/Validation/ConstraintViolation.php create mode 100644 src/Validation/NoConstraintsGiven.php create mode 100644 src/Validation/RequiredConstraintsViolated.php create mode 100644 src/Validation/Validator.php create mode 100644 src/Validator.php create mode 100644 test/unit/Validation/Constraint/ConstraintTestCase.php create mode 100644 test/unit/Validation/Constraint/IdentifiedByTest.php create mode 100644 test/unit/Validation/Constraint/IssuedByTest.php create mode 100644 test/unit/Validation/Constraint/PermittedForTest.php create mode 100644 test/unit/Validation/Constraint/RelatedToTest.php create mode 100644 test/unit/Validation/Constraint/SignedWithTest.php create mode 100644 test/unit/Validation/Constraint/ValidAtTest.php create mode 100644 test/unit/Validation/RequiredConstraintsViolatedTest.php create mode 100644 test/unit/Validation/ValidatorTest.php diff --git a/src/Token.php b/src/Token.php index d377c0aa..704d094b 100644 --- a/src/Token.php +++ b/src/Token.php @@ -244,6 +244,9 @@ public function getClaim($name, $default = null) /** * Verify if the key matches with the one that created the signature * + * @deprecated This method has been removed from the interface in v4.0 + * @see \Lcobucci\JWT\Validation\Validator + * * @param Signer $signer * @param Key|string $key * @@ -267,6 +270,9 @@ public function verify(Signer $signer, $key) /** * Validates if the token is valid * + * @deprecated This method has been removed from the interface in v4.0 + * @see \Lcobucci\JWT\Validation\Validator + * * @param ValidationData $data * * @return boolean diff --git a/src/Validation/Constraint.php b/src/Validation/Constraint.php new file mode 100644 index 00000000..c488ccd9 --- /dev/null +++ b/src/Validation/Constraint.php @@ -0,0 +1,11 @@ +id = $id; + } + + public function assert(Token $token) + { + if (! $token->isIdentifiedBy($this->id)) { + throw new ConstraintViolation( + 'The token is not identified with the expected ID' + ); + } + } +} diff --git a/src/Validation/Constraint/IssuedBy.php b/src/Validation/Constraint/IssuedBy.php new file mode 100644 index 00000000..84219349 --- /dev/null +++ b/src/Validation/Constraint/IssuedBy.php @@ -0,0 +1,28 @@ + $issuers */ + public function __construct(...$issuers) + { + $this->issuers = $issuers; + } + + public function assert(Token $token) + { + if (! $token->hasBeenIssuedBy(...$this->issuers)) { + throw new ConstraintViolation( + 'The token was not issued by the given issuers' + ); + } + } +} diff --git a/src/Validation/Constraint/LeewayCannotBeNegative.php b/src/Validation/Constraint/LeewayCannotBeNegative.php new file mode 100644 index 00000000..0e20b154 --- /dev/null +++ b/src/Validation/Constraint/LeewayCannotBeNegative.php @@ -0,0 +1,15 @@ +audience = $audience; + } + + public function assert(Token $token) + { + if (! $token->isPermittedFor($this->audience)) { + throw new ConstraintViolation( + 'The token is not allowed to be used by this audience' + ); + } + } +} diff --git a/src/Validation/Constraint/RelatedTo.php b/src/Validation/Constraint/RelatedTo.php new file mode 100644 index 00000000..61782186 --- /dev/null +++ b/src/Validation/Constraint/RelatedTo.php @@ -0,0 +1,27 @@ +subject = $subject; + } + + public function assert(Token $token) + { + if (! $token->isRelatedTo($this->subject)) { + throw new ConstraintViolation( + 'The token is not related to the expected subject' + ); + } + } +} diff --git a/src/Validation/Constraint/SignedWith.php b/src/Validation/Constraint/SignedWith.php new file mode 100644 index 00000000..d3675a72 --- /dev/null +++ b/src/Validation/Constraint/SignedWith.php @@ -0,0 +1,34 @@ +signer = $signer; + $this->key = $key; + } + + public function assert(Token $token) + { + if ($token->headers()->get('alg') !== $this->signer->getAlgorithmId()) { + throw new ConstraintViolation('Token signer mismatch'); + } + + if (! $this->signer->verify((string) $token->signature(), $token->getPayload(), $this->key)) { + throw new ConstraintViolation('Token signature mismatch'); + } + } +} diff --git a/src/Validation/Constraint/ValidAt.php b/src/Validation/Constraint/ValidAt.php new file mode 100644 index 00000000..3190af3e --- /dev/null +++ b/src/Validation/Constraint/ValidAt.php @@ -0,0 +1,72 @@ +clock = $clock; + $this->leeway = $this->guardLeeway($leeway); + } + + /** @return DateInterval */ + private function guardLeeway(DateInterval $leeway = null) + { + if ($leeway === null) { + return new DateInterval('PT0S'); + } + + if ($leeway->invert === 1) { + throw LeewayCannotBeNegative::create(); + } + + return $leeway; + } + + public function assert(Token $token) + { + $now = $this->clock->now(); + + $this->assertIssueTime($token, $now->add($this->leeway)); + $this->assertMinimumTime($token, $now->add($this->leeway)); + $this->assertExpiration($token, $now->sub($this->leeway)); + } + + /** @throws ConstraintViolation */ + private function assertExpiration(Token $token, DateTimeInterface $now) + { + if ($token->isExpired($now)) { + throw new ConstraintViolation('The token is expired'); + } + } + + /** @throws ConstraintViolation */ + private function assertMinimumTime(Token $token, DateTimeInterface $now) + { + if (! $token->isMinimumTimeBefore($now)) { + throw new ConstraintViolation('The token cannot be used yet'); + } + } + + /** @throws ConstraintViolation */ + private function assertIssueTime(Token $token, DateTimeInterface $now) + { + if (! $token->hasBeenIssuedBefore($now)) { + throw new ConstraintViolation('The token was issued in the future'); + } + } +} diff --git a/src/Validation/ConstraintViolation.php b/src/Validation/ConstraintViolation.php new file mode 100644 index 00000000..bc50c6bd --- /dev/null +++ b/src/Validation/ConstraintViolation.php @@ -0,0 +1,10 @@ +violations = $violations; + + return $exception; + } + + /** + * @param ConstraintViolation[] $violations + * + * @return string + */ + private static function buildMessage(array $violations) + { + $violations = array_map( + static function (ConstraintViolation $violation) { + return '- ' . $violation->getMessage(); + }, + $violations + ); + + $message = "The token violates some mandatory constraints, details:\n"; + $message .= implode("\n", $violations); + + return $message; + } + + /** @return ConstraintViolation[] */ + public function violations() + { + return $this->violations; + } +} diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php new file mode 100644 index 00000000..752f6c1b --- /dev/null +++ b/src/Validation/Validator.php @@ -0,0 +1,55 @@ +checkConstraint($constraint, $token, $violations); + } + + if ($violations) { + throw RequiredConstraintsViolated::fromViolations(...$violations); + } + } + + /** @param ConstraintViolation[] $violations */ + private function checkConstraint( + Constraint $constraint, + Token $token, + array &$violations + ) { + try { + $constraint->assert($token); + } catch (ConstraintViolation $e) { + $violations[] = $e; + } + } + + public function validate(Token $token, Constraint ...$constraints) + { + if ($constraints === []) { + throw new NoConstraintsGiven('No constraint given.'); + } + + try { + foreach ($constraints as $constraint) { + $constraint->assert($token); + } + + return true; + } catch (ConstraintViolation $e) { + return false; + } + } +} diff --git a/src/Validator.php b/src/Validator.php new file mode 100644 index 00000000..689a6243 --- /dev/null +++ b/src/Validator.php @@ -0,0 +1,23 @@ +expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token is not identified with the expected ID'); + + $constraint = new IdentifiedBy('123456'); + $constraint->assert($this->buildToken()); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldRaiseExceptionWhenIdDoesNotMatch() + { + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token is not identified with the expected ID'); + + $constraint = new IdentifiedBy('123456'); + $constraint->assert($this->buildToken([RegisteredClaims::ID => 15])); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldNotRaiseExceptionWhenIdMatches() + { + $token = $this->buildToken([RegisteredClaims::ID => '123456']); + + $constraint = new IdentifiedBy('123456'); + + $constraint->assert($token); + $this->addToAssertionCount(1); + } +} diff --git a/test/unit/Validation/Constraint/IssuedByTest.php b/test/unit/Validation/Constraint/IssuedByTest.php new file mode 100644 index 00000000..cc3b5eff --- /dev/null +++ b/test/unit/Validation/Constraint/IssuedByTest.php @@ -0,0 +1,77 @@ +expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token was not issued by the given issuers'); + + $constraint = new IssuedBy('test.com', 'test.net'); + $constraint->assert($this->buildToken()); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldRaiseExceptionWhenIssuerValueDoesNotMatch() + { + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token was not issued by the given issuers'); + + $constraint = new IssuedBy('test.com', 'test.net'); + $constraint->assert($this->buildToken([RegisteredClaims::ISSUER => 'example.com'])); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldRaiseExceptionWhenIssuerTypeValueDoesNotMatch() + { + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token was not issued by the given issuers'); + + $constraint = new IssuedBy('test.com', '123'); + $constraint->assert($this->buildToken([RegisteredClaims::ISSUER => 123])); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldNotRaiseExceptionWhenIssuerMatches() + { + $token = $this->buildToken([RegisteredClaims::ISSUER => 'test.com']); + $constraint = new IssuedBy('test.com', 'test.net'); + + $constraint->assert($token); + $this->addToAssertionCount(1); + } +} diff --git a/test/unit/Validation/Constraint/PermittedForTest.php b/test/unit/Validation/Constraint/PermittedForTest.php new file mode 100644 index 00000000..e4cb1ab3 --- /dev/null +++ b/test/unit/Validation/Constraint/PermittedForTest.php @@ -0,0 +1,77 @@ +expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token is not allowed to be used by this audience'); + + $constraint = new PermittedFor('test.com'); + $constraint->assert($this->buildToken()); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldRaiseExceptionWhenAudienceValueDoesNotMatch() + { + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token is not allowed to be used by this audience'); + + $constraint = new PermittedFor('test.com'); + $constraint->assert($this->buildToken([RegisteredClaims::AUDIENCE => 'aa.com'])); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldRaiseExceptionWhenAudienceTypeDoesNotMatch() + { + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token is not allowed to be used by this audience'); + + $constraint = new PermittedFor('123'); + $constraint->assert($this->buildToken([RegisteredClaims::AUDIENCE => 123])); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldNotRaiseExceptionWhenAudienceMatches() + { + $token = $this->buildToken([RegisteredClaims::AUDIENCE => 'test.com']); + $constraint = new PermittedFor('test.com'); + + $constraint->assert($token); + $this->addToAssertionCount(1); + } +} diff --git a/test/unit/Validation/Constraint/RelatedToTest.php b/test/unit/Validation/Constraint/RelatedToTest.php new file mode 100644 index 00000000..b1b2b5c8 --- /dev/null +++ b/test/unit/Validation/Constraint/RelatedToTest.php @@ -0,0 +1,62 @@ +expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token is not related to the expected subject'); + + $constraint = new RelatedTo('user-auth'); + $constraint->assert($this->buildToken()); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldRaiseExceptionWhenSubjectDoesNotMatch() + { + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token is not related to the expected subject'); + + $constraint = new RelatedTo('user-auth'); + $constraint->assert($this->buildToken([RegisteredClaims::SUBJECT => 'password-recovery'])); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldNotRaiseExceptionWhenSubjectMatches() + { + $token = $this->buildToken([RegisteredClaims::SUBJECT => 'user-auth']); + $constraint = new RelatedTo('user-auth'); + + $constraint->assert($token); + $this->addToAssertionCount(1); + } +} diff --git a/test/unit/Validation/Constraint/SignedWithTest.php b/test/unit/Validation/Constraint/SignedWithTest.php new file mode 100644 index 00000000..8bae67b1 --- /dev/null +++ b/test/unit/Validation/Constraint/SignedWithTest.php @@ -0,0 +1,103 @@ +signer = $this->createMock(Signer::class); + $this->signer->method('getAlgorithmId')->willReturn('RS256'); + + $this->key = Signer\Key\InMemory::plainText('123'); + $this->signature = new Signature('1234'); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldRaiseExceptionWhenSignerIsNotTheSame() + { + $token = $this->buildToken([], ['alg' => 'test'], $this->signature); + + $this->signer->expects(self::never())->method('verify'); + + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('Token signer mismatch'); + + $constraint = new SignedWith($this->signer, $this->key); + $constraint->assert($token); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldRaiseExceptionWhenSignatureIsInvalid() + { + $token = $this->buildToken([], ['alg' => 'RS256'], $this->signature); + + $this->signer->expects(self::once()) + ->method('verify') + ->with((string) $this->signature, $token->getPayload(), $this->key) + ->willReturn(false); + + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('Token signature mismatch'); + + $constraint = new SignedWith($this->signer, $this->key); + $constraint->assert($token); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::assert + */ + public function assertShouldRaiseExceptionWhenSignatureIsValid() + { + $token = $this->buildToken([], ['alg' => 'RS256'], $this->signature); + + $this->signer->expects(self::once()) + ->method('verify') + ->with((string) $this->signature, $token->getPayload(), $this->key) + ->willReturn(true); + + $constraint = new SignedWith($this->signer, $this->key); + + $constraint->assert($token); + $this->addToAssertionCount(1); + } +} diff --git a/test/unit/Validation/Constraint/ValidAtTest.php b/test/unit/Validation/Constraint/ValidAtTest.php new file mode 100644 index 00000000..8ba6fc46 --- /dev/null +++ b/test/unit/Validation/Constraint/ValidAtTest.php @@ -0,0 +1,209 @@ +clock = new FrozenClock(new DateTimeImmutable()); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::guardLeeway + * @covers \Lcobucci\JWT\Validation\Constraint\LeewayCannotBeNegative + */ + public function constructShouldRaiseExceptionOnNegativeLeeway() + { + $leeway = new DateInterval('PT30S'); + $leeway->invert = 1; + + $this->expectException(LeewayCannotBeNegative::class); + $this->expectExceptionMessage('Leeway cannot be negative'); + + new ValidAt($this->clock, $leeway); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::guardLeeway + * @covers ::assert + * @covers ::assertExpiration + * @covers ::assertIssueTime + * @covers ::assertMinimumTime + */ + public function assertShouldRaiseExceptionWhenTokenIsExpired() + { + $now = $this->clock->now(); + + $claims = [ + RegisteredClaims::ISSUED_AT => $now->modify('-20 seconds'), + RegisteredClaims::NOT_BEFORE => $now->modify('-10 seconds'), + RegisteredClaims::EXPIRATION_TIME => $now->modify('-10 seconds'), + ]; + + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token is expired'); + + $constraint = new ValidAt($this->clock); + $constraint->assert($this->buildToken($claims)); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::guardLeeway + * @covers ::assert + * @covers ::assertIssueTime + * @covers ::assertMinimumTime + */ + public function assertShouldRaiseExceptionWhenMinimumTimeIsNotMet() + { + $now = $this->clock->now(); + + $claims = [ + RegisteredClaims::ISSUED_AT => $now->modify('-20 seconds'), + RegisteredClaims::NOT_BEFORE => $now->modify('+40 seconds'), + RegisteredClaims::EXPIRATION_TIME => $now->modify('+60 seconds'), + ]; + + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token cannot be used yet'); + + $constraint = new ValidAt($this->clock); + $constraint->assert($this->buildToken($claims)); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::guardLeeway + * @covers ::assert + * @covers ::assertIssueTime + */ + public function assertShouldRaiseExceptionWhenTokenWasIssuedInTheFuture() + { + $now = $this->clock->now(); + + $claims = [ + RegisteredClaims::ISSUED_AT => $now->modify('+20 seconds'), + RegisteredClaims::NOT_BEFORE => $now->modify('+40 seconds'), + RegisteredClaims::EXPIRATION_TIME => $now->modify('+60 seconds'), + ]; + + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('The token was issued in the future'); + + $constraint = new ValidAt($this->clock); + $constraint->assert($this->buildToken($claims)); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::guardLeeway + * @covers ::assert + * @covers ::assertExpiration + * @covers ::assertIssueTime + * @covers ::assertMinimumTime + */ + public function assertShouldNotRaiseExceptionWhenLeewayIsUsed() + { + $now = $this->clock->now(); + + $claims = [ + RegisteredClaims::ISSUED_AT => $now->modify('+5 seconds'), + RegisteredClaims::NOT_BEFORE => $now->modify('+5 seconds'), + RegisteredClaims::EXPIRATION_TIME => $now->modify('-5 seconds'), + ]; + + $constraint = new ValidAt($this->clock, new DateInterval('PT5S')); + $constraint->assert($this->buildToken($claims)); + + $this->addToAssertionCount(1); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::guardLeeway + * @covers ::assert + * @covers ::assertExpiration + * @covers ::assertIssueTime + * @covers ::assertMinimumTime + */ + public function assertShouldNotRaiseExceptionWhenTokenIsUsedInTheRightMoment() + { + $constraint = new ValidAt($this->clock); + $now = $this->clock->now(); + + $token = $this->buildToken( + [ + RegisteredClaims::ISSUED_AT => $now->modify('-40 seconds'), + RegisteredClaims::NOT_BEFORE => $now->modify('-20 seconds'), + RegisteredClaims::EXPIRATION_TIME => $now->modify('+60 seconds'), + ] + ); + + $constraint->assert($token); + $this->addToAssertionCount(1); + + $token = $this->buildToken( + [ + RegisteredClaims::ISSUED_AT => $now, + RegisteredClaims::NOT_BEFORE => $now, + RegisteredClaims::EXPIRATION_TIME => $now->modify('+60 seconds'), + ] + ); + + $constraint->assert($token); + $this->addToAssertionCount(1); + } + + /** + * @test + * + * @covers ::__construct + * @covers ::guardLeeway + * @covers ::assert + * @covers ::assertExpiration + * @covers ::assertIssueTime + * @covers ::assertMinimumTime + */ + public function assertShouldNotRaiseExceptionWhenTokenDoesNotHaveTimeClaims() + { + $token = $this->buildToken(); + $constraint = new ValidAt($this->clock); + + $constraint->assert($token); + $this->addToAssertionCount(1); + } +} diff --git a/test/unit/Validation/RequiredConstraintsViolatedTest.php b/test/unit/Validation/RequiredConstraintsViolatedTest.php new file mode 100644 index 00000000..c8368de1 --- /dev/null +++ b/test/unit/Validation/RequiredConstraintsViolatedTest.php @@ -0,0 +1,29 @@ +getMessage() + ); + + self::assertSame([$violation], $exception->violations()); + } +} diff --git a/test/unit/Validation/ValidatorTest.php b/test/unit/Validation/ValidatorTest.php new file mode 100644 index 00000000..bbeef383 --- /dev/null +++ b/test/unit/Validation/ValidatorTest.php @@ -0,0 +1,139 @@ +token = $this->createMock(Token::class); + } + + /** + * @test + * + * @covers ::assert + */ + public function assertShouldRaiseExceptionWhenNoConstraintIsGiven() + { + $validator = new Validator(); + + $this->expectException(NoConstraintsGiven::class); + + $validator->assert($this->token, ...[]); + } + + /** + * @test + * + * @covers ::assert + * @covers ::checkConstraint + * + * @uses \Lcobucci\JWT\Validation\RequiredConstraintsViolated + */ + public function assertShouldRaiseExceptionWhenAtLeastOneConstraintFails() + { + $failedConstraint = $this->createMock(Constraint::class); + $successfulConstraint = $this->createMock(Constraint::class); + + $failedConstraint->expects(self::once()) + ->method('assert') + ->willThrowException(new ConstraintViolation()); + + $successfulConstraint->expects(self::once()) + ->method('assert'); + + $validator = new Validator(); + + $this->expectException(RequiredConstraintsViolated::class); + $this->expectExceptionMessage('The token violates some mandatory constraints'); + + $validator->assert( + $this->token, + $failedConstraint, + $successfulConstraint + ); + } + + /** + * @test + * + * @covers ::assert + * @covers ::checkConstraint + */ + public function assertShouldNotRaiseExceptionWhenNoConstraintFails() + { + $constraint = $this->createMock(Constraint::class); + $constraint->expects(self::once())->method('assert'); + + $validator = new Validator(); + + $validator->assert($this->token, $constraint); + $this->addToAssertionCount(1); + } + + /** + * @test + * + * @covers ::validate + */ + public function validateShouldRaiseExceptionWhenNoConstraintIsGiven() + { + $validator = new Validator(); + + $this->expectException(NoConstraintsGiven::class); + + $validator->validate($this->token, ...[]); + } + + /** + * @test + * + * @covers ::validate + */ + public function validateShouldReturnFalseWhenAtLeastOneConstraintFails() + { + $failedConstraint = $this->createMock(Constraint::class); + $successfulConstraint = $this->createMock(Constraint::class); + + $failedConstraint->expects(self::once()) + ->method('assert') + ->willThrowException(new ConstraintViolation()); + + $successfulConstraint->expects(self::never()) + ->method('assert'); + + $validator = new Validator(); + + self::assertFalse( + $validator->validate( + $this->token, + $failedConstraint, + $successfulConstraint + ) + ); + } + + /** + * @test + * + * @covers ::validate + */ + public function validateShouldReturnTrueWhenNoConstraintFails() + { + $constraint = $this->createMock(Constraint::class); + $constraint->expects(self::once())->method('assert'); + + $validator = new Validator(); + self::assertTrue($validator->validate($this->token, $constraint)); + } +} From df3b58a8b078da2361a86e78951bcc27733aba22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Mon, 23 Nov 2020 17:11:21 +0100 Subject: [PATCH 16/17] Backport unsecure signer --- src/Signer/None.php | 21 ++++++++++ test/unit/Signer/NoneTest.php | 72 +++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/Signer/None.php create mode 100644 test/unit/Signer/NoneTest.php diff --git a/src/Signer/None.php b/src/Signer/None.php new file mode 100644 index 00000000..1f35d3d6 --- /dev/null +++ b/src/Signer/None.php @@ -0,0 +1,21 @@ +getAlgorithmId()); + } + + /** + * @test + * + * @covers ::createHash + * + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function signShouldReturnAnEmptyString() + { + $signer = new None(); + + self::assertEquals('', $signer->sign('test', InMemory::plainText('test'))); + } + + /** + * @test + * + * @covers ::doVerify + * + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function verifyShouldReturnTrueWhenSignatureHashIsEmpty() + { + $signer = new None(); + + self::assertTrue($signer->verify('', 'test', InMemory::plainText('test'))); + } + + /** + * @test + * + * @covers ::doVerify + * + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function verifyShouldReturnFalseWhenSignatureHashIsEmpty() + { + $signer = new None(); + + self::assertFalse($signer->verify('testing', 'test', InMemory::plainText('test'))); + } +} From 3d00f205ba5f476b8e1b1b7e761233603d102495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Mon, 23 Nov 2020 17:24:48 +0100 Subject: [PATCH 17/17] Backport configuration object --- src/Configuration.php | 178 ++++++++++++++++++ test/unit/ConfigurationTest.php | 310 ++++++++++++++++++++++++++++++++ 2 files changed, 488 insertions(+) create mode 100644 src/Configuration.php create mode 100644 test/unit/ConfigurationTest.php diff --git a/src/Configuration.php b/src/Configuration.php new file mode 100644 index 00000000..3efa716a --- /dev/null +++ b/src/Configuration.php @@ -0,0 +1,178 @@ +signer = $signer; + $this->signingKey = $signingKey; + $this->verificationKey = $verificationKey; + $this->parser = new Parser($decoder ?: new Decoder()); + $this->validator = new Validation\Validator(); + + $this->builderFactory = static function () use ($encoder) { + return new Builder($encoder ?: new Encoder()); + }; + } + + /** @return self */ + public static function forAsymmetricSigner( + Signer $signer, + Key $signingKey, + Key $verificationKey, + Encoder $encoder = null, + Decoder $decoder = null + ) { + return new self( + $signer, + $signingKey, + $verificationKey, + $encoder, + $decoder + ); + } + + /** @return self */ + public static function forSymmetricSigner( + Signer $signer, + Key $key, + Encoder $encoder = null, + Decoder $decoder = null + ) { + return new self( + $signer, + $key, + $key, + $encoder, + $decoder + ); + } + + /** @return self */ + public static function forUnsecuredSigner( + Encoder $encoder = null, + Decoder $decoder = null + ) { + $key = InMemory::plainText(''); + + return new self( + new None(), + $key, + $key, + $encoder, + $decoder + ); + } + + /** @param callable(): Builder $builderFactory */ + public function setBuilderFactory(callable $builderFactory) + { + if (! $builderFactory instanceof Closure) { + $builderFactory = static function() use ($builderFactory) { + return $builderFactory(); + }; + } + $this->builderFactory = $builderFactory; + } + + /** @return Builder */ + public function builder() + { + $factory = $this->builderFactory; + + return $factory(); + } + + /** @return Parser */ + public function parser() + { + return $this->parser; + } + + public function setParser(Parser $parser) + { + $this->parser = $parser; + } + + /** @return Signer */ + public function signer() + { + return $this->signer; + } + + /** @return Key */ + public function signingKey() + { + return $this->signingKey; + } + + /** @return Key */ + public function verificationKey() + { + return $this->verificationKey; + } + + /** @return Validator */ + public function validator() + { + return $this->validator; + } + + public function setValidator(Validator $validator) + { + $this->validator = $validator; + } + + /** @return Constraint[] */ + public function validationConstraints() + { + return $this->validationConstraints; + } + + public function setValidationConstraints(Constraint ...$validationConstraints) + { + $this->validationConstraints = $validationConstraints; + } +} diff --git a/test/unit/ConfigurationTest.php b/test/unit/ConfigurationTest.php new file mode 100644 index 00000000..e1fc5566 --- /dev/null +++ b/test/unit/ConfigurationTest.php @@ -0,0 +1,310 @@ +signer = $this->createMock(Signer::class); + $this->encoder = $this->createMock(Encoder::class); + $this->decoder = $this->createMock(Decoder::class); + $this->parser = $this->createMock(Parser::class); + $this->validator = $this->createMock(Validator::class); + $this->validationConstraints = $this->createMock(Constraint::class); + } + + /** + * @test + * + * @covers ::forAsymmetricSigner + * @covers ::__construct + * @covers ::signer + * @covers ::signingKey + * @covers ::verificationKey + * + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function forAsymmetricSignerShouldConfigureSignerAndBothKeys() + { + $signingKey = InMemory::plainText('private'); + $verificationKey = InMemory::plainText('public'); + + $config = Configuration::forAsymmetricSigner($this->signer, $signingKey, $verificationKey); + + self::assertSame($this->signer, $config->signer()); + self::assertSame($signingKey, $config->signingKey()); + self::assertSame($verificationKey, $config->verificationKey()); + } + + /** + * @test + * + * @covers ::forSymmetricSigner + * @covers ::__construct + * @covers ::signer + * @covers ::signingKey + * @covers ::verificationKey + * + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function forSymmetricSignerShouldConfigureSignerAndBothKeys() + { + $key = InMemory::plainText('private'); + $config = Configuration::forSymmetricSigner($this->signer, $key); + + self::assertSame($this->signer, $config->signer()); + self::assertSame($key, $config->signingKey()); + self::assertSame($key, $config->verificationKey()); + } + + /** + * @test + * + * @covers ::forUnsecuredSigner + * @covers ::__construct + * @covers ::signer + * @covers ::signingKey + * @covers ::verificationKey + * + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function forUnsecuredSignerShouldConfigureSignerAndBothKeys() + { + $key = InMemory::plainText(''); + $config = Configuration::forUnsecuredSigner(); + + self::assertInstanceOf(None::class, $config->signer()); + self::assertEquals($key, $config->signingKey()); + self::assertEquals($key, $config->verificationKey()); + } + + /** + * @test + * + * @covers ::builder + * + * @uses \Lcobucci\JWT\Configuration::forUnsecuredSigner + * @uses \Lcobucci\JWT\Configuration::__construct + * @uses \Lcobucci\JWT\Builder + * @uses \Lcobucci\JWT\Signer\None + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function builderShouldCreateABuilderWithDefaultEncoder() + { + $config = Configuration::forUnsecuredSigner(); + $builder = $config->builder(); + + self::assertInstanceOf(Builder::class, $builder); + self::assertNotEquals(new Builder($this->encoder), $builder); + self::assertEquals(new Builder(new Encoder()), $builder); + } + + /** + * @test + * + * @covers ::builder + * @covers ::__construct + * + * @uses \Lcobucci\JWT\Configuration::forUnsecuredSigner + * @uses \Lcobucci\JWT\Builder + * @uses \Lcobucci\JWT\Signer\None + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function builderShouldCreateABuilderWithCustomizedEncoder() + { + $config = Configuration::forUnsecuredSigner($this->encoder); + $builder = $config->builder(); + + self::assertInstanceOf(Builder::class, $builder); + self::assertEquals(new Builder($this->encoder), $builder); + } + + /** + * @test + * + * @covers ::builder + * @covers ::setBuilderFactory + * + * @uses \Lcobucci\JWT\Configuration::forUnsecuredSigner + * @uses \Lcobucci\JWT\Configuration::__construct + * @uses \Lcobucci\JWT\Builder + * @uses \Lcobucci\JWT\Signer\None + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function builderShouldUseBuilderFactoryWhenThatIsConfigured() + { + $builder = $this->createMock(Builder::class); + + $config = Configuration::forUnsecuredSigner(); + $config->setBuilderFactory( + static function () use ($builder) { + return $builder; + } + ); + self::assertSame($builder, $config->builder()); + } + + /** + * @test + * + * @covers ::parser + * + * @uses \Lcobucci\JWT\Configuration::forUnsecuredSigner + * @uses \Lcobucci\JWT\Configuration::__construct + * @uses \Lcobucci\JWT\Signer\None + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function parserShouldReturnAParserWithDefaultDecoder() + { + $config = Configuration::forUnsecuredSigner(); + $parser = $config->parser(); + + self::assertNotEquals(new Parser($this->decoder), $parser); + } + + /** + * @test + * + * @covers ::parser + * @covers ::__construct + * + * @uses \Lcobucci\JWT\Configuration::forUnsecuredSigner + * @uses \Lcobucci\JWT\Signer\None + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function parserShouldReturnAParserWithCustomizedDecoder() + { + $config = Configuration::forUnsecuredSigner(null, $this->decoder); + $parser = $config->parser(); + + self::assertEquals(new Parser($this->decoder), $parser); + } + + /** + * @test + * + * @covers ::parser + * @covers ::setParser + * + * @uses \Lcobucci\JWT\Configuration::forUnsecuredSigner + * @uses \Lcobucci\JWT\Configuration::__construct + * @uses \Lcobucci\JWT\Signer\None + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function parserShouldNotCreateAnInstanceIfItWasConfigured() + { + $config = Configuration::forUnsecuredSigner(); + $config->setParser($this->parser); + + self::assertSame($this->parser, $config->parser()); + } + + /** + * @test + * + * @covers ::validator + * + * @uses \Lcobucci\JWT\Configuration::forUnsecuredSigner + * @uses \Lcobucci\JWT\Configuration::__construct + * @uses \Lcobucci\JWT\Signer\None + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function validatorShouldReturnTheDefaultWhenItWasNotConfigured() + { + $config = Configuration::forUnsecuredSigner(); + $validator = $config->validator(); + + self::assertNotSame($this->validator, $validator); + } + + /** + * @test + * + * @covers ::validator + * @covers ::setValidator + * + * @uses \Lcobucci\JWT\Configuration::forUnsecuredSigner + * @uses \Lcobucci\JWT\Configuration::__construct + * @uses \Lcobucci\JWT\Signer\None + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function validatorShouldReturnTheConfiguredValidator() + { + $config = Configuration::forUnsecuredSigner(); + $config->setValidator($this->validator); + + self::assertSame($this->validator, $config->validator()); + } + + /** + * @test + * + * @covers ::validationConstraints + * + * @uses \Lcobucci\JWT\Configuration::forUnsecuredSigner + * @uses \Lcobucci\JWT\Configuration::__construct + * @uses \Lcobucci\JWT\Signer\None + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function validationConstraintsShouldReturnAnEmptyArrayWhenItWasNotConfigured() + { + $config = Configuration::forUnsecuredSigner(); + + self::assertSame([], $config->validationConstraints()); + } + + /** + * @test + * + * @covers ::validationConstraints + * @covers ::setValidationConstraints + * + * @uses \Lcobucci\JWT\Configuration::forUnsecuredSigner + * @uses \Lcobucci\JWT\Configuration::__construct + * @uses \Lcobucci\JWT\Signer\None + * @uses \Lcobucci\JWT\Signer\Key\InMemory + */ + public function validationConstraintsShouldReturnTheConfiguredValidator() + { + $config = Configuration::forUnsecuredSigner(); + $config->setValidationConstraints($this->validationConstraints); + + self::assertSame([$this->validationConstraints], $config->validationConstraints()); + } +}