From 26960f17cc8040dea553b3fb9fa6c02d86ea2342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sun, 19 Nov 2023 23:07:36 +0100 Subject: [PATCH] [try-out] experiment with design to ensure validity of constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Luís Cobucci --- .../Constraint/SignedWithOneInSet.php | 7 +- .../Constraint/SignedWithUntilDate.php | 47 +++++++ tests/JwtFacadeTest.php | 18 ++- .../Constraint/SignedWithOneInSetTest.php | 71 +++++----- .../Constraint/SignedWithUntilDateTest.php | 125 ++++++++++++++++++ 5 files changed, 232 insertions(+), 36 deletions(-) create mode 100644 src/Validation/Constraint/SignedWithUntilDate.php create mode 100644 tests/Validation/Constraint/SignedWithUntilDateTest.php diff --git a/src/Validation/Constraint/SignedWithOneInSet.php b/src/Validation/Constraint/SignedWithOneInSet.php index 4e0fe0be6..fb542fb3d 100644 --- a/src/Validation/Constraint/SignedWithOneInSet.php +++ b/src/Validation/Constraint/SignedWithOneInSet.php @@ -11,10 +11,10 @@ final class SignedWithOneInSet implements SignedWithInterface { - /** @var array */ + /** @var array */ private readonly array $constraints; - public function __construct(SignedWithInterface ...$constraints) + public function __construct(SignedWithUntilDate ...$constraints) { $this->constraints = $constraints; } @@ -29,8 +29,7 @@ public function assert(Token $token): void return; } catch (ConstraintViolation $violation) { - $errorMessage .= PHP_EOL . '- ' . $violation->getMessage() - . ' (verified by ' . $violation->constraint . ')'; + $errorMessage .= PHP_EOL . '- ' . $violation->getMessage(); } } diff --git a/src/Validation/Constraint/SignedWithUntilDate.php b/src/Validation/Constraint/SignedWithUntilDate.php new file mode 100644 index 000000000..85429e890 --- /dev/null +++ b/src/Validation/Constraint/SignedWithUntilDate.php @@ -0,0 +1,47 @@ +verifySignature = new SignedWith($signer, $key); + + $this->clock = $clock ?? new class () implements ClockInterface { + public function now(): DateTimeImmutable + { + return new DateTimeImmutable(); + } + }; + } + + public function assert(Token $token): void + { + if ($this->validUntil < $this->clock->now()) { + throw ConstraintViolation::error( + 'This constraint was only usable until ' + . $this->validUntil->format(DateTimeInterface::RFC3339), + $this, + ); + } + + $this->verifySignature->assert($token); + } +} diff --git a/tests/JwtFacadeTest.php b/tests/JwtFacadeTest.php index c9de4345a..abf9a0f00 100644 --- a/tests/JwtFacadeTest.php +++ b/tests/JwtFacadeTest.php @@ -15,6 +15,7 @@ use Lcobucci\JWT\Validation\Constraint\IssuedBy; use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\Constraint\SignedWithOneInSet; +use Lcobucci\JWT\Validation\Constraint\SignedWithUntilDate; use Lcobucci\JWT\Validation\Constraint\StrictValidAt; use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use PHPUnit\Framework\Attributes as PHPUnit; @@ -42,6 +43,7 @@ * @uses \Lcobucci\JWT\Validation\Constraint\IssuedBy * @uses \Lcobucci\JWT\Validation\Constraint\SignedWith * @uses \Lcobucci\JWT\Validation\Constraint\SignedWithOneInSet + * @uses \Lcobucci\JWT\Validation\Constraint\SignedWithUntilDate * @uses \Lcobucci\JWT\Validation\Constraint\StrictValidAt * @uses \Lcobucci\JWT\Validation\ConstraintViolation * @uses \Lcobucci\JWT\Validation\RequiredConstraintsViolated @@ -252,11 +254,23 @@ public function now(): DateTimeImmutable #[PHPUnit\Test] public function multipleKeys(): void { + $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:10:00')); + $token = (new JwtFacade())->parse( $this->createToken(), new SignedWithOneInSet( - new SignedWith($this->signer, InMemory::base64Encoded('czyPTpN595zVNSuvoNNlXCRFgXS2fHscMR36dGojaUE=')), - new SignedWith($this->signer, $this->key), + new SignedWithUntilDate( + $this->signer, + InMemory::base64Encoded('czyPTpN595zVNSuvoNNlXCRFgXS2fHscMR36dGojaUE='), + new DateTimeImmutable('2024-11-19 22:10:00'), + $clock, + ), + new SignedWithUntilDate( + $this->signer, + $this->key, + new DateTimeImmutable('2025-11-19 22:10:00'), + $clock, + ), ), new StrictValidAt($this->clock), new IssuedBy($this->issuer), diff --git a/tests/Validation/Constraint/SignedWithOneInSetTest.php b/tests/Validation/Constraint/SignedWithOneInSetTest.php index 38245956e..622417a8d 100644 --- a/tests/Validation/Constraint/SignedWithOneInSetTest.php +++ b/tests/Validation/Constraint/SignedWithOneInSetTest.php @@ -3,71 +3,82 @@ namespace Lcobucci\JWT\Tests\Validation\Constraint; +use DateTimeImmutable; +use Lcobucci\Clock\FrozenClock; +use Lcobucci\JWT\Encoding\ChainedFormatter; +use Lcobucci\JWT\Encoding\JoseEncoder; +use Lcobucci\JWT\Encoding\UnifyAudience; +use Lcobucci\JWT\Encoding\UnixTimestampDates; +use Lcobucci\JWT\JwtFacade; use Lcobucci\JWT\Signer\Key\InMemory; -use Lcobucci\JWT\Token; +use Lcobucci\JWT\SodiumBase64Polyfill; +use Lcobucci\JWT\Tests\Signer\FakeSigner; +use Lcobucci\JWT\Token\Builder; use Lcobucci\JWT\Token\DataSet; +use Lcobucci\JWT\Token\Parser; use Lcobucci\JWT\Token\Plain; use Lcobucci\JWT\Token\Signature; +use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\Constraint\SignedWithOneInSet; +use Lcobucci\JWT\Validation\Constraint\SignedWithUntilDate; use Lcobucci\JWT\Validation\ConstraintViolation; -use Lcobucci\JWT\Validation\SignedWith; use PHPUnit\Framework\Attributes as PHPUnit; use const PHP_EOL; #[PHPUnit\CoversClass(SignedWithOneInSet::class)] +#[PHPUnit\CoversClass(SignedWithUntilDate::class)] +#[PHPUnit\CoversClass(SignedWith::class)] #[PHPUnit\CoversClass(ConstraintViolation::class)] -#[PHPUnit\UsesClass(DataSet::class)] #[PHPUnit\UsesClass(InMemory::class)] +#[PHPUnit\UsesClass(JwtFacade::class)] +#[PHPUnit\UsesClass(ChainedFormatter::class)] +#[PHPUnit\UsesClass(JoseEncoder::class)] +#[PHPUnit\UsesClass(UnifyAudience::class)] +#[PHPUnit\UsesClass(UnixTimestampDates::class)] +#[PHPUnit\UsesClass(SodiumBase64Polyfill::class)] +#[PHPUnit\UsesClass(Builder::class)] +#[PHPUnit\UsesClass(DataSet::class)] #[PHPUnit\UsesClass(Plain::class)] #[PHPUnit\UsesClass(Signature::class)] -#[PHPUnit\UsesClass(SignedWith::class)] +#[PHPUnit\UsesClass(Parser::class)] final class SignedWithOneInSetTest extends ConstraintTestCase { #[PHPUnit\Test] public function exceptionShouldBeRaisedWhenSignatureIsNotVerifiedByAllConstraints(): void { - $token = $this->createMock(Token::class); - - $verifySignature1 = $this->createMock(SignedWith::class); - $verifySignature1 - ->expects(self::once()) - ->method('assert') - ->willThrowException(ConstraintViolation::error('Msg1', $verifySignature1)); - - $verifySignature2 = $this->createMock(SignedWith::class); - $verifySignature2 - ->expects(self::once()) - ->method('assert') - ->willThrowException(ConstraintViolation::error('Msg2', $verifySignature2)); + $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:20:00')); + $signer = new FakeSigner('123'); - $constraint = new SignedWithOneInSet($verifySignature1, $verifySignature2); + $constraint = new SignedWithOneInSet( + new SignedWithUntilDate($signer, InMemory::plainText('b'), $clock->now(), $clock), + new SignedWithUntilDate($signer, InMemory::plainText('c'), $clock->now()->modify('-2 minutes'), $clock), + ); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage( 'It was not possible to verify the signature of the token, reasons:' - . PHP_EOL . '- Msg1 (verified by ' . $verifySignature1::class . ')' - . PHP_EOL . '- Msg2 (verified by ' . $verifySignature2::class . ')', + . PHP_EOL . '- Token signature mismatch' + . PHP_EOL . '- This constraint was only usable until 2023-11-19T22:18:00+00:00', ); + $token = $this->issueToken($signer, InMemory::plainText('a')); $constraint->assert($token); } #[PHPUnit\Test] public function assertShouldNotRaiseExceptionsWhenSignatureIsVerifiedByAtLeastOneConstraint(): void { - $token = $this->createMock(Token::class); - - $verifySignature1 = $this->createMock(SignedWith::class); - $verifySignature1 - ->expects(self::once()) - ->method('assert') - ->willThrowException(ConstraintViolation::error('Msg', $verifySignature1)); + $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:20:00')); + $signer = new FakeSigner('123'); - $verifySignature2 = $this->createMock(SignedWith::class); - $verifySignature2->expects(self::once())->method('assert'); + $constraint = new SignedWithOneInSet( + new SignedWithUntilDate($signer, InMemory::plainText('b'), $clock->now(), $clock), + new SignedWithUntilDate($signer, InMemory::plainText('c'), $clock->now()->modify('-2 minutes'), $clock), + new SignedWithUntilDate($signer, InMemory::plainText('a'), $clock->now(), $clock), + ); - $constraint = new SignedWithOneInSet($verifySignature1, $verifySignature2); + $token = $this->issueToken($signer, InMemory::plainText('a')); $constraint->assert($token); $this->addToAssertionCount(1); diff --git a/tests/Validation/Constraint/SignedWithUntilDateTest.php b/tests/Validation/Constraint/SignedWithUntilDateTest.php new file mode 100644 index 000000000..03980f23a --- /dev/null +++ b/tests/Validation/Constraint/SignedWithUntilDateTest.php @@ -0,0 +1,125 @@ +expectException(ConstraintViolation::class); + $this->expectExceptionMessage('This constraint was only usable until 2023-11-19T21:45:10+00:00'); + + $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:45:10')); + + $constraint = new SignedWithUntilDate( + new FakeSigner('1'), + InMemory::plainText('a'), + $clock->now()->modify('-1 hour'), + $clock, + ); + + $constraint->assert($this->issueToken(new FakeSigner('1'), InMemory::plainText('a'))); + } + + #[PHPUnit\Test] + public function assertShouldRaiseExceptionWhenTokenIsNotAPlainToken(): void + { + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('You should pass a plain token'); + + $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:45:10')); + + $constraint = new SignedWithUntilDate(new FakeSigner('1'), InMemory::plainText('a'), $clock->now(), $clock); + $constraint->assert($this->createMock(Token::class)); + } + + #[PHPUnit\Test] + public function assertShouldRaiseExceptionWhenSignerIsNotTheSame(): void + { + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('Token signer mismatch'); + + $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:45:10')); + $key = InMemory::plainText('a'); + + $constraint = new SignedWithUntilDate(new FakeSigner('1'), $key, $clock->now(), $clock); + $constraint->assert($this->issueToken(new FakeSigner('2'), $key)); + } + + #[PHPUnit\Test] + public function assertShouldRaiseExceptionWhenSignatureIsInvalid(): void + { + $this->expectException(ConstraintViolation::class); + $this->expectExceptionMessage('Token signature mismatch'); + + $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:45:10')); + $signer = new FakeSigner('1'); + + $constraint = new SignedWithUntilDate($signer, InMemory::plainText('a'), $clock->now(), $clock); + $constraint->assert($this->issueToken($signer, InMemory::plainText('b'))); + } + + #[PHPUnit\Test] + public function assertShouldNotRaiseExceptionWhenSignatureIsValid(): void + { + $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:45:10')); + + $signer = new FakeSigner('1'); + $key = InMemory::plainText('a'); + + $constraint = new SignedWithUntilDate($signer, $key, $clock->now(), $clock); + $constraint->assert($this->issueToken($signer, $key)); + + $this->addToAssertionCount(1); + } + + #[PHPUnit\Test] + public function clockShouldBeOptional(): void + { + $signer = new FakeSigner('1'); + $key = InMemory::plainText('a'); + + $constraint = new SignedWithUntilDate($signer, $key, new DateTimeImmutable('+10 seconds')); + $constraint->assert($this->issueToken($signer, $key)); + + $this->addToAssertionCount(1); + } +}