diff --git a/docs/validating-tokens.md b/docs/validating-tokens.md index 7e130db9e..a833502b7 100644 --- a/docs/validating-tokens.md +++ b/docs/validating-tokens.md @@ -85,6 +85,7 @@ This library provides the following constraints: * `Lcobucci\JWT\Validation\Constraint\PermittedFor`: verifies if the claim `aud` contains the expected value * `Lcobucci\JWT\Validation\Constraint\RelatedTo`: verifies if the claim `sub` matches the expected value * `Lcobucci\JWT\Validation\Constraint\SignedWith`: verifies if the token was signed with the expected signer and key +* `Lcobucci\JWT\Validation\Constraint\SignedWithOneInSet`: verifies the token signature against multiple `SignedWith` constraints * `Lcobucci\JWT\Validation\Constraint\StrictValidAt`: verifies presence and validity of the claims `iat`, `nbf`, and `exp` (supports leeway configuration) * `Lcobucci\JWT\Validation\Constraint\LooseValidAt`: verifies the claims `iat`, `nbf`, and `exp`, when present (supports leeway configuration) * `Lcobucci\JWT\Validation\Constraint\HasClaimWithValue`: verifies that a **custom claim** has the expected value (not recommended when comparing cryptographic hashes) diff --git a/src/Validation/Constraint/SignedWithOneInSet.php b/src/Validation/Constraint/SignedWithOneInSet.php new file mode 100644 index 000000000..4e0fe0be6 --- /dev/null +++ b/src/Validation/Constraint/SignedWithOneInSet.php @@ -0,0 +1,39 @@ + */ + private readonly array $constraints; + + public function __construct(SignedWithInterface ...$constraints) + { + $this->constraints = $constraints; + } + + public function assert(Token $token): void + { + $errorMessage = 'It was not possible to verify the signature of the token, reasons:'; + + foreach ($this->constraints as $constraint) { + try { + $constraint->assert($token); + + return; + } catch (ConstraintViolation $violation) { + $errorMessage .= PHP_EOL . '- ' . $violation->getMessage() + . ' (verified by ' . $violation->constraint . ')'; + } + } + + throw ConstraintViolation::error($errorMessage, $this); + } +} diff --git a/tests/JwtFacadeTest.php b/tests/JwtFacadeTest.php index ed3caea43..c9de4345a 100644 --- a/tests/JwtFacadeTest.php +++ b/tests/JwtFacadeTest.php @@ -14,14 +14,15 @@ use Lcobucci\JWT\Token\Plain; use Lcobucci\JWT\Validation\Constraint\IssuedBy; use Lcobucci\JWT\Validation\Constraint\SignedWith; +use Lcobucci\JWT\Validation\Constraint\SignedWithOneInSet; use Lcobucci\JWT\Validation\Constraint\StrictValidAt; use Lcobucci\JWT\Validation\RequiredConstraintsViolated; +use PHPUnit\Framework\Attributes as PHPUnit; use PHPUnit\Framework\TestCase; use Psr\Clock\ClockInterface; /** - * @covers ::__construct - * @coversDefaultClass \Lcobucci\JWT\JwtFacade + * @covers \Lcobucci\JWT\JwtFacade * * @uses \Lcobucci\JWT\Token\Parser * @uses \Lcobucci\JWT\Encoding\JoseEncoder @@ -40,6 +41,7 @@ * @uses \Lcobucci\JWT\Validation\Validator * @uses \Lcobucci\JWT\Validation\Constraint\IssuedBy * @uses \Lcobucci\JWT\Validation\Constraint\SignedWith + * @uses \Lcobucci\JWT\Validation\Constraint\SignedWithOneInSet * @uses \Lcobucci\JWT\Validation\Constraint\StrictValidAt * @uses \Lcobucci\JWT\Validation\ConstraintViolation * @uses \Lcobucci\JWT\Validation\RequiredConstraintsViolated @@ -72,12 +74,7 @@ private function createToken(): string )->toString(); } - /** - * @test - * - * @covers ::issue - * @covers ::parse - */ + #[PHPUnit\Test] public function issueSetTimeValidity(): void { $token = (new JwtFacade(clock: $this->clock))->issue( @@ -105,12 +102,7 @@ public function issueSetTimeValidity(): void self::assertTrue($token->isExpired($inOneYear)); } - /** - * @test - * - * @covers ::issue - * @covers ::parse - */ + #[PHPUnit\Test] public function issueAllowsTimeValidityOverwrite(): void { $then = new DateTimeImmutable('2001-02-03 04:05:06'); @@ -144,12 +136,7 @@ static function (Builder $builder) use ($then): Builder { self::assertTrue($token->isExpired($inOneYear)); } - /** - * @test - * - * @covers ::issue - * @covers ::parse - */ + #[PHPUnit\Test] public function goodJwt(): void { $token = (new JwtFacade())->parse( @@ -162,12 +149,7 @@ public function goodJwt(): void self::assertInstanceOf(Plain::class, $token); } - /** - * @test - * - * @covers ::issue - * @covers ::parse - */ + #[PHPUnit\Test] public function badSigner(): void { $this->expectException(RequiredConstraintsViolated::class); @@ -181,12 +163,7 @@ public function badSigner(): void ); } - /** - * @test - * - * @covers ::issue - * @covers ::parse - */ + #[PHPUnit\Test] public function badKey(): void { $this->expectException(RequiredConstraintsViolated::class); @@ -200,12 +177,7 @@ public function badKey(): void ); } - /** - * @test - * - * @covers ::issue - * @covers ::parse - */ + #[PHPUnit\Test] public function badTime(): void { $token = $this->createToken(); @@ -222,12 +194,7 @@ public function badTime(): void ); } - /** - * @test - * - * @covers ::issue - * @covers ::parse - */ + #[PHPUnit\Test] public function badIssuer(): void { $this->expectException(RequiredConstraintsViolated::class); @@ -241,11 +208,7 @@ public function badIssuer(): void ); } - /** - * @test - * - * @covers ::parse - */ + #[PHPUnit\Test] public function parserForNonUnencryptedTokens(): void { $this->expectException(AssertionError::class); @@ -258,12 +221,7 @@ public function parserForNonUnencryptedTokens(): void ); } - /** - * @test - * - * @covers ::issue - * @covers ::parse - */ + #[PHPUnit\Test] public function customPsrClock(): void { $clock = new class () implements ClockInterface { @@ -290,4 +248,20 @@ public function now(): DateTimeImmutable ), ); } + + #[PHPUnit\Test] + public function multipleKeys(): void + { + $token = (new JwtFacade())->parse( + $this->createToken(), + new SignedWithOneInSet( + new SignedWith($this->signer, InMemory::base64Encoded('czyPTpN595zVNSuvoNNlXCRFgXS2fHscMR36dGojaUE=')), + new SignedWith($this->signer, $this->key), + ), + new StrictValidAt($this->clock), + new IssuedBy($this->issuer), + ); + + self::assertInstanceOf(Plain::class, $token); + } } diff --git a/tests/Validation/Constraint/SignedWithOneInSetTest.php b/tests/Validation/Constraint/SignedWithOneInSetTest.php new file mode 100644 index 000000000..38245956e --- /dev/null +++ b/tests/Validation/Constraint/SignedWithOneInSetTest.php @@ -0,0 +1,75 @@ +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)); + + $constraint = new SignedWithOneInSet($verifySignature1, $verifySignature2); + + $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 . ')', + ); + + $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)); + + $verifySignature2 = $this->createMock(SignedWith::class); + $verifySignature2->expects(self::once())->method('assert'); + + $constraint = new SignedWithOneInSet($verifySignature1, $verifySignature2); + $constraint->assert($token); + + $this->addToAssertionCount(1); + } +}