Skip to content

Commit

Permalink
[try-out] experiment with design to ensure validity of constraints
Browse files Browse the repository at this point in the history
Signed-off-by: Luís Cobucci <[email protected]>
  • Loading branch information
lcobucci committed Nov 19, 2023
1 parent cabd7da commit 26960f1
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 36 deletions.
7 changes: 3 additions & 4 deletions src/Validation/Constraint/SignedWithOneInSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@

final class SignedWithOneInSet implements SignedWithInterface
{
/** @var array<SignedWithInterface> */
/** @var array<SignedWithUntilDate> */
private readonly array $constraints;

public function __construct(SignedWithInterface ...$constraints)
public function __construct(SignedWithUntilDate ...$constraints)
{
$this->constraints = $constraints;
}
Expand All @@ -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();
}
}

Expand Down
47 changes: 47 additions & 0 deletions src/Validation/Constraint/SignedWithUntilDate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);

namespace Lcobucci\JWT\Validation\Constraint;

use DateTimeImmutable;
use DateTimeInterface;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Validation\ConstraintViolation;
use Lcobucci\JWT\Validation\SignedWith as SignedWithInterface;
use Psr\Clock\ClockInterface;

final class SignedWithUntilDate implements SignedWithInterface
{
private readonly SignedWith $verifySignature;
private readonly ClockInterface $clock;

public function __construct(
Signer $signer,
Signer\Key $key,
private readonly DateTimeImmutable $validUntil,
?ClockInterface $clock = null,
) {
$this->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);
}
}
18 changes: 16 additions & 2 deletions tests/JwtFacadeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
71 changes: 41 additions & 30 deletions tests/Validation/Constraint/SignedWithOneInSetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
125 changes: 125 additions & 0 deletions tests/Validation/Constraint/SignedWithUntilDateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);

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\SodiumBase64Polyfill;
use Lcobucci\JWT\Tests\Signer\FakeSigner;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Token\Builder;
use Lcobucci\JWT\Token\DataSet;
use Lcobucci\JWT\Token\Plain;
use Lcobucci\JWT\Token\Signature;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\SignedWithUntilDate;
use Lcobucci\JWT\Validation\ConstraintViolation;
use PHPUnit\Framework\Attributes as PHPUnit;

#[PHPUnit\CoversClass(SignedWithUntilDate::class)]
#[PHPUnit\CoversClass(SignedWith::class)]
#[PHPUnit\CoversClass(ConstraintViolation::class)]
#[PHPUnit\UsesClass(DataSet::class)]
#[PHPUnit\UsesClass(InMemory::class)]
#[PHPUnit\UsesClass(Plain::class)]
#[PHPUnit\UsesClass(Signature::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(Token\Parser::class)]
final class SignedWithUntilDateTest extends ConstraintTestCase
{
#[PHPUnit\Test]
public function assertShouldRaiseExceptionWhenConstraintUsageIsNotValidAnymore(): void
{
$this->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);
}
}

0 comments on commit 26960f1

Please sign in to comment.