From 83f6c9b1fcb62b59d881ba114e2ff91f9c9e94e6 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Tue, 8 Apr 2025 15:14:58 +0200 Subject: [PATCH 01/66] Move JwsParser to JwsDecoratorBuilder --- README.md | 13 +++++ src/Core.php | 42 +++++++++------ src/Core/Factories/ClientAssertionFactory.php | 2 +- src/Core/Factories/RequestObjectFactory.php | 2 +- src/Federation.php | 38 ++++++++----- .../Factories/EntityStatementFactory.php | 2 +- .../Factories/RequestObjectFactory.php | 2 +- .../Factories/TrustMarkDelegationFactory.php | 2 +- src/Federation/Factories/TrustMarkFactory.php | 2 +- src/Jwks.php | 33 ++++++++---- src/Jwks/Factories/SignedJwksFactory.php | 2 +- .../Factories/JwsDecoratorBuilderFactory.php | 27 ++++++++++ src/Jws/Factories/JwsParserFactory.php | 16 ------ src/Jws/Factories/ParsedJwsFactory.php | 6 +-- ...{JwsParser.php => JwsDecoratorBuilder.php} | 8 ++- .../Factories/JwtVcJsonFactory.php | 15 ++++++ src/VerifiableCredentials/JwtVcJson.php | 16 ++++++ .../VerifiableCredentialInterface.php | 10 ++++ .../Factories/ClientAssertionFactoryTest.php | 14 ++--- .../Factories/RequestObjectFactoryTest.php | 14 ++--- tests/src/CoreTest.php | 8 +-- .../Factories/EntityStatementFactoryTest.php | 14 ++--- .../Factories/RequestObjectFactoryTest.php | 14 ++--- .../TrustMarkDelegationFactoryTest.php | 14 ++--- .../Factories/TrustMarkFactoryTest.php | 14 ++--- tests/src/FederationTest.php | 8 +-- .../Jwks/Factories/SignedJwksFactoryTest.php | 14 ++--- tests/src/JwksTest.php | 8 +-- .../JwsDecoratorBuilderFactoryTest.php | 53 +++++++++++++++++++ .../Jws/Factories/JwsParserFactoryTest.php | 43 --------------- .../Jws/Factories/ParsedJwsFactoryTest.php | 12 ++--- ...erTest.php => JwsDecoratorBuilderTest.php} | 29 +++++++--- 32 files changed, 312 insertions(+), 185 deletions(-) create mode 100644 src/Jws/Factories/JwsDecoratorBuilderFactory.php delete mode 100644 src/Jws/Factories/JwsParserFactory.php rename src/Jws/{JwsParser.php => JwsDecoratorBuilder.php} (72%) create mode 100644 src/VerifiableCredentials/Factories/JwtVcJsonFactory.php create mode 100644 src/VerifiableCredentials/JwtVcJson.php create mode 100644 src/VerifiableCredentials/VerifiableCredentialInterface.php create mode 100644 tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php delete mode 100644 tests/src/Jws/Factories/JwsParserFactoryTest.php rename tests/src/Jws/{JwsParserTest.php => JwsDecoratorBuilderTest.php} (65%) diff --git a/README.md b/README.md index ed8f5af..b637d83 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,19 @@ The library is under development, and you can expect braking changes along the w The library provides some common tools that you might find useful when working with OpenID family of specifications. +``` +/* + * + * | + * \ ___ / _________ + * _ / \ _ GÉANT | * * | Co-Funded by + * | ~ | Trust & Identity | * * | the European + * \_/ Incubator |__*_*__| Union + * = + * + */ +``` + ## Installation Library can be installed by using Composer: diff --git a/src/Core.php b/src/Core.php index 6312d8a..7acefab 100644 --- a/src/Core.php +++ b/src/Core.php @@ -6,6 +6,7 @@ use DateInterval; use Psr\Log\LoggerInterface; +use SimpleSAML\OpenID\Algorithms\AlgorithmManagerDecorator; use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmBag; use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Core\Factories\ClientAssertionFactory; @@ -16,9 +17,9 @@ use SimpleSAML\OpenID\Factories\DateIntervalDecoratorFactory; use SimpleSAML\OpenID\Factories\JwsSerializerManagerDecoratorFactory; use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; -use SimpleSAML\OpenID\Jws\Factories\JwsParserFactory; +use SimpleSAML\OpenID\Jws\Factories\JwsDecoratorBuilderFactory; use SimpleSAML\OpenID\Jws\Factories\JwsVerifierDecoratorFactory; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; @@ -28,7 +29,7 @@ class Core protected ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null; - protected ?JwsParser $jwsParser = null; + protected ?JwsDecoratorBuilder $jwsDecoratorBuilder = null; protected ?JwsVerifierDecorator $jwsVerifierDecorator = null; @@ -42,7 +43,7 @@ class Core protected ?JwsSerializerManagerDecoratorFactory $jwsSerializerManagerDecoratorFactory = null; - protected ?JwsParserFactory $jwsParserFactory = null; + protected ?JwsDecoratorBuilderFactory $jwsDecoratorBuilderFactory = null; protected ?JwsVerifierDecoratorFactory $jwsVerifierDecoratorFactory = null; @@ -51,6 +52,7 @@ class Core protected ?DateIntervalDecoratorFactory $dateIntervalDecoratorFactory = null; protected ?ClaimFactory $claimFactory = null; + protected ?AlgorithmManagerDecorator $algorithmManagerDecorator = null; public function __construct( protected readonly SupportedAlgorithms $supportedAlgorithms = new SupportedAlgorithms( @@ -70,7 +72,7 @@ public function __construct( public function requestObjectFactory(): RequestObjectFactory { return $this->requestObjectFactory ??= new RequestObjectFactory( - $this->jwsParser(), + $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), $this->jwksFactory(), $this->jwsSerializerManagerDecorator(), @@ -83,7 +85,7 @@ public function requestObjectFactory(): RequestObjectFactory public function clientAssertionFactory(): ClientAssertionFactory { return $this->clientAssertionFactory ??= new ClientAssertionFactory( - $this->jwsParser(), + $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), $this->jwksFactory(), $this->jwsSerializerManagerDecorator(), @@ -107,6 +109,12 @@ public function algorithmManagerDecoratorFactory(): AlgorithmManagerDecoratorFac return $this->algorithmManagerDecoratorFactory; } + public function algorithmManagerDecorator(): AlgorithmManagerDecorator + { + return $this->algorithmManagerDecorator ??= $this->algorithmManagerDecoratorFactory() + ->build($this->supportedAlgorithms); + } + public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDecoratorFactory { if (is_null($this->jwsSerializerManagerDecoratorFactory)) { @@ -116,13 +124,13 @@ public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDeco return $this->jwsSerializerManagerDecoratorFactory; } - public function jwsParserFactory(): JwsParserFactory + public function jwsDecoratorBuilderFactory(): JwsDecoratorBuilderFactory { - if (is_null($this->jwsParserFactory)) { - $this->jwsParserFactory = new JwsParserFactory(); + if (is_null($this->jwsDecoratorBuilderFactory)) { + $this->jwsDecoratorBuilderFactory = new JwsDecoratorBuilderFactory(); } - return $this->jwsParserFactory; + return $this->jwsDecoratorBuilderFactory; } public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory @@ -158,20 +166,24 @@ public function jwsSerializerManagerDecorator(): JwsSerializerManagerDecorator return $this->jwsSerializerManagerDecorator; } - public function jwsParser(): JwsParser + public function jwsDecoratorBuilder(): JwsDecoratorBuilder { - if (is_null($this->jwsParser)) { - $this->jwsParser = $this->jwsParserFactory()->build($this->jwsSerializerManagerDecorator()); + if (is_null($this->jwsDecoratorBuilder)) { + $this->jwsDecoratorBuilder = $this->jwsDecoratorBuilderFactory()->build( + $this->jwsSerializerManagerDecorator(), + $this->algorithmManagerDecorator(), + $this->helpers(), + ); } - return $this->jwsParser; + return $this->jwsDecoratorBuilder; } public function jwsVerifierDecorator(): JwsVerifierDecorator { if (is_null($this->jwsVerifierDecorator)) { $this->jwsVerifierDecorator = $this->jwsVerifierDecoratorFactory()->build( - $this->algorithmManagerDecoratorFactory()->build($this->supportedAlgorithms), + $this->algorithmManagerDecorator(), ); } diff --git a/src/Core/Factories/ClientAssertionFactory.php b/src/Core/Factories/ClientAssertionFactory.php index b15ea27..dd7f46a 100644 --- a/src/Core/Factories/ClientAssertionFactory.php +++ b/src/Core/Factories/ClientAssertionFactory.php @@ -12,7 +12,7 @@ class ClientAssertionFactory extends ParsedJwsFactory public function fromToken(string $token): ClientAssertion { return new ClientAssertion( - $this->jwsParser->parse($token), + $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, $this->jwksFactory, $this->jwsSerializerManagerDecorator, diff --git a/src/Core/Factories/RequestObjectFactory.php b/src/Core/Factories/RequestObjectFactory.php index 4721dda..8f02244 100644 --- a/src/Core/Factories/RequestObjectFactory.php +++ b/src/Core/Factories/RequestObjectFactory.php @@ -12,7 +12,7 @@ class RequestObjectFactory extends ParsedJwsFactory public function fromToken(string $token): RequestObject { return new RequestObject( - $this->jwsParser->parse($token), + $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, $this->jwksFactory, $this->jwsSerializerManagerDecorator, diff --git a/src/Federation.php b/src/Federation.php index f772d0f..3f9aa63 100644 --- a/src/Federation.php +++ b/src/Federation.php @@ -8,6 +8,7 @@ use GuzzleHttp\Client; use Psr\Log\LoggerInterface; use Psr\SimpleCache\CacheInterface; +use SimpleSAML\OpenID\Algorithms\AlgorithmManagerDecorator; use SimpleSAML\OpenID\Decorators\CacheDecorator; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Decorators\HttpClientDecorator; @@ -30,9 +31,9 @@ use SimpleSAML\OpenID\Federation\TrustMarkFetcher; use SimpleSAML\OpenID\Federation\TrustMarkValidator; use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; -use SimpleSAML\OpenID\Jws\Factories\JwsParserFactory; +use SimpleSAML\OpenID\Jws\Factories\JwsDecoratorBuilderFactory; use SimpleSAML\OpenID\Jws\Factories\JwsVerifierDecoratorFactory; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; use SimpleSAML\OpenID\Utils\ArtifactFetcher; @@ -51,7 +52,7 @@ class Federation protected ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null; - protected ?JwsParser $jwsParser = null; + protected ?JwsDecoratorBuilder $jwsDecoratorBuilder = null; protected ?JwsVerifierDecorator $jwsVerifierDecorator = null; @@ -77,7 +78,7 @@ class Federation protected ?JwsSerializerManagerDecoratorFactory $jwsSerializerManagerDecoratorFactory = null; - protected ?JwsParserFactory $jwsParserFactory = null; + protected ?JwsDecoratorBuilderFactory $jwsDecoratorBuilderFactory = null; protected ?JwsVerifierDecoratorFactory $jwsVerifierDecoratorFactory = null; @@ -100,6 +101,7 @@ class Federation protected ?TrustMarkValidator $trustMarkValidator = null; protected ?TrustMarkFetcher $trustMarkFetcher = null; + protected ?AlgorithmManagerDecorator $algorithmManagerDecorator = null; public function __construct( protected readonly SupportedAlgorithms $supportedAlgorithms = new SupportedAlgorithms(), @@ -122,13 +124,17 @@ public function __construct( public function jwsVerifierDecorator(): JwsVerifierDecorator { return $this->jwsVerifierDecorator ??= $this->jwsVerifierDecoratorFactory()->build( - $this->algorithmManagerDecoratorFactory()->build($this->supportedAlgorithms), + $this->algorithmManagerDecorator(), ); } - public function jwsParser(): JwsParser + public function jwsDecoratorBuilder(): JwsDecoratorBuilder { - return $this->jwsParser ??= $this->jwsParserFactory()->build($this->jwsSerializerManagerDecorator()); + return $this->jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderFactory()->build( + $this->jwsSerializerManagerDecorator(), + $this->algorithmManagerDecorator(), + $this->helpers(), + ); } public function jwsSerializerManagerDecorator(): JwsSerializerManagerDecorator @@ -185,7 +191,7 @@ public function trustChainResolver(): TrustChainResolver public function entityStatementFactory(): EntityStatementFactory { return $this->entityStatementFactory ??= new EntityStatementFactory( - $this->jwsParser(), + $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), $this->jwksFactory(), $this->jwsSerializerManagerDecorator(), @@ -198,7 +204,7 @@ public function entityStatementFactory(): EntityStatementFactory public function requestObjectFactory(): RequestObjectFactory { return $this->requestObjectFactory ??= new RequestObjectFactory( - $this->jwsParser(), + $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), $this->jwksFactory(), $this->jwsSerializerManagerDecorator(), @@ -211,7 +217,7 @@ public function requestObjectFactory(): RequestObjectFactory public function trustMarkFactory(): TrustMarkFactory { return $this->trustMarkFactory ??= new TrustMarkFactory( - $this->jwsParser(), + $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), $this->jwksFactory(), $this->jwsSerializerManagerDecorator(), @@ -224,7 +230,7 @@ public function trustMarkFactory(): TrustMarkFactory public function trustMarkDelegationFactory(): TrustMarkDelegationFactory { return $this->trustMarkDelegationFactory ?? new TrustMarkDelegationFactory( - $this->jwsParser(), + $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), $this->jwksFactory(), $this->jwsSerializerManagerDecorator(), @@ -267,14 +273,20 @@ public function algorithmManagerDecoratorFactory(): AlgorithmManagerDecoratorFac return $this->algorithmManagerDecoratorFactory ??= new AlgorithmManagerDecoratorFactory(); } + public function algorithmManagerDecorator(): AlgorithmManagerDecorator + { + return $this->algorithmManagerDecorator ??= $this->algorithmManagerDecoratorFactory() + ->build($this->supportedAlgorithms); + } + public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDecoratorFactory { return $this->jwsSerializerManagerDecoratorFactory ??= new JwsSerializerManagerDecoratorFactory(); } - public function jwsParserFactory(): JwsParserFactory + public function jwsDecoratorBuilderFactory(): JwsDecoratorBuilderFactory { - return $this->jwsParserFactory ??= new JwsParserFactory(); + return $this->jwsDecoratorBuilderFactory ??= new JwsDecoratorBuilderFactory(); } public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory diff --git a/src/Federation/Factories/EntityStatementFactory.php b/src/Federation/Factories/EntityStatementFactory.php index 447facf..e6c77fd 100644 --- a/src/Federation/Factories/EntityStatementFactory.php +++ b/src/Federation/Factories/EntityStatementFactory.php @@ -16,7 +16,7 @@ class EntityStatementFactory extends ParsedJwsFactory public function fromToken(string $token): EntityStatement { return new EntityStatement( - $this->jwsParser->parse($token), + $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, $this->jwksFactory, $this->jwsSerializerManagerDecorator, diff --git a/src/Federation/Factories/RequestObjectFactory.php b/src/Federation/Factories/RequestObjectFactory.php index 04edb98..d6d6792 100644 --- a/src/Federation/Factories/RequestObjectFactory.php +++ b/src/Federation/Factories/RequestObjectFactory.php @@ -16,7 +16,7 @@ class RequestObjectFactory extends ParsedJwsFactory public function fromToken(string $token): RequestObject { return new RequestObject( - $this->jwsParser->parse($token), + $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, $this->jwksFactory, $this->jwsSerializerManagerDecorator, diff --git a/src/Federation/Factories/TrustMarkDelegationFactory.php b/src/Federation/Factories/TrustMarkDelegationFactory.php index 29b54cd..386d911 100644 --- a/src/Federation/Factories/TrustMarkDelegationFactory.php +++ b/src/Federation/Factories/TrustMarkDelegationFactory.php @@ -12,7 +12,7 @@ class TrustMarkDelegationFactory extends ParsedJwsFactory public function fromToken(string $token): TrustMarkDelegation { return new TrustMarkDelegation( - $this->jwsParser->parse($token), + $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, $this->jwksFactory, $this->jwsSerializerManagerDecorator, diff --git a/src/Federation/Factories/TrustMarkFactory.php b/src/Federation/Factories/TrustMarkFactory.php index de4c825..f3be3fe 100644 --- a/src/Federation/Factories/TrustMarkFactory.php +++ b/src/Federation/Factories/TrustMarkFactory.php @@ -15,7 +15,7 @@ public function fromToken( JwtTypesEnum $expectedJwtType = JwtTypesEnum::TrustMarkJwt, ): TrustMark { return new TrustMark( - $this->jwsParser->parse($token), + $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, $this->jwksFactory, $this->jwsSerializerManagerDecorator, diff --git a/src/Jwks.php b/src/Jwks.php index e1268c1..794edde 100644 --- a/src/Jwks.php +++ b/src/Jwks.php @@ -8,6 +8,7 @@ use GuzzleHttp\Client; use Psr\Log\LoggerInterface; use Psr\SimpleCache\CacheInterface; +use SimpleSAML\OpenID\Algorithms\AlgorithmManagerDecorator; use SimpleSAML\OpenID\Decorators\CacheDecorator; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Decorators\HttpClientDecorator; @@ -20,9 +21,9 @@ use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; use SimpleSAML\OpenID\Jwks\Factories\SignedJwksFactory; use SimpleSAML\OpenID\Jwks\JwksFetcher; -use SimpleSAML\OpenID\Jws\Factories\JwsParserFactory; +use SimpleSAML\OpenID\Jws\Factories\JwsDecoratorBuilderFactory; use SimpleSAML\OpenID\Jws\Factories\JwsVerifierDecoratorFactory; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; @@ -40,7 +41,7 @@ class Jwks protected ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null; - protected ?JwsParser $jwsParser = null; + protected ?JwsDecoratorBuilder $jwsDecoratorBuilder = null; protected ?JwsVerifierDecorator $jwsVerifierDecorator = null; @@ -54,7 +55,7 @@ class Jwks protected ?JwsSerializerManagerDecoratorFactory $jwsSerializerManagerDecoratorFactory = null; - protected ?JwsParserFactory $jwsParserFactory = null; + protected ?JwsDecoratorBuilderFactory $jwsDecoratorBuilderFactory = null; protected ?JwsVerifierDecoratorFactory $jwsVerifierDecoratorFactory = null; @@ -65,6 +66,7 @@ class Jwks protected ?HttpClientDecoratorFactory $httpClientDecoratorFactory = null; protected ?ClaimFactory $claimFactory = null; + protected ?AlgorithmManagerDecorator $algorithmManagerDecorator = null; public function __construct( protected readonly SupportedAlgorithms $supportedAlgorithms = new SupportedAlgorithms(), @@ -90,7 +92,7 @@ public function jwksFactory(): JwksFactory public function signedJwksFactory(): SignedJwksFactory { return $this->signedJwksFactory ??= new SignedJwksFactory( - $this->jwsParser(), + $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), $this->jwksFactory(), $this->jwsSerializerManagerDecorator(), @@ -128,6 +130,13 @@ public function algorithmManagerDecoratorFactory(): AlgorithmManagerDecoratorFac return $this->algorithmManagerDecoratorFactory; } + public function algorithmManagerDecorator(): AlgorithmManagerDecorator + { + return $this->algorithmManagerDecorator ??= $this->algorithmManagerDecoratorFactory()->build( + $this->supportedAlgorithms, + ); + } + public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDecoratorFactory { if (is_null($this->jwsSerializerManagerDecoratorFactory)) { @@ -137,13 +146,13 @@ public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDeco return $this->jwsSerializerManagerDecoratorFactory; } - public function jwsParserFactory(): JwsParserFactory + public function jwsDecoratorBuilderFactory(): JwsDecoratorBuilderFactory { - if (is_null($this->jwsParserFactory)) { - $this->jwsParserFactory = new JwsParserFactory(); + if (is_null($this->jwsDecoratorBuilderFactory)) { + $this->jwsDecoratorBuilderFactory = new JwsDecoratorBuilderFactory(); } - return $this->jwsParserFactory; + return $this->jwsDecoratorBuilderFactory; } public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory @@ -189,9 +198,11 @@ public function jwsVerifierDecorator(): JwsVerifierDecorator ); } - public function jwsParser(): JwsParser + public function jwsDecoratorBuilder(): JwsDecoratorBuilder { - return $this->jwsParser ??= $this->jwsParserFactory()->build($this->jwsSerializerManagerDecorator()); + return $this->jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderFactory()->build( + $this->jwsSerializerManagerDecorator(), + ); } public function jwsSerializerManagerDecorator(): JwsSerializerManagerDecorator diff --git a/src/Jwks/Factories/SignedJwksFactory.php b/src/Jwks/Factories/SignedJwksFactory.php index 1ec7161..1c2c5b7 100644 --- a/src/Jwks/Factories/SignedJwksFactory.php +++ b/src/Jwks/Factories/SignedJwksFactory.php @@ -15,7 +15,7 @@ class SignedJwksFactory extends ParsedJwsFactory public function fromToken(string $token): SignedJwks { return new SignedJwks( - $this->jwsParser->parse($token), + $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, $this->jwksFactory, $this->jwsSerializerManagerDecorator, diff --git a/src/Jws/Factories/JwsDecoratorBuilderFactory.php b/src/Jws/Factories/JwsDecoratorBuilderFactory.php new file mode 100644 index 0000000..37d5ced --- /dev/null +++ b/src/Jws/Factories/JwsDecoratorBuilderFactory.php @@ -0,0 +1,27 @@ +algorithmManager()), + $helpers, + ); + } +} diff --git a/src/Jws/Factories/JwsParserFactory.php b/src/Jws/Factories/JwsParserFactory.php deleted file mode 100644 index d637963..0000000 --- a/src/Jws/Factories/JwsParserFactory.php +++ /dev/null @@ -1,16 +0,0 @@ -jwsParser->parse($token), + $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, $this->jwksFactory, $this->jwsSerializerManagerDecorator, diff --git a/src/Jws/JwsParser.php b/src/Jws/JwsDecoratorBuilder.php similarity index 72% rename from src/Jws/JwsParser.php rename to src/Jws/JwsDecoratorBuilder.php index 95d3dce..d992fdd 100644 --- a/src/Jws/JwsParser.php +++ b/src/Jws/JwsDecoratorBuilder.php @@ -4,21 +4,25 @@ namespace SimpleSAML\OpenID\Jws; +use Jose\Component\Signature\JWSBuilder; use SimpleSAML\OpenID\Exceptions\JwsException; +use SimpleSAML\OpenID\Helpers; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; use Throwable; -class JwsParser +class JwsDecoratorBuilder { public function __construct( protected readonly JwsSerializerManagerDecorator $serializerManagerDecorator, + protected readonly JWSBuilder $jwsBuilder, + protected readonly Helpers $helpers, ) { } /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ - public function parse(string $token): JwsDecorator + public function fromToken(string $token): JwsDecorator { try { return $this->serializerManagerDecorator->unserialize($token); diff --git a/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php b/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php new file mode 100644 index 0000000..220ee4b --- /dev/null +++ b/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php @@ -0,0 +1,15 @@ +createMock(JwsDecorator::class); $jwsDecoratorMock->method('jws')->willReturn($jwsMock); - $this->jwsParserMock = $this->createMock(JwsParser::class); - $this->jwsParserMock->method('parse')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksFactoryMock = $this->createMock(JwksFactory::class); @@ -94,7 +94,7 @@ protected function setUp(): void } protected function sut( - ?JwsParser $jwsParser = null, + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, ?JwksFactory $jwksFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, @@ -102,7 +102,7 @@ protected function sut( ?Helpers $helpers = null, ?ClaimFactory $claimFactory = null, ): ClientAssertionFactory { - $jwsParser ??= $this->jwsParserMock; + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; $jwksFactory ??= $this->jwksFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; @@ -111,7 +111,7 @@ protected function sut( $claimFactory ??= $this->claimFactoryMock; return new ClientAssertionFactory( - $jwsParser, + $jwsDecoratorBuilder, $jwsVerifierDecorator, $jwksFactory, $jwsSerializerManagerDecorator, diff --git a/tests/src/Core/Factories/RequestObjectFactoryTest.php b/tests/src/Core/Factories/RequestObjectFactoryTest.php index 44c3db1..30b780e 100644 --- a/tests/src/Core/Factories/RequestObjectFactoryTest.php +++ b/tests/src/Core/Factories/RequestObjectFactoryTest.php @@ -18,7 +18,7 @@ use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; @@ -32,7 +32,7 @@ final class RequestObjectFactoryTest extends TestCase - protected MockObject $jwsParserMock; + protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -63,8 +63,8 @@ protected function setUp(): void $jwsDecoratorMock = $this->createMock(JwsDecorator::class); $jwsDecoratorMock->method('jws')->willReturn($jwsMock); - $this->jwsParserMock = $this->createMock(JwsParser::class); - $this->jwsParserMock->method('parse')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksFactoryMock = $this->createMock(JwksFactory::class); @@ -79,7 +79,7 @@ protected function setUp(): void } protected function sut( - ?JwsParser $jwsParser = null, + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, ?JwksFactory $jwksFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, @@ -87,7 +87,7 @@ protected function sut( ?Helpers $helpers = null, ?ClaimFactory $claimFactory = null, ): RequestObjectFactory { - $jwsParser ??= $this->jwsParserMock; + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; $jwksFactory ??= $this->jwksFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; @@ -96,7 +96,7 @@ protected function sut( $claimFactory ??= $this->claimFactoryMock; return new RequestObjectFactory( - $jwsParser, + $jwsDecoratorBuilder, $jwsVerifierDecorator, $jwksFactory, $jwsSerializerManagerDecorator, diff --git a/tests/src/CoreTest.php b/tests/src/CoreTest.php index 4703139..fa87851 100644 --- a/tests/src/CoreTest.php +++ b/tests/src/CoreTest.php @@ -19,10 +19,10 @@ use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Factories\DateIntervalDecoratorFactory; use SimpleSAML\OpenID\Factories\JwsSerializerManagerDecoratorFactory; -use SimpleSAML\OpenID\Jws\Factories\JwsParserFactory; +use SimpleSAML\OpenID\Jws\Factories\JwsDecoratorBuilderFactory; use SimpleSAML\OpenID\Jws\Factories\JwsVerifierDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; use SimpleSAML\OpenID\SupportedAlgorithms; @@ -36,9 +36,9 @@ #[UsesClass(DateIntervalDecoratorFactory::class)] #[UsesClass(AlgorithmManagerDecoratorFactory::class)] #[UsesClass(JwsSerializerManagerDecoratorFactory::class)] -#[UsesClass(JwsParserFactory::class)] +#[UsesClass(JwsDecoratorBuilderFactory::class)] #[UsesClass(JwsVerifierDecoratorFactory::class)] -#[UsesClass(JwsParser::class)] +#[UsesClass(JwsDecoratorBuilder::class)] #[UsesClass(AlgorithmManagerDecorator::class)] #[UsesClass(JwsVerifierDecorator::class)] #[UsesClass(JwsSerializerManagerDecorator::class)] diff --git a/tests/src/Federation/Factories/EntityStatementFactoryTest.php b/tests/src/Federation/Factories/EntityStatementFactoryTest.php index d97cf9e..16337f3 100644 --- a/tests/src/Federation/Factories/EntityStatementFactoryTest.php +++ b/tests/src/Federation/Factories/EntityStatementFactoryTest.php @@ -18,7 +18,7 @@ use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; @@ -33,7 +33,7 @@ final class EntityStatementFactoryTest extends TestCase - protected MockObject $jwsParserMock; + protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -114,8 +114,8 @@ protected function setUp(): void $jwsDecoratorMock = $this->createMock(JwsDecorator::class); $jwsDecoratorMock->method('jws')->willReturn($jwsMock); - $this->jwsParserMock = $this->createMock(JwsParser::class); - $this->jwsParserMock->method('parse')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksFactoryMock = $this->createMock(JwksFactory::class); @@ -138,7 +138,7 @@ protected function setUp(): void } protected function sut( - ?JwsParser $jwsParser = null, + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, ?JwksFactory $jwksFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, @@ -146,7 +146,7 @@ protected function sut( ?Helpers $helpers = null, ?ClaimFactory $claimFactory = null, ): EntityStatementFactory { - $jwsParser ??= $this->jwsParserMock; + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; $jwksFactory ??= $this->jwksFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; @@ -155,7 +155,7 @@ protected function sut( $claimFactory ??= $this->claimFactoryMock; return new EntityStatementFactory( - $jwsParser, + $jwsDecoratorBuilder, $jwsVerifierDecorator, $jwksFactory, $jwsSerializerManagerDecorator, diff --git a/tests/src/Federation/Factories/RequestObjectFactoryTest.php b/tests/src/Federation/Factories/RequestObjectFactoryTest.php index 4d11d17..1b8969d 100644 --- a/tests/src/Federation/Factories/RequestObjectFactoryTest.php +++ b/tests/src/Federation/Factories/RequestObjectFactoryTest.php @@ -18,7 +18,7 @@ use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; @@ -33,7 +33,7 @@ final class RequestObjectFactoryTest extends TestCase - protected MockObject $jwsParserMock; + protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -82,8 +82,8 @@ protected function setUp(): void $jwsDecoratorMock = $this->createMock(JwsDecorator::class); $jwsDecoratorMock->method('jws')->willReturn($jwsMock); - $this->jwsParserMock = $this->createMock(JwsParser::class); - $this->jwsParserMock->method('parse')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksFactoryMock = $this->createMock(JwksFactory::class); @@ -105,7 +105,7 @@ protected function setUp(): void } protected function sut( - ?JwsParser $jwsParser = null, + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, ?JwksFactory $jwksFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, @@ -113,7 +113,7 @@ protected function sut( ?Helpers $helpers = null, ?ClaimFactory $claimFactory = null, ): RequestObjectFactory { - $jwsParser ??= $this->jwsParserMock; + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; $jwksFactory ??= $this->jwksFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; @@ -122,7 +122,7 @@ protected function sut( $claimFactory ??= $this->claimFactoryMock; return new RequestObjectFactory( - $jwsParser, + $jwsDecoratorBuilder, $jwsVerifierDecorator, $jwksFactory, $jwsSerializerManagerDecorator, diff --git a/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php b/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php index 52f8014..ae26e2f 100644 --- a/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php +++ b/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php @@ -17,7 +17,7 @@ use SimpleSAML\OpenID\Helpers; use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; @@ -29,7 +29,7 @@ final class TrustMarkDelegationFactoryTest extends TestCase - protected MockObject $jwsParserMock; + protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -77,8 +77,8 @@ protected function setUp(): void $jwsDecoratorMock = $this->createMock(JwsDecorator::class); $jwsDecoratorMock->method('jws')->willReturn($jwsMock); - $this->jwsParserMock = $this->createMock(JwsParser::class); - $this->jwsParserMock->method('parse')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksFactoryMock = $this->createMock(JwksFactory::class); @@ -101,7 +101,7 @@ protected function setUp(): void } protected function sut( - ?JwsParser $jwsParser = null, + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, ?JwksFactory $jwksFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, @@ -109,7 +109,7 @@ protected function sut( ?Helpers $helpers = null, ?ClaimFactory $claimFactory = null, ): TrustMarkDelegationFactory { - $jwsParser ??= $this->jwsParserMock; + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; $jwksFactory ??= $this->jwksFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerMock; @@ -118,7 +118,7 @@ protected function sut( $claimFactory ??= $this->claimFactoryMock; return new TrustMarkDelegationFactory( - $jwsParser, + $jwsDecoratorBuilder, $jwsVerifierDecorator, $jwksFactory, $jwsSerializerManagerDecorator, diff --git a/tests/src/Federation/Factories/TrustMarkFactoryTest.php b/tests/src/Federation/Factories/TrustMarkFactoryTest.php index d00719c..0698ce3 100644 --- a/tests/src/Federation/Factories/TrustMarkFactoryTest.php +++ b/tests/src/Federation/Factories/TrustMarkFactoryTest.php @@ -18,7 +18,7 @@ use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; @@ -33,7 +33,7 @@ final class TrustMarkFactoryTest extends TestCase - protected MockObject $jwsParserMock; + protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -80,8 +80,8 @@ protected function setUp(): void $jwsDecoratorMock = $this->createMock(JwsDecorator::class); $jwsDecoratorMock->method('jws')->willReturn($jwsMock); - $this->jwsParserMock = $this->createMock(JwsParser::class); - $this->jwsParserMock->method('parse')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksFactoryMock = $this->createMock(JwksFactory::class); @@ -104,7 +104,7 @@ protected function setUp(): void } protected function sut( - ?JwsParser $jwsParser = null, + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, ?JwksFactory $jwksFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, @@ -112,7 +112,7 @@ protected function sut( ?Helpers $helpers = null, ?ClaimFactory $claimFactory = null, ): TrustMarkFactory { - $jwsParser ??= $this->jwsParserMock; + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; $jwksFactory ??= $this->jwksFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerMock; @@ -121,7 +121,7 @@ protected function sut( $claimFactory ??= $this->claimFactoryMock; return new TrustMarkFactory( - $jwsParser, + $jwsDecoratorBuilder, $jwsVerifierDecorator, $jwksFactory, $jwsSerializerManagerDecorator, diff --git a/tests/src/FederationTest.php b/tests/src/FederationTest.php index 123a341..2f8cbdd 100644 --- a/tests/src/FederationTest.php +++ b/tests/src/FederationTest.php @@ -35,11 +35,11 @@ use SimpleSAML\OpenID\Federation\TrustMarkFetcher; use SimpleSAML\OpenID\Federation\TrustMarkValidator; use SimpleSAML\OpenID\Jws\AbstractJwsFetcher; -use SimpleSAML\OpenID\Jws\Factories\JwsParserFactory; +use SimpleSAML\OpenID\Jws\Factories\JwsDecoratorBuilderFactory; use SimpleSAML\OpenID\Jws\Factories\JwsVerifierDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsFetcher; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; use SimpleSAML\OpenID\SupportedAlgorithms; @@ -58,8 +58,8 @@ #[UsesClass(TrustMarkFactory::class)] #[UsesClass(AlgorithmManagerDecoratorFactory::class)] #[UsesClass(JwsSerializerManagerDecoratorFactory::class)] -#[UsesClass(JwsParserFactory::class)] -#[UsesClass(JwsParser::class)] +#[UsesClass(JwsDecoratorBuilderFactory::class)] +#[UsesClass(JwsDecoratorBuilder::class)] #[UsesClass(JwsVerifierDecoratorFactory::class)] #[UsesClass(DateIntervalDecorator::class)] #[UsesClass(DateIntervalDecoratorFactory::class)] diff --git a/tests/src/Jwks/Factories/SignedJwksFactoryTest.php b/tests/src/Jwks/Factories/SignedJwksFactoryTest.php index 447390e..4933f30 100644 --- a/tests/src/Jwks/Factories/SignedJwksFactoryTest.php +++ b/tests/src/Jwks/Factories/SignedJwksFactoryTest.php @@ -18,7 +18,7 @@ use SimpleSAML\OpenID\Jwks\SignedJwks; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; @@ -33,7 +33,7 @@ final class SignedJwksFactoryTest extends TestCase - protected MockObject $jwsParserMock; + protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -89,8 +89,8 @@ protected function setUp(): void $jwsDecoratorMock = $this->createMock(JwsDecorator::class); $jwsDecoratorMock->method('jws')->willReturn($jwsMock); - $this->jwsParserMock = $this->createMock(JwsParser::class); - $this->jwsParserMock->method('parse')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksFactoryMock = $this->createMock(JwksFactory::class); @@ -113,7 +113,7 @@ protected function setUp(): void } protected function sut( - ?JwsParser $jwsParser = null, + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, ?JwksFactory $jwksFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, @@ -121,7 +121,7 @@ protected function sut( ?Helpers $helpers = null, ?ClaimFactory $claimFactory = null, ): SignedJwksFactory { - $jwsParser ??= $this->jwsParserMock; + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; $jwksFactory ??= $this->jwksFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; @@ -130,7 +130,7 @@ protected function sut( $claimFactory ??= $this->claimFactoryMock; return new SignedJwksFactory( - $jwsParser, + $jwsDecoratorBuilder, $jwsVerifierDecorator, $jwksFactory, $jwsSerializerManagerDecorator, diff --git a/tests/src/JwksTest.php b/tests/src/JwksTest.php index 5cec8e9..bc03a16 100644 --- a/tests/src/JwksTest.php +++ b/tests/src/JwksTest.php @@ -26,10 +26,10 @@ use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; use SimpleSAML\OpenID\Jwks\Factories\SignedJwksFactory; use SimpleSAML\OpenID\Jwks\JwksFetcher; -use SimpleSAML\OpenID\Jws\Factories\JwsParserFactory; +use SimpleSAML\OpenID\Jws\Factories\JwsDecoratorBuilderFactory; use SimpleSAML\OpenID\Jws\Factories\JwsVerifierDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; use SimpleSAML\OpenID\SupportedAlgorithms; @@ -48,9 +48,9 @@ #[UsesClass(HttpClientDecoratorFactory::class)] #[UsesClass(AlgorithmManagerDecoratorFactory::class)] #[UsesClass(JwsSerializerManagerDecoratorFactory::class)] -#[UsesClass(JwsParserFactory::class)] +#[UsesClass(JwsDecoratorBuilderFactory::class)] #[UsesClass(JwsVerifierDecoratorFactory::class)] -#[UsesClass(JwsParser::class)] +#[UsesClass(JwsDecoratorBuilder::class)] #[UsesClass(AlgorithmManagerDecorator::class)] #[UsesClass(JwsVerifierDecorator::class)] #[UsesClass(JwsSerializerManagerDecorator::class)] diff --git a/tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php b/tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php new file mode 100644 index 0000000..4b2139c --- /dev/null +++ b/tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php @@ -0,0 +1,53 @@ +jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); + $this->algorithmMenagerDecoratorMock = $this->createMock(AlgorithmManagerDecorator::class); + $this->helpersMock = $this->createMock(Helpers::class); + } + + protected function sut(): JwsDecoratorBuilderFactory + { + return new JwsDecoratorBuilderFactory(); + } + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(JwsDecoratorBuilderFactory::class, $this->sut()); + } + + public function testCanBuild(): void + { + $this->assertInstanceOf( + JwsDecoratorBuilder::class, + $this->sut()->build( + $this->jwsSerializerManagerDecoratorMock, + $this->algorithmMenagerDecoratorMock, + $this->helpersMock, + ), + ); + } +} diff --git a/tests/src/Jws/Factories/JwsParserFactoryTest.php b/tests/src/Jws/Factories/JwsParserFactoryTest.php deleted file mode 100644 index ec803c3..0000000 --- a/tests/src/Jws/Factories/JwsParserFactoryTest.php +++ /dev/null @@ -1,43 +0,0 @@ -jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); - } - - protected function sut(): JwsParserFactory - { - return new JwsParserFactory(); - } - - public function testCanCreateInstance(): void - { - $this->assertInstanceOf(JwsParserFactory::class, $this->sut()); - } - - public function testCanBuild(): void - { - $this->assertInstanceOf( - JwsParser::class, - $this->sut()->build($this->jwsSerializerManagerDecoratorMock), - ); - } -} diff --git a/tests/src/Jws/Factories/ParsedJwsFactoryTest.php b/tests/src/Jws/Factories/ParsedJwsFactoryTest.php index d5024e2..e1933b7 100644 --- a/tests/src/Jws/Factories/ParsedJwsFactoryTest.php +++ b/tests/src/Jws/Factories/ParsedJwsFactoryTest.php @@ -13,7 +13,7 @@ use SimpleSAML\OpenID\Helpers; use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; @@ -22,7 +22,7 @@ #[UsesClass(ParsedJws::class)] final class ParsedJwsFactoryTest extends TestCase { - protected MockObject $jwsParserMock; + protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -38,7 +38,7 @@ final class ParsedJwsFactoryTest extends TestCase protected function setUp(): void { - $this->jwsParserMock = $this->createMock(JwsParser::class); + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksFactoryMock = $this->createMock(JwksFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); @@ -48,7 +48,7 @@ protected function setUp(): void } protected function sut( - ?JwsParser $jwsParser = null, + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, ?JwksFactory $jwksFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, @@ -56,7 +56,7 @@ protected function sut( ?Helpers $helpers = null, ?ClaimFactory $claimFactory = null, ): ParsedJwsFactory { - $jwsParser ??= $this->jwsParserMock; + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; $jwksFactory ??= $this->jwksFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; @@ -65,7 +65,7 @@ protected function sut( $claimFactory ??= $this->claimFactoryMock; return new ParsedJwsFactory( - $jwsParser, + $jwsDecoratorBuilder, $jwsVerifierDecorator, $jwksFactory, $jwsSerializerManagerDecorator, diff --git a/tests/src/Jws/JwsParserTest.php b/tests/src/Jws/JwsDecoratorBuilderTest.php similarity index 65% rename from tests/src/Jws/JwsParserTest.php rename to tests/src/Jws/JwsDecoratorBuilderTest.php index 0a72600..50d52b2 100644 --- a/tests/src/Jws/JwsParserTest.php +++ b/tests/src/Jws/JwsDecoratorBuilderTest.php @@ -4,40 +4,53 @@ namespace SimpleSAML\Test\OpenID\Jws; +use Jose\Component\Signature\JWSBuilder; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use SimpleSAML\OpenID\Exceptions\JwsException; +use SimpleSAML\OpenID\Helpers; use SimpleSAML\OpenID\Jws\JwsDecorator; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; -#[CoversClass(JwsParser::class)] +#[CoversClass(JwsDecoratorBuilder::class)] #[UsesClass(JwsDecorator::class)] -final class JwsParserTest extends TestCase +final class JwsDecoratorBuilderTest extends TestCase { protected MockObject $jwsSerializerManagerDecoratorMock; + protected MockObject $jwsBuilderMock; protected MockObject $jwsDecoratorMock; protected function setUp(): void { $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); + $this->jwsBuilderMock = $this->createMock(JWSBuilder::class); + $this->helpersMock = $this->createMock(Helpers::class); $this->jwsDecoratorMock = $this->createMock(JwsDecorator::class); } protected function sut( ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, - ): JwsParser { + ?JwsBuilder $jwsBuilder = null, + ?Helpers $helpers = null, + ): JwsDecoratorBuilder { $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; + $jwsBuilder ??= $this->jwsBuilderMock; + $helpers ??= $this->helpersMock; - return new JwsParser($jwsSerializerManagerDecorator); + return new JwsDecoratorBuilder( + $jwsSerializerManagerDecorator, + $jwsBuilder, + $helpers, + ); } public function testCanCreateInstance(): void { - $this->assertInstanceOf(JwsParser::class, $this->sut()); + $this->assertInstanceOf(JwsDecoratorBuilder::class, $this->sut()); } public function testCanParseToken(): void @@ -45,7 +58,7 @@ public function testCanParseToken(): void $this->jwsSerializerManagerDecoratorMock->expects($this->once())->method('unserialize') ->willReturn($this->jwsDecoratorMock); - $this->assertInstanceOf(JwsDecorator::class, $this->sut()->parse('token')); + $this->assertInstanceOf(JwsDecorator::class, $this->sut()->fromToken('token')); } public function testThrowsOnTokenParseError(): void @@ -56,6 +69,6 @@ public function testThrowsOnTokenParseError(): void $this->expectException(JwsException::class); $this->expectExceptionMessage('parse'); - $this->sut()->parse('token'); + $this->sut()->fromToken('token'); } } From 7631f7705288795e236bab070d8d2eba9cac577a Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Tue, 8 Apr 2025 19:48:57 +0200 Subject: [PATCH 02/66] Rename JwksFactory to JwksDecoratorFactory --- src/Core.php | 12 ++++---- src/Core/Factories/ClientAssertionFactory.php | 2 +- src/Core/Factories/RequestObjectFactory.php | 2 +- src/Federation.php | 16 +++++------ .../Factories/EntityStatementFactory.php | 2 +- .../Factories/RequestObjectFactory.php | 2 +- .../Factories/TrustMarkDelegationFactory.php | 2 +- src/Federation/Factories/TrustMarkFactory.php | 2 +- src/Federation/TrustMark.php | 6 ++-- src/Jwks.php | 12 ++++---- ...ksFactory.php => JwksDecoratorFactory.php} | 2 +- src/Jwks/Factories/SignedJwksFactory.php | 2 +- src/Jwks/JwksFetcher.php | 10 +++---- src/Jws/Factories/ParsedJwsFactory.php | 6 ++-- src/Jws/ParsedJws.php | 6 ++-- tests/src/Core/ClientAssertionTest.php | 12 ++++---- .../Factories/ClientAssertionFactoryTest.php | 12 ++++---- .../Factories/RequestObjectFactoryTest.php | 12 ++++---- tests/src/Core/RequestObjectTest.php | 12 ++++---- tests/src/Federation/EntityStatementTest.php | 12 ++++---- .../Factories/EntityStatementFactoryTest.php | 12 ++++---- .../Factories/RequestObjectFactoryTest.php | 12 ++++---- .../TrustMarkDelegationFactoryTest.php | 12 ++++---- .../Factories/TrustMarkFactoryTest.php | 12 ++++---- tests/src/Federation/RequestObjectTest.php | 12 ++++---- .../Federation/TrustMarkDelegationTest.php | 12 ++++---- tests/src/Federation/TrustMarkTest.php | 12 ++++---- ...yTest.php => JwksDecoratorFactoryTest.php} | 14 +++++----- .../Jwks/Factories/SignedJwksFactoryTest.php | 12 ++++---- tests/src/Jwks/JwksFetcherTest.php | 28 +++++++++---------- tests/src/Jwks/SignedJwksTest.php | 12 ++++---- tests/src/JwksTest.php | 6 ++-- .../Jws/Factories/ParsedJwsFactoryTest.php | 12 ++++---- tests/src/Jws/ParsedJwsTest.php | 16 +++++------ 34 files changed, 164 insertions(+), 164 deletions(-) rename src/Jwks/Factories/{JwksFactory.php => JwksDecoratorFactory.php} (93%) rename tests/src/Jwks/Factories/{JwksFactoryTest.php => JwksDecoratorFactoryTest.php} (78%) diff --git a/src/Core.php b/src/Core.php index 7acefab..207e5d9 100644 --- a/src/Core.php +++ b/src/Core.php @@ -16,7 +16,7 @@ use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Factories\DateIntervalDecoratorFactory; use SimpleSAML\OpenID\Factories\JwsSerializerManagerDecoratorFactory; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\JwsDecoratorBuilderFactory; use SimpleSAML\OpenID\Jws\Factories\JwsVerifierDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; @@ -47,7 +47,7 @@ class Core protected ?JwsVerifierDecoratorFactory $jwsVerifierDecoratorFactory = null; - protected ?JwksFactory $jwksFactory = null; + protected ?JwksDecoratorFactory $jwksDecoratorFactory = null; protected ?DateIntervalDecoratorFactory $dateIntervalDecoratorFactory = null; @@ -74,7 +74,7 @@ public function requestObjectFactory(): RequestObjectFactory return $this->requestObjectFactory ??= new RequestObjectFactory( $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), - $this->jwksFactory(), + $this->jwksDecoratorFactory(), $this->jwsSerializerManagerDecorator(), $this->timestampValidationLeewayDecorator, $this->helpers(), @@ -87,7 +87,7 @@ public function clientAssertionFactory(): ClientAssertionFactory return $this->clientAssertionFactory ??= new ClientAssertionFactory( $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), - $this->jwksFactory(), + $this->jwksDecoratorFactory(), $this->jwsSerializerManagerDecorator(), $this->timestampValidationLeewayDecorator, $this->helpers(), @@ -142,9 +142,9 @@ public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory return $this->jwsVerifierDecoratorFactory; } - public function jwksFactory(): JwksFactory + public function jwksDecoratorFactory(): JwksDecoratorFactory { - return $this->jwksFactory ??= new JwksFactory(); + return $this->jwksDecoratorFactory ??= new JwksDecoratorFactory(); } public function dateIntervalDecoratorFactory(): DateIntervalDecoratorFactory diff --git a/src/Core/Factories/ClientAssertionFactory.php b/src/Core/Factories/ClientAssertionFactory.php index dd7f46a..8852a1b 100644 --- a/src/Core/Factories/ClientAssertionFactory.php +++ b/src/Core/Factories/ClientAssertionFactory.php @@ -14,7 +14,7 @@ public function fromToken(string $token): ClientAssertion return new ClientAssertion( $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, - $this->jwksFactory, + $this->jwksDecoratorFactory, $this->jwsSerializerManagerDecorator, $this->timestampValidationLeeway, $this->helpers, diff --git a/src/Core/Factories/RequestObjectFactory.php b/src/Core/Factories/RequestObjectFactory.php index 8f02244..47020dd 100644 --- a/src/Core/Factories/RequestObjectFactory.php +++ b/src/Core/Factories/RequestObjectFactory.php @@ -14,7 +14,7 @@ public function fromToken(string $token): RequestObject return new RequestObject( $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, - $this->jwksFactory, + $this->jwksDecoratorFactory, $this->jwsSerializerManagerDecorator, $this->timestampValidationLeeway, $this->helpers, diff --git a/src/Federation.php b/src/Federation.php index 3f9aa63..4007f8e 100644 --- a/src/Federation.php +++ b/src/Federation.php @@ -30,7 +30,7 @@ use SimpleSAML\OpenID\Federation\TrustChainResolver; use SimpleSAML\OpenID\Federation\TrustMarkFetcher; use SimpleSAML\OpenID\Federation\TrustMarkValidator; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\JwsDecoratorBuilderFactory; use SimpleSAML\OpenID\Jws\Factories\JwsVerifierDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; @@ -82,7 +82,7 @@ class Federation protected ?JwsVerifierDecoratorFactory $jwsVerifierDecoratorFactory = null; - protected ?JwksFactory $jwksFactory = null; + protected ?JwksDecoratorFactory $jwksDecoratorFactory = null; protected ?DateIntervalDecoratorFactory $dateIntervalDecoratorFactory = null; @@ -193,7 +193,7 @@ public function entityStatementFactory(): EntityStatementFactory return $this->entityStatementFactory ??= new EntityStatementFactory( $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), - $this->jwksFactory(), + $this->jwksDecoratorFactory(), $this->jwsSerializerManagerDecorator(), $this->timestampValidationLeewayDecorator, $this->helpers(), @@ -206,7 +206,7 @@ public function requestObjectFactory(): RequestObjectFactory return $this->requestObjectFactory ??= new RequestObjectFactory( $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), - $this->jwksFactory(), + $this->jwksDecoratorFactory(), $this->jwsSerializerManagerDecorator(), $this->timestampValidationLeewayDecorator, $this->helpers(), @@ -219,7 +219,7 @@ public function trustMarkFactory(): TrustMarkFactory return $this->trustMarkFactory ??= new TrustMarkFactory( $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), - $this->jwksFactory(), + $this->jwksDecoratorFactory(), $this->jwsSerializerManagerDecorator(), $this->timestampValidationLeewayDecorator, $this->helpers(), @@ -232,7 +232,7 @@ public function trustMarkDelegationFactory(): TrustMarkDelegationFactory return $this->trustMarkDelegationFactory ?? new TrustMarkDelegationFactory( $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), - $this->jwksFactory(), + $this->jwksDecoratorFactory(), $this->jwsSerializerManagerDecorator(), $this->timestampValidationLeewayDecorator, $this->helpers(), @@ -294,9 +294,9 @@ public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory return $this->jwsVerifierDecoratorFactory ??= new JwsVerifierDecoratorFactory(); } - public function jwksFactory(): JwksFactory + public function jwksDecoratorFactory(): JwksDecoratorFactory { - return $this->jwksFactory ??= new JwksFactory(); + return $this->jwksDecoratorFactory ??= new JwksDecoratorFactory(); } public function dateIntervalDecoratorFactory(): DateIntervalDecoratorFactory diff --git a/src/Federation/Factories/EntityStatementFactory.php b/src/Federation/Factories/EntityStatementFactory.php index e6c77fd..4761dd2 100644 --- a/src/Federation/Factories/EntityStatementFactory.php +++ b/src/Federation/Factories/EntityStatementFactory.php @@ -18,7 +18,7 @@ public function fromToken(string $token): EntityStatement return new EntityStatement( $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, - $this->jwksFactory, + $this->jwksDecoratorFactory, $this->jwsSerializerManagerDecorator, $this->timestampValidationLeeway, $this->helpers, diff --git a/src/Federation/Factories/RequestObjectFactory.php b/src/Federation/Factories/RequestObjectFactory.php index d6d6792..f3de1bd 100644 --- a/src/Federation/Factories/RequestObjectFactory.php +++ b/src/Federation/Factories/RequestObjectFactory.php @@ -18,7 +18,7 @@ public function fromToken(string $token): RequestObject return new RequestObject( $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, - $this->jwksFactory, + $this->jwksDecoratorFactory, $this->jwsSerializerManagerDecorator, $this->timestampValidationLeeway, $this->helpers, diff --git a/src/Federation/Factories/TrustMarkDelegationFactory.php b/src/Federation/Factories/TrustMarkDelegationFactory.php index 386d911..c61445b 100644 --- a/src/Federation/Factories/TrustMarkDelegationFactory.php +++ b/src/Federation/Factories/TrustMarkDelegationFactory.php @@ -14,7 +14,7 @@ public function fromToken(string $token): TrustMarkDelegation return new TrustMarkDelegation( $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, - $this->jwksFactory, + $this->jwksDecoratorFactory, $this->jwsSerializerManagerDecorator, $this->timestampValidationLeeway, $this->helpers, diff --git a/src/Federation/Factories/TrustMarkFactory.php b/src/Federation/Factories/TrustMarkFactory.php index f3be3fe..07cc2b8 100644 --- a/src/Federation/Factories/TrustMarkFactory.php +++ b/src/Federation/Factories/TrustMarkFactory.php @@ -17,7 +17,7 @@ public function fromToken( return new TrustMark( $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, - $this->jwksFactory, + $this->jwksDecoratorFactory, $this->jwsSerializerManagerDecorator, $this->timestampValidationLeeway, $this->helpers, diff --git a/src/Federation/TrustMark.php b/src/Federation/TrustMark.php index d0bddba..3b5c03a 100644 --- a/src/Federation/TrustMark.php +++ b/src/Federation/TrustMark.php @@ -10,7 +10,7 @@ use SimpleSAML\OpenID\Exceptions\TrustMarkException; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; @@ -21,7 +21,7 @@ class TrustMark extends ParsedJws public function __construct( JwsDecorator $jwsDecorator, JwsVerifierDecorator $jwsVerifierDecorator, - JwksFactory $jwksFactory, + JwksDecoratorFactory $jwksDecoratorFactory, JwsSerializerManagerDecorator $jwsSerializerManagerDecorator, DateIntervalDecorator $timestampValidationLeeway, Helpers $helpers, @@ -31,7 +31,7 @@ public function __construct( parent::__construct( $jwsDecorator, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $timestampValidationLeeway, $helpers, diff --git a/src/Jwks.php b/src/Jwks.php index 794edde..ded3a56 100644 --- a/src/Jwks.php +++ b/src/Jwks.php @@ -18,7 +18,7 @@ use SimpleSAML\OpenID\Factories\DateIntervalDecoratorFactory; use SimpleSAML\OpenID\Factories\HttpClientDecoratorFactory; use SimpleSAML\OpenID\Factories\JwsSerializerManagerDecoratorFactory; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jwks\Factories\SignedJwksFactory; use SimpleSAML\OpenID\Jwks\JwksFetcher; use SimpleSAML\OpenID\Jws\Factories\JwsDecoratorBuilderFactory; @@ -45,7 +45,7 @@ class Jwks protected ?JwsVerifierDecorator $jwsVerifierDecorator = null; - protected ?JwksFactory $jwksFactory = null; + protected ?JwksDecoratorFactory $jwksDecoratorFactory = null; protected ?SignedJwksFactory $signedJwksFactory = null; @@ -84,9 +84,9 @@ public function __construct( $this->httpClientDecorator = $this->httpClientDecoratorFactory()->build($httpClient); } - public function jwksFactory(): JwksFactory + public function jwksDecoratorFactory(): JwksDecoratorFactory { - return $this->jwksFactory ??= new JwksFactory(); + return $this->jwksDecoratorFactory ??= new JwksDecoratorFactory(); } public function signedJwksFactory(): SignedJwksFactory @@ -94,7 +94,7 @@ public function signedJwksFactory(): SignedJwksFactory return $this->signedJwksFactory ??= new SignedJwksFactory( $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), - $this->jwksFactory(), + $this->jwksDecoratorFactory(), $this->jwsSerializerManagerDecorator(), $this->timestampValidationLeewayDecorator, $this->helpers(), @@ -106,7 +106,7 @@ public function jwksFetcher(): JwksFetcher { return $this->jwksFetcher ??= new JwksFetcher( $this->httpClientDecorator, - $this->jwksFactory(), + $this->jwksDecoratorFactory(), $this->signedJwksFactory(), $this->maxCacheDurationDecorator, $this->helpers(), diff --git a/src/Jwks/Factories/JwksFactory.php b/src/Jwks/Factories/JwksDecoratorFactory.php similarity index 93% rename from src/Jwks/Factories/JwksFactory.php rename to src/Jwks/Factories/JwksDecoratorFactory.php index 8cd2b95..69826c7 100644 --- a/src/Jwks/Factories/JwksFactory.php +++ b/src/Jwks/Factories/JwksDecoratorFactory.php @@ -7,7 +7,7 @@ use Jose\Component\Core\JWKSet; use SimpleSAML\OpenID\Jwks\JwksDecorator; -class JwksFactory +class JwksDecoratorFactory { /** * @phpstan-ignore missingType.iterableValue (JWKS array is validated later) diff --git a/src/Jwks/Factories/SignedJwksFactory.php b/src/Jwks/Factories/SignedJwksFactory.php index 1c2c5b7..a9df487 100644 --- a/src/Jwks/Factories/SignedJwksFactory.php +++ b/src/Jwks/Factories/SignedJwksFactory.php @@ -17,7 +17,7 @@ public function fromToken(string $token): SignedJwks return new SignedJwks( $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, - $this->jwksFactory, + $this->jwksDecoratorFactory, $this->jwsSerializerManagerDecorator, $this->timestampValidationLeeway, $this->helpers, diff --git a/src/Jwks/JwksFetcher.php b/src/Jwks/JwksFetcher.php index 88ea470..5bbb120 100644 --- a/src/Jwks/JwksFetcher.php +++ b/src/Jwks/JwksFetcher.php @@ -13,7 +13,7 @@ use SimpleSAML\OpenID\Exceptions\JwksException; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jwks\Factories\SignedJwksFactory; use Throwable; @@ -21,7 +21,7 @@ class JwksFetcher { public function __construct( protected readonly HttpClientDecorator $httpClientDecorator, - protected readonly JwksFactory $jwksFactory, + protected readonly JwksDecoratorFactory $jwksDecoratorFactory, protected readonly SignedJwksFactory $signedJwksFactory, protected readonly DateIntervalDecorator $maxCacheDurationDecorator, protected readonly Helpers $helpers, @@ -92,7 +92,7 @@ public function fromCache(string $uri): ?JwksDecorator $this->logger?->debug('JWKS JSON decoded, proceeding to instance building.', ['uri' => $uri, 'jwks' => $jwks]); - return $this->jwksFactory->fromKeyData($jwks); + return $this->jwksDecoratorFactory->fromKeyData($jwks); } /** @@ -152,7 +152,7 @@ public function fromJwksUri(string $uri): ?JwksDecorator $this->logger?->debug('Proceeding to instance building.', ['uri' => $uri, 'jwks' => $jwks]); - return $this->jwksFactory->fromKeyData($jwks); + return $this->jwksDecoratorFactory->fromKeyData($jwks); } /** @@ -216,6 +216,6 @@ public function fromSignedJwksUri(string $uri, array $federationJwks): ?JwksDeco ); } - return $this->jwksFactory->fromKeyData($signedJwks->jsonSerialize()); + return $this->jwksDecoratorFactory->fromKeyData($signedJwks->jsonSerialize()); } } diff --git a/src/Jws/Factories/ParsedJwsFactory.php b/src/Jws/Factories/ParsedJwsFactory.php index 41d5e01..c1920c2 100644 --- a/src/Jws/Factories/ParsedJwsFactory.php +++ b/src/Jws/Factories/ParsedJwsFactory.php @@ -7,7 +7,7 @@ use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; @@ -18,7 +18,7 @@ class ParsedJwsFactory public function __construct( protected readonly JwsDecoratorBuilder $jwsDecoratorBuilder, protected readonly JwsVerifierDecorator $jwsVerifierDecorator, - protected readonly JwksFactory $jwksFactory, + protected readonly JwksDecoratorFactory $jwksDecoratorFactory, protected readonly JwsSerializerManagerDecorator $jwsSerializerManagerDecorator, protected readonly DateIntervalDecorator $timestampValidationLeeway, protected readonly Helpers $helpers, @@ -34,7 +34,7 @@ public function fromToken(string $token): ParsedJws return new ParsedJws( $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, - $this->jwksFactory, + $this->jwksDecoratorFactory, $this->jwsSerializerManagerDecorator, $this->timestampValidationLeeway, $this->helpers, diff --git a/src/Jws/ParsedJws.php b/src/Jws/ParsedJws.php index 62fa534..f1296dc 100644 --- a/src/Jws/ParsedJws.php +++ b/src/Jws/ParsedJws.php @@ -10,7 +10,7 @@ use SimpleSAML\OpenID\Exceptions\JwsException; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Serializers\JwsSerializerEnum; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; use Throwable; @@ -32,7 +32,7 @@ class ParsedJws public function __construct( protected readonly JwsDecorator $jwsDecorator, protected readonly JwsVerifierDecorator $jwsVerifierDecorator, - protected readonly JwksFactory $jwksFactory, + protected readonly JwksDecoratorFactory $jwksDecoratorFactory, protected readonly JwsSerializerManagerDecorator $jwsSerializerManagerDecorator, protected readonly DateIntervalDecorator $timestampValidationLeeway, protected readonly Helpers $helpers, @@ -139,7 +139,7 @@ public function verifyWithKeySet(array $jwks, int $signatureIndex = 0): void if ( !$this->jwsVerifierDecorator->verifyWithKeySet( $this->jwsDecorator, - $this->jwksFactory->fromKeyData($jwks), + $this->jwksDecoratorFactory->fromKeyData($jwks), $signatureIndex, ) ) { diff --git a/tests/src/Core/ClientAssertionTest.php b/tests/src/Core/ClientAssertionTest.php index dde787f..52e1f7e 100644 --- a/tests/src/Core/ClientAssertionTest.php +++ b/tests/src/Core/ClientAssertionTest.php @@ -14,7 +14,7 @@ use SimpleSAML\OpenID\Exceptions\JwsException; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; @@ -28,7 +28,7 @@ final class ClientAssertionTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -67,7 +67,7 @@ protected function setUp(): void $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -89,7 +89,7 @@ protected function setUp(): void protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -97,7 +97,7 @@ protected function sut( ): ClientAssertion { $jwsDecorator ??= $this->jwsDecoratorMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -106,7 +106,7 @@ protected function sut( return new ClientAssertion( $jwsDecorator, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Core/Factories/ClientAssertionFactoryTest.php b/tests/src/Core/Factories/ClientAssertionFactoryTest.php index e989c3f..1763032 100644 --- a/tests/src/Core/Factories/ClientAssertionFactoryTest.php +++ b/tests/src/Core/Factories/ClientAssertionFactoryTest.php @@ -14,7 +14,7 @@ use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; @@ -32,7 +32,7 @@ final class ClientAssertionFactoryTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -74,7 +74,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -96,7 +96,7 @@ protected function setUp(): void protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -104,7 +104,7 @@ protected function sut( ): ClientAssertionFactory { $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -113,7 +113,7 @@ protected function sut( return new ClientAssertionFactory( $jwsDecoratorBuilder, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Core/Factories/RequestObjectFactoryTest.php b/tests/src/Core/Factories/RequestObjectFactoryTest.php index 30b780e..0160b8a 100644 --- a/tests/src/Core/Factories/RequestObjectFactoryTest.php +++ b/tests/src/Core/Factories/RequestObjectFactoryTest.php @@ -15,7 +15,7 @@ use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; @@ -36,7 +36,7 @@ final class RequestObjectFactoryTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -67,7 +67,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -81,7 +81,7 @@ protected function setUp(): void protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -89,7 +89,7 @@ protected function sut( ): RequestObjectFactory { $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -98,7 +98,7 @@ protected function sut( return new RequestObjectFactory( $jwsDecoratorBuilder, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Core/RequestObjectTest.php b/tests/src/Core/RequestObjectTest.php index 46743c7..dea2eab 100644 --- a/tests/src/Core/RequestObjectTest.php +++ b/tests/src/Core/RequestObjectTest.php @@ -15,7 +15,7 @@ use SimpleSAML\OpenID\Exceptions\RequestObjectException; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; @@ -32,7 +32,7 @@ final class RequestObjectTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -61,7 +61,7 @@ protected function setUp(): void $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -79,7 +79,7 @@ protected function setUp(): void protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -87,7 +87,7 @@ protected function sut( ): RequestObject { $jwsDecorator ??= $this->jwsDecoratorMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -96,7 +96,7 @@ protected function sut( return new RequestObject( $jwsDecorator, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Federation/EntityStatementTest.php b/tests/src/Federation/EntityStatementTest.php index 22ebc36..bfb436a 100644 --- a/tests/src/Federation/EntityStatementTest.php +++ b/tests/src/Federation/EntityStatementTest.php @@ -16,7 +16,7 @@ use SimpleSAML\OpenID\Federation\EntityStatement; use SimpleSAML\OpenID\Federation\Factories\FederationClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; @@ -33,7 +33,7 @@ final class EntityStatementTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -122,7 +122,7 @@ protected function setUp(): void $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -149,7 +149,7 @@ protected function setUp(): void protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -157,7 +157,7 @@ protected function sut( ): EntityStatement { $jwsDecorator ??= $this->jwsDecoratorMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -166,7 +166,7 @@ protected function sut( return new EntityStatement( $jwsDecorator, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Federation/Factories/EntityStatementFactoryTest.php b/tests/src/Federation/Factories/EntityStatementFactoryTest.php index 16337f3..3a24be8 100644 --- a/tests/src/Federation/Factories/EntityStatementFactoryTest.php +++ b/tests/src/Federation/Factories/EntityStatementFactoryTest.php @@ -15,7 +15,7 @@ use SimpleSAML\OpenID\Federation\EntityStatement; use SimpleSAML\OpenID\Federation\Factories\EntityStatementFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; @@ -37,7 +37,7 @@ final class EntityStatementFactoryTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -118,7 +118,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -140,7 +140,7 @@ protected function setUp(): void protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -148,7 +148,7 @@ protected function sut( ): EntityStatementFactory { $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -157,7 +157,7 @@ protected function sut( return new EntityStatementFactory( $jwsDecoratorBuilder, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Federation/Factories/RequestObjectFactoryTest.php b/tests/src/Federation/Factories/RequestObjectFactoryTest.php index 1b8969d..9c596fd 100644 --- a/tests/src/Federation/Factories/RequestObjectFactoryTest.php +++ b/tests/src/Federation/Factories/RequestObjectFactoryTest.php @@ -15,7 +15,7 @@ use SimpleSAML\OpenID\Federation\Factories\RequestObjectFactory; use SimpleSAML\OpenID\Federation\RequestObject; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; @@ -37,7 +37,7 @@ final class RequestObjectFactoryTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -86,7 +86,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -107,7 +107,7 @@ protected function setUp(): void protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -115,7 +115,7 @@ protected function sut( ): RequestObjectFactory { $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -124,7 +124,7 @@ protected function sut( return new RequestObjectFactory( $jwsDecoratorBuilder, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php b/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php index ae26e2f..5aeff43 100644 --- a/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php +++ b/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php @@ -15,7 +15,7 @@ use SimpleSAML\OpenID\Federation\Factories\TrustMarkDelegationFactory; use SimpleSAML\OpenID\Federation\TrustMarkDelegation; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; @@ -33,7 +33,7 @@ final class TrustMarkDelegationFactoryTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerMock; @@ -81,7 +81,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -103,7 +103,7 @@ protected function setUp(): void protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -111,7 +111,7 @@ protected function sut( ): TrustMarkDelegationFactory { $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -120,7 +120,7 @@ protected function sut( return new TrustMarkDelegationFactory( $jwsDecoratorBuilder, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Federation/Factories/TrustMarkFactoryTest.php b/tests/src/Federation/Factories/TrustMarkFactoryTest.php index 0698ce3..3f976bf 100644 --- a/tests/src/Federation/Factories/TrustMarkFactoryTest.php +++ b/tests/src/Federation/Factories/TrustMarkFactoryTest.php @@ -15,7 +15,7 @@ use SimpleSAML\OpenID\Federation\Factories\TrustMarkFactory; use SimpleSAML\OpenID\Federation\TrustMark; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; @@ -37,7 +37,7 @@ final class TrustMarkFactoryTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerMock; @@ -84,7 +84,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -106,7 +106,7 @@ protected function setUp(): void protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -114,7 +114,7 @@ protected function sut( ): TrustMarkFactory { $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -123,7 +123,7 @@ protected function sut( return new TrustMarkFactory( $jwsDecoratorBuilder, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Federation/RequestObjectTest.php b/tests/src/Federation/RequestObjectTest.php index 45629dc..efc34a3 100644 --- a/tests/src/Federation/RequestObjectTest.php +++ b/tests/src/Federation/RequestObjectTest.php @@ -15,7 +15,7 @@ use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Federation\RequestObject; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; @@ -29,7 +29,7 @@ final class RequestObjectTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -69,7 +69,7 @@ protected function setUp(): void $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -91,7 +91,7 @@ protected function setUp(): void protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -99,7 +99,7 @@ protected function sut( ): RequestObject { $jwsDecorator ??= $this->jwsDecoratorMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -108,7 +108,7 @@ protected function sut( return new RequestObject( $jwsDecorator, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Federation/TrustMarkDelegationTest.php b/tests/src/Federation/TrustMarkDelegationTest.php index 38df66a..62d4ee3 100644 --- a/tests/src/Federation/TrustMarkDelegationTest.php +++ b/tests/src/Federation/TrustMarkDelegationTest.php @@ -14,7 +14,7 @@ use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Federation\TrustMarkDelegation; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; @@ -31,7 +31,7 @@ final class TrustMarkDelegationTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -76,7 +76,7 @@ protected function setUp(): void $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -98,7 +98,7 @@ protected function setUp(): void protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -106,7 +106,7 @@ protected function sut( ): TrustMarkDelegation { $jwsDecorator ??= $this->jwsDecoratorMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -115,7 +115,7 @@ protected function sut( return new TrustMarkDelegation( $jwsDecorator, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Federation/TrustMarkTest.php b/tests/src/Federation/TrustMarkTest.php index 8688aeb..9f9e610 100644 --- a/tests/src/Federation/TrustMarkTest.php +++ b/tests/src/Federation/TrustMarkTest.php @@ -16,7 +16,7 @@ use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Federation\TrustMark; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; @@ -33,7 +33,7 @@ final class TrustMarkTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -79,7 +79,7 @@ protected function setUp(): void $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -103,7 +103,7 @@ protected function setUp(): void protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -112,7 +112,7 @@ protected function sut( ): TrustMark { $jwsDecorator ??= $this->jwsDecoratorMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -122,7 +122,7 @@ protected function sut( return new TrustMark( $jwsDecorator, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Jwks/Factories/JwksFactoryTest.php b/tests/src/Jwks/Factories/JwksDecoratorFactoryTest.php similarity index 78% rename from tests/src/Jwks/Factories/JwksFactoryTest.php rename to tests/src/Jwks/Factories/JwksDecoratorFactoryTest.php index 15b3f49..8e94411 100644 --- a/tests/src/Jwks/Factories/JwksFactoryTest.php +++ b/tests/src/Jwks/Factories/JwksDecoratorFactoryTest.php @@ -8,13 +8,13 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use SimpleSAML\OpenID\Jwks; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jwks\JwksDecorator; -#[CoversClass(JwksFactory::class)] +#[CoversClass(JwksDecoratorFactory::class)] #[UsesClass(JwksDecorator::class)] -#[UsesClass(JwksFactory::class)] -final class JwksFactoryTest extends TestCase +#[UsesClass(JwksDecoratorFactory::class)] +final class JwksDecoratorFactoryTest extends TestCase { protected array $jwksArraySample = [ 'keys' => [ @@ -34,14 +34,14 @@ protected function setUp(): void { } - protected function sut(): JwksFactory + protected function sut(): JwksDecoratorFactory { - return new JwksFactory(); + return new JwksDecoratorFactory(); } public function testCanCreateInstance(): void { - $this->assertInstanceOf(JwksFactory::class, $this->sut()); + $this->assertInstanceOf(JwksDecoratorFactory::class, $this->sut()); } public function testCanBuildFromKeyData(): void diff --git a/tests/src/Jwks/Factories/SignedJwksFactoryTest.php b/tests/src/Jwks/Factories/SignedJwksFactoryTest.php index 4933f30..33deed0 100644 --- a/tests/src/Jwks/Factories/SignedJwksFactoryTest.php +++ b/tests/src/Jwks/Factories/SignedJwksFactoryTest.php @@ -13,7 +13,7 @@ use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jwks\Factories\SignedJwksFactory; use SimpleSAML\OpenID\Jwks\SignedJwks; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; @@ -37,7 +37,7 @@ final class SignedJwksFactoryTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -93,7 +93,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); $this->helpersMock = $this->createMock(Helpers::class); @@ -115,7 +115,7 @@ protected function setUp(): void protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -123,7 +123,7 @@ protected function sut( ): SignedJwksFactory { $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -132,7 +132,7 @@ protected function sut( return new SignedJwksFactory( $jwsDecoratorBuilder, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Jwks/JwksFetcherTest.php b/tests/src/Jwks/JwksFetcherTest.php index 1e7fb05..666ff51 100644 --- a/tests/src/Jwks/JwksFetcherTest.php +++ b/tests/src/Jwks/JwksFetcherTest.php @@ -19,7 +19,7 @@ use SimpleSAML\OpenID\Exceptions\HttpException; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jwks\Factories\SignedJwksFactory; use SimpleSAML\OpenID\Jwks\JwksDecorator; use SimpleSAML\OpenID\Jwks\JwksFetcher; @@ -30,7 +30,7 @@ final class JwksFetcherTest extends TestCase { protected MockObject $httpClientDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $signedJwksFactoryMock; @@ -72,7 +72,7 @@ final class JwksFetcherTest extends TestCase protected function setUp(): void { $this->httpClientDecoratorMock = $this->createMock(HttpClientDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->signedJwksFactoryMock = $this->createMock(SignedJwksFactory::class); $this->maxCacheDurationDecoratorMock = $this->createMock(DateIntervalDecorator::class); $this->helpersMock = $this->createMock(Helpers::class); @@ -97,7 +97,7 @@ protected function setUp(): void protected function sut( ?HttpClientDecorator $httpClientDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?SignedJwksFactory $signedJwksFactory = null, ?DateIntervalDecorator $maxCacheDurationDecorator = null, ?Helpers $helpers = null, @@ -106,7 +106,7 @@ protected function sut( ?LoggerInterface $logger = null, ): JwksFetcher { $httpClientDecorator ??= $this->httpClientDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $signedJwksFactory ??= $this->signedJwksFactoryMock; $maxCacheDurationDecorator ??= $this->maxCacheDurationDecoratorMock; $helpers ??= $this->helpersMock; @@ -116,7 +116,7 @@ protected function sut( return new JwksFetcher( $httpClientDecorator, - $jwksFactory, + $jwksDecoratorFactory, $signedJwksFactory, $maxCacheDurationDecorator, $helpers, @@ -149,7 +149,7 @@ public function testCanGetFromCache(): void ->method('getValue') ->willReturn($this->jwksArraySample); - $this->jwksFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') ->with($this->jwksArraySample); $this->assertInstanceOf(JwksDecorator::class, $this->sut()->fromCache('uri')); @@ -213,7 +213,7 @@ public function testCanGetFromJwksUri(): void ->method('getValue') ->willReturn($this->jwksArraySample); - $this->jwksFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') ->with($this->jwksArraySample); $this->cacheDecoratorMock->expects($this->once())->method('set') @@ -274,7 +274,7 @@ public function testJwksUriLogsErrorInCaseOfCacheSetError(): void ->method('getValue') ->willReturn($this->jwksArraySample); - $this->jwksFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') ->with($this->jwksArraySample); $this->cacheDecoratorMock->expects($this->once())->method('set') @@ -314,7 +314,7 @@ public function testCanGetFromCacheOrJwksUri(): void ->method('getValue') ->willReturn($this->jwksArraySample); - $this->jwksFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') ->with($this->jwksArraySample); $this->assertInstanceOf(JwksDecorator::class, $this->sut()->fromCacheOrJwksUri('uri')); @@ -340,7 +340,7 @@ public function testCanGetFromSignedJwksUri(): void ->with($this->jwksArraySample) ->willReturn('jwks-json'); - $this->jwksFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') ->with($this->jwksArraySample); $this->cacheDecoratorMock->expects($this->once())->method('set') @@ -372,7 +372,7 @@ public function testSignedJwksUriTakesExpClaimIntoAccountForCaching(): void ->with($this->jwksArraySample) ->willReturn('jwks-json'); - $this->jwksFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') ->with($this->jwksArraySample); $this->maxCacheDurationDecoratorMock->expects($this->once()) @@ -417,7 +417,7 @@ public function testSignedJwksUriLogsErrorOnCacheSetError(): void ->with($this->jwksArraySample) ->willReturn('jwks-json'); - $this->jwksFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') ->with($this->jwksArraySample); $this->cacheDecoratorMock->expects($this->once())->method('set') @@ -453,7 +453,7 @@ public function testCanGetFromCacheOrSignedJwksUri(): void ->with($this->jwksArraySample) ->willReturn('jwks-json'); - $this->jwksFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') ->with($this->jwksArraySample); $this->cacheDecoratorMock->expects($this->once())->method('set') diff --git a/tests/src/Jwks/SignedJwksTest.php b/tests/src/Jwks/SignedJwksTest.php index 8a00582..073a47d 100644 --- a/tests/src/Jwks/SignedJwksTest.php +++ b/tests/src/Jwks/SignedJwksTest.php @@ -13,7 +13,7 @@ use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jwks\SignedJwks; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; @@ -31,7 +31,7 @@ final class SignedJwksTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -84,7 +84,7 @@ protected function setUp(): void $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -106,7 +106,7 @@ protected function setUp(): void protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -114,7 +114,7 @@ protected function sut( ): SignedJwks { $jwsDecorator ??= $this->jwsDecoratorMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -123,7 +123,7 @@ protected function sut( return new SignedJwks( $jwsDecorator, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/JwksTest.php b/tests/src/JwksTest.php index bc03a16..beddec6 100644 --- a/tests/src/JwksTest.php +++ b/tests/src/JwksTest.php @@ -23,7 +23,7 @@ use SimpleSAML\OpenID\Factories\HttpClientDecoratorFactory; use SimpleSAML\OpenID\Factories\JwsSerializerManagerDecoratorFactory; use SimpleSAML\OpenID\Jwks; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jwks\Factories\SignedJwksFactory; use SimpleSAML\OpenID\Jwks\JwksFetcher; use SimpleSAML\OpenID\Jws\Factories\JwsDecoratorBuilderFactory; @@ -36,7 +36,7 @@ use SimpleSAML\OpenID\SupportedSerializers; #[CoversClass(Jwks::class)] -#[UsesClass(JwksFactory::class)] +#[UsesClass(JwksDecoratorFactory::class)] #[UsesClass(ParsedJwsFactory::class)] #[UsesClass(SignedJwksFactory::class)] #[UsesClass(JwksFetcher::class)] @@ -119,7 +119,7 @@ public function testCanBuildTools(): void { $sut = $this->sut(); - $this->assertInstanceOf(JwksFactory::class, $sut->jwksFactory()); + $this->assertInstanceOf(JwksDecoratorFactory::class, $sut->jwksDecoratorFactory()); $this->assertInstanceOf(SignedJwksFactory::class, $sut->signedJwksFactory()); $this->assertInstanceOf(JwksFetcher::class, $sut->jwksFetcher()); } diff --git a/tests/src/Jws/Factories/ParsedJwsFactoryTest.php b/tests/src/Jws/Factories/ParsedJwsFactoryTest.php index e1933b7..df2c935 100644 --- a/tests/src/Jws/Factories/ParsedJwsFactoryTest.php +++ b/tests/src/Jws/Factories/ParsedJwsFactoryTest.php @@ -11,7 +11,7 @@ use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; @@ -26,7 +26,7 @@ final class ParsedJwsFactoryTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -40,7 +40,7 @@ protected function setUp(): void { $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); $this->helpersMock = $this->createMock(Helpers::class); @@ -50,7 +50,7 @@ protected function setUp(): void protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -58,7 +58,7 @@ protected function sut( ): ParsedJwsFactory { $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -67,7 +67,7 @@ protected function sut( return new ParsedJwsFactory( $jwsDecoratorBuilder, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Jws/ParsedJwsTest.php b/tests/src/Jws/ParsedJwsTest.php index 8f3f573..7e66fe3 100644 --- a/tests/src/Jws/ParsedJwsTest.php +++ b/tests/src/Jws/ParsedJwsTest.php @@ -13,7 +13,7 @@ use SimpleSAML\OpenID\Exceptions\JwsException; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; @@ -26,7 +26,7 @@ final class ParsedJwsTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -106,7 +106,7 @@ protected function setUp(): void { $this->jwsDecoratorMock = $this->createMock(JwsDecorator::class); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->timestampValidationLeewayMock = $this->createMock(DateIntervalDecorator::class); $this->helpersMock = $this->createMock(Helpers::class); @@ -135,7 +135,7 @@ protected function setUp(): void protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $timestampValidationLeewayMock = null, ?Helpers $helpers = null, @@ -143,7 +143,7 @@ protected function sut( ): ParsedJws { $jwsDecorator ??= $this->jwsDecoratorMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $timestampValidationLeewayMock ??= $this->timestampValidationLeewayMock; $helpers ??= $this->helpersMock; @@ -152,7 +152,7 @@ protected function sut( return new ParsedJws( $jwsDecorator, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $timestampValidationLeewayMock, $helpers, @@ -170,7 +170,7 @@ public function testCanValidateByCallbacks(): void $sut = new class ( $this->jwsDecoratorMock, $this->jwsVerifierDecoratorMock, - $this->jwksFactoryMock, + $this->jwksDecoratorFactoryMock, $this->jwsSerializerManagerDecoratorMock, $this->timestampValidationLeewayMock, $this->helpersMock, @@ -198,7 +198,7 @@ public function testThrowsOnValidateByCallbacksError(): void new class ( $this->jwsDecoratorMock, $this->jwsVerifierDecoratorMock, - $this->jwksFactoryMock, + $this->jwksDecoratorFactoryMock, $this->jwsSerializerManagerDecoratorMock, $this->timestampValidationLeewayMock, $this->helpersMock, From e85095c7b2a7c481de614f4c6c95c85525b457bc Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Tue, 8 Apr 2025 21:15:14 +0200 Subject: [PATCH 03/66] Start with JWK --- src/Jwk/Factories/JwkDecoratorFactory.php | 90 +++++++++++++++++++ src/Jwk/JwkDecorator.php | 20 +++++ src/Jwks.php | 2 + .../Factories/JwsDecoratorBuilderFactory.php | 3 +- src/Jws/JwsDecoratorBuilder.php | 33 +++++++ .../Factories/JwtVcJsonFactory.php | 11 +-- src/VerifiableCredentials/JwtVcJson.php | 2 +- .../VerifiableCredentialInterface.php | 3 +- tests/src/FederationTest.php | 2 +- 9 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 src/Jwk/Factories/JwkDecoratorFactory.php create mode 100644 src/Jwk/JwkDecorator.php diff --git a/src/Jwk/Factories/JwkDecoratorFactory.php b/src/Jwk/Factories/JwkDecoratorFactory.php new file mode 100644 index 0000000..8ac479c --- /dev/null +++ b/src/Jwk/Factories/JwkDecoratorFactory.php @@ -0,0 +1,90 @@ +jwk; + } +} diff --git a/src/Jwks.php b/src/Jwks.php index ded3a56..878bdbd 100644 --- a/src/Jwks.php +++ b/src/Jwks.php @@ -202,6 +202,8 @@ public function jwsDecoratorBuilder(): JwsDecoratorBuilder { return $this->jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderFactory()->build( $this->jwsSerializerManagerDecorator(), + $this->algorithmManagerDecorator(), + $this->helpers(), ); } diff --git a/src/Jws/Factories/JwsDecoratorBuilderFactory.php b/src/Jws/Factories/JwsDecoratorBuilderFactory.php index 37d5ced..61a2c12 100644 --- a/src/Jws/Factories/JwsDecoratorBuilderFactory.php +++ b/src/Jws/Factories/JwsDecoratorBuilderFactory.php @@ -16,8 +16,7 @@ public function build( JwsSerializerManagerDecorator $jwsSerializerManagerDecorator, AlgorithmManagerDecorator $algorithmManagerDecorator, Helpers $helpers, - ): JwsDecoratorBuilder - { + ): JwsDecoratorBuilder { return new JwsDecoratorBuilder( $jwsSerializerManagerDecorator, new JWSBuilder($algorithmManagerDecorator->algorithmManager()), diff --git a/src/Jws/JwsDecoratorBuilder.php b/src/Jws/JwsDecoratorBuilder.php index d992fdd..68b630d 100644 --- a/src/Jws/JwsDecoratorBuilder.php +++ b/src/Jws/JwsDecoratorBuilder.php @@ -5,8 +5,11 @@ namespace SimpleSAML\OpenID\Jws; use Jose\Component\Signature\JWSBuilder; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; +use SimpleSAML\OpenID\Codebooks\ClaimsEnum; use SimpleSAML\OpenID\Exceptions\JwsException; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; use Throwable; @@ -30,4 +33,34 @@ public function fromToken(string $token): JwsDecorator throw new JwsException('Unable to parse token.', (int)$throwable->getCode(), $throwable); } } + + /** + * @param array $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function fromData( + JwkDecorator $signatureJwkDecorator, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ): JwsDecorator { + $header = array_merge( + $header, + [ClaimsEnum::Alg->value => $signatureAlgorithm->value], + ); + + try { + return new JwsDecorator( + $this->jwsBuilder->create()->withPayload( + $this->helpers->json()->encode($payload), + )->addSignature( + $signatureJwkDecorator->jwk(), + $header, + )->build(), + ); + } catch (Throwable $throwable) { + throw new JwsException('Unable to build JWS.', (int)$throwable->getCode(), $throwable); + } + } } diff --git a/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php b/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php index 220ee4b..aef73d4 100644 --- a/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php +++ b/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php @@ -5,11 +5,12 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\Factories; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; -use SimpleSAML\OpenID\VerifiableCredentials\JwtVcJson; class JwtVcJsonFactory extends ParsedJwsFactory { - public function fromData(): JwtVcJson - { - } -} \ No newline at end of file + // TODO mivanci Continue +// public function fromData(): JwtVcJson +// { +// return new JwtVcJson(); +// } +} diff --git a/src/VerifiableCredentials/JwtVcJson.php b/src/VerifiableCredentials/JwtVcJson.php index 79925e6..3108226 100644 --- a/src/VerifiableCredentials/JwtVcJson.php +++ b/src/VerifiableCredentials/JwtVcJson.php @@ -13,4 +13,4 @@ public function getCredentialFormatIdentifier(): CredentialFormatIdentifiersEnum { return CredentialFormatIdentifiersEnum::JwtVcJson; } -} \ No newline at end of file +} diff --git a/src/VerifiableCredentials/VerifiableCredentialInterface.php b/src/VerifiableCredentials/VerifiableCredentialInterface.php index cf5e31d..4eeab94 100644 --- a/src/VerifiableCredentials/VerifiableCredentialInterface.php +++ b/src/VerifiableCredentials/VerifiableCredentialInterface.php @@ -6,5 +6,4 @@ interface VerifiableCredentialInterface { - -} \ No newline at end of file +} diff --git a/tests/src/FederationTest.php b/tests/src/FederationTest.php index 2f8cbdd..c0dd41e 100644 --- a/tests/src/FederationTest.php +++ b/tests/src/FederationTest.php @@ -38,8 +38,8 @@ use SimpleSAML\OpenID\Jws\Factories\JwsDecoratorBuilderFactory; use SimpleSAML\OpenID\Jws\Factories\JwsVerifierDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; -use SimpleSAML\OpenID\Jws\JwsFetcher; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; +use SimpleSAML\OpenID\Jws\JwsFetcher; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; use SimpleSAML\OpenID\SupportedAlgorithms; From 803bacc9179e2e75eba63656559b9e925e93d690 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Wed, 9 Apr 2025 10:02:36 +0200 Subject: [PATCH 04/66] Fix styles --- src/Core.php | 1 + src/Federation.php | 1 + src/Jwks.php | 1 + .../Factories/JwsDecoratorBuilderFactoryTest.php | 15 ++++++++++++--- tests/src/Jws/JwsDecoratorBuilderTest.php | 3 +++ 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Core.php b/src/Core.php index 207e5d9..9e091f7 100644 --- a/src/Core.php +++ b/src/Core.php @@ -52,6 +52,7 @@ class Core protected ?DateIntervalDecoratorFactory $dateIntervalDecoratorFactory = null; protected ?ClaimFactory $claimFactory = null; + protected ?AlgorithmManagerDecorator $algorithmManagerDecorator = null; public function __construct( diff --git a/src/Federation.php b/src/Federation.php index 4007f8e..314fe91 100644 --- a/src/Federation.php +++ b/src/Federation.php @@ -101,6 +101,7 @@ class Federation protected ?TrustMarkValidator $trustMarkValidator = null; protected ?TrustMarkFetcher $trustMarkFetcher = null; + protected ?AlgorithmManagerDecorator $algorithmManagerDecorator = null; public function __construct( diff --git a/src/Jwks.php b/src/Jwks.php index 878bdbd..e75597a 100644 --- a/src/Jwks.php +++ b/src/Jwks.php @@ -66,6 +66,7 @@ class Jwks protected ?HttpClientDecoratorFactory $httpClientDecoratorFactory = null; protected ?ClaimFactory $claimFactory = null; + protected ?AlgorithmManagerDecorator $algorithmManagerDecorator = null; public function __construct( diff --git a/tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php b/tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php index 4b2139c..28b1e9e 100644 --- a/tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php +++ b/tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php @@ -4,6 +4,8 @@ namespace SimpleSAML\Test\OpenID\Jws\Factories; +use Jose\Component\Core\AlgorithmManager; +use Jose\Component\Signature\Algorithm\RS256; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; @@ -16,16 +18,23 @@ #[CoversClass(JwsDecoratorBuilderFactory::class)] #[UsesClass(JwsDecoratorBuilder::class)] +#[UsesClass(AlgorithmManagerDecorator::class)] final class JwsDecoratorBuilderFactoryTest extends TestCase { protected MockObject $jwsSerializerManagerDecoratorMock; - protected MockObject $algorithmMenagerDecoratorMock; + + protected AlgorithmManagerDecorator $algorithmManagerDecorator; + protected MockObject $helpersMock; protected function setUp(): void { $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); - $this->algorithmMenagerDecoratorMock = $this->createMock(AlgorithmManagerDecorator::class); + $this->algorithmManagerDecorator = new AlgorithmManagerDecorator( + new AlgorithmManager( // Final class, can't mock. + [new RS256()], + ), + ); $this->helpersMock = $this->createMock(Helpers::class); } @@ -45,7 +54,7 @@ public function testCanBuild(): void JwsDecoratorBuilder::class, $this->sut()->build( $this->jwsSerializerManagerDecoratorMock, - $this->algorithmMenagerDecoratorMock, + $this->algorithmManagerDecorator, $this->helpersMock, ), ); diff --git a/tests/src/Jws/JwsDecoratorBuilderTest.php b/tests/src/Jws/JwsDecoratorBuilderTest.php index 50d52b2..64f5138 100644 --- a/tests/src/Jws/JwsDecoratorBuilderTest.php +++ b/tests/src/Jws/JwsDecoratorBuilderTest.php @@ -20,8 +20,11 @@ final class JwsDecoratorBuilderTest extends TestCase { protected MockObject $jwsSerializerManagerDecoratorMock; + protected MockObject $jwsBuilderMock; + protected MockObject $helpersMock; + protected MockObject $jwsDecoratorMock; protected function setUp(): void From 37c62c786650c6939a26cd1bc81d4af131f810e2 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Wed, 9 Apr 2025 14:39:50 +0200 Subject: [PATCH 05/66] Propagate fromData JWS factory method --- src/Core/Factories/ClientAssertionFactory.php | 29 ++++++++++++ src/Core/Factories/RequestObjectFactory.php | 29 ++++++++++++ .../Factories/EntityStatementFactory.php | 29 ++++++++++++ .../Factories/RequestObjectFactory.php | 29 ++++++++++++ .../Factories/TrustMarkDelegationFactory.php | 29 ++++++++++++ src/Federation/Factories/TrustMarkFactory.php | 31 ++++++++++++ src/Jwks/Factories/SignedJwksFactory.php | 29 ++++++++++++ src/Jws/Factories/ParsedJwsFactory.php | 29 ++++++++++++ src/Jws/JwsDecoratorBuilder.php | 4 +- .../Factories/JwtVcJsonFactory.php | 47 +++++++++++++++++-- 10 files changed, 278 insertions(+), 7 deletions(-) diff --git a/src/Core/Factories/ClientAssertionFactory.php b/src/Core/Factories/ClientAssertionFactory.php index 8852a1b..a3cab0d 100644 --- a/src/Core/Factories/ClientAssertionFactory.php +++ b/src/Core/Factories/ClientAssertionFactory.php @@ -4,7 +4,9 @@ namespace SimpleSAML\OpenID\Core\Factories; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Core\ClientAssertion; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; class ClientAssertionFactory extends ParsedJwsFactory @@ -21,4 +23,31 @@ public function fromToken(string $token): ClientAssertion $this->claimFactory, ); } + + /** + * @param array $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ): ClientAssertion { + return new ClientAssertion( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + ); + } } diff --git a/src/Core/Factories/RequestObjectFactory.php b/src/Core/Factories/RequestObjectFactory.php index 47020dd..961506f 100644 --- a/src/Core/Factories/RequestObjectFactory.php +++ b/src/Core/Factories/RequestObjectFactory.php @@ -4,7 +4,9 @@ namespace SimpleSAML\OpenID\Core\Factories; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Core\RequestObject; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; class RequestObjectFactory extends ParsedJwsFactory @@ -21,4 +23,31 @@ public function fromToken(string $token): RequestObject $this->claimFactory, ); } + + /** + * @param array $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ): RequestObject { + return new RequestObject( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + ); + } } diff --git a/src/Federation/Factories/EntityStatementFactory.php b/src/Federation/Factories/EntityStatementFactory.php index 4761dd2..5cfba3c 100644 --- a/src/Federation/Factories/EntityStatementFactory.php +++ b/src/Federation/Factories/EntityStatementFactory.php @@ -4,7 +4,9 @@ namespace SimpleSAML\OpenID\Federation\Factories; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Federation\EntityStatement; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; class EntityStatementFactory extends ParsedJwsFactory @@ -25,4 +27,31 @@ public function fromToken(string $token): EntityStatement $this->claimFactory, ); } + + /** + * @param array $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ): EntityStatement { + return new EntityStatement( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + ); + } } diff --git a/src/Federation/Factories/RequestObjectFactory.php b/src/Federation/Factories/RequestObjectFactory.php index f3de1bd..c79691f 100644 --- a/src/Federation/Factories/RequestObjectFactory.php +++ b/src/Federation/Factories/RequestObjectFactory.php @@ -4,7 +4,9 @@ namespace SimpleSAML\OpenID\Federation\Factories; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Federation\RequestObject; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; class RequestObjectFactory extends ParsedJwsFactory @@ -25,4 +27,31 @@ public function fromToken(string $token): RequestObject $this->claimFactory, ); } + + /** + * @param array $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ): RequestObject { + return new RequestObject( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + ); + } } diff --git a/src/Federation/Factories/TrustMarkDelegationFactory.php b/src/Federation/Factories/TrustMarkDelegationFactory.php index c61445b..bcdb6c9 100644 --- a/src/Federation/Factories/TrustMarkDelegationFactory.php +++ b/src/Federation/Factories/TrustMarkDelegationFactory.php @@ -4,7 +4,9 @@ namespace SimpleSAML\OpenID\Federation\Factories; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Federation\TrustMarkDelegation; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; class TrustMarkDelegationFactory extends ParsedJwsFactory @@ -21,4 +23,31 @@ public function fromToken(string $token): TrustMarkDelegation $this->claimFactory, ); } + + /** + * @param array $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ): TrustMarkDelegation { + return new TrustMarkDelegation( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + ); + } } diff --git a/src/Federation/Factories/TrustMarkFactory.php b/src/Federation/Factories/TrustMarkFactory.php index 07cc2b8..772672d 100644 --- a/src/Federation/Factories/TrustMarkFactory.php +++ b/src/Federation/Factories/TrustMarkFactory.php @@ -4,8 +4,10 @@ namespace SimpleSAML\OpenID\Federation\Factories; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Codebooks\JwtTypesEnum; use SimpleSAML\OpenID\Federation\TrustMark; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; class TrustMarkFactory extends ParsedJwsFactory @@ -25,4 +27,33 @@ public function fromToken( $expectedJwtType, ); } + + /** + * @param array $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + JwtTypesEnum $expectedJwtType = JwtTypesEnum::TrustMarkJwt, + ): TrustMark { + return new TrustMark( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + $expectedJwtType, + ); + } } diff --git a/src/Jwks/Factories/SignedJwksFactory.php b/src/Jwks/Factories/SignedJwksFactory.php index a9df487..11c5176 100644 --- a/src/Jwks/Factories/SignedJwksFactory.php +++ b/src/Jwks/Factories/SignedJwksFactory.php @@ -4,6 +4,8 @@ namespace SimpleSAML\OpenID\Jwks\Factories; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\SignedJwks; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; @@ -24,4 +26,31 @@ public function fromToken(string $token): SignedJwks $this->claimFactory, ); } + + /** + * @param array $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ): SignedJwks { + return new SignedJwks( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + ); + } } diff --git a/src/Jws/Factories/ParsedJwsFactory.php b/src/Jws/Factories/ParsedJwsFactory.php index c1920c2..304c7d9 100644 --- a/src/Jws/Factories/ParsedJwsFactory.php +++ b/src/Jws/Factories/ParsedJwsFactory.php @@ -4,9 +4,11 @@ namespace SimpleSAML\OpenID\Jws\Factories; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; @@ -41,4 +43,31 @@ public function fromToken(string $token): ParsedJws $this->claimFactory, ); } + + /** + * @param array $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ): ParsedJws { + return new ParsedJws( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + ); + } } diff --git a/src/Jws/JwsDecoratorBuilder.php b/src/Jws/JwsDecoratorBuilder.php index 68b630d..4104f65 100644 --- a/src/Jws/JwsDecoratorBuilder.php +++ b/src/Jws/JwsDecoratorBuilder.php @@ -40,7 +40,7 @@ public function fromToken(string $token): JwsDecorator * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ public function fromData( - JwkDecorator $signatureJwkDecorator, + JwkDecorator $signingKey, SignatureAlgorithmEnum $signatureAlgorithm, array $payload, array $header, @@ -55,7 +55,7 @@ public function fromData( $this->jwsBuilder->create()->withPayload( $this->helpers->json()->encode($payload), )->addSignature( - $signatureJwkDecorator->jwk(), + $signingKey->jwk(), $header, )->build(), ); diff --git a/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php b/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php index aef73d4..a95234c 100644 --- a/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php +++ b/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php @@ -4,13 +4,50 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\Factories; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; +use SimpleSAML\OpenID\VerifiableCredentials\JwtVcJson; class JwtVcJsonFactory extends ParsedJwsFactory { - // TODO mivanci Continue -// public function fromData(): JwtVcJson -// { -// return new JwtVcJson(); -// } + public function fromToken(string $token): JwtVcJson + { + return new JwtVcJson( + $this->jwsDecoratorBuilder->fromToken($token), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + ); + } + + /** + * @param array $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ): JwtVcJson { + return new JwtVcJson( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + ); + } } From 03c7ad18a418ce4b973a72a71b1896baae5580b3 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Thu, 10 Apr 2025 13:26:51 +0200 Subject: [PATCH 06/66] Introduce JWK suite --- src/Codebooks/JwtTypesEnum.php | 1 + .../Factories/EntityStatementFactory.php | 7 ++++++ src/Jwk.php | 23 +++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 src/Jwk.php diff --git a/src/Codebooks/JwtTypesEnum.php b/src/Codebooks/JwtTypesEnum.php index e023046..95e96a1 100644 --- a/src/Codebooks/JwtTypesEnum.php +++ b/src/Codebooks/JwtTypesEnum.php @@ -8,6 +8,7 @@ enum JwtTypesEnum: string { case EntityStatementJwt = 'entity-statement+jwt'; case JwkSetJwt = 'jwk-set+jwt'; + case Jwt = 'JWT'; case TrustMarkJwt = 'trust-mark+jwt'; case TrustMarkDelegationJwt = 'trust-mark-delegation+jwt'; } diff --git a/src/Federation/Factories/EntityStatementFactory.php b/src/Federation/Factories/EntityStatementFactory.php index 5cfba3c..eaf209f 100644 --- a/src/Federation/Factories/EntityStatementFactory.php +++ b/src/Federation/Factories/EntityStatementFactory.php @@ -5,6 +5,8 @@ namespace SimpleSAML\OpenID\Federation\Factories; use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; +use SimpleSAML\OpenID\Codebooks\ClaimsEnum; +use SimpleSAML\OpenID\Codebooks\JwtTypesEnum; use SimpleSAML\OpenID\Federation\EntityStatement; use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; @@ -39,6 +41,11 @@ public function fromData( array $payload, array $header, ): EntityStatement { + + if (!array_key_exists(ClaimsEnum::Typ->value, $header)) { + $header[ClaimsEnum::Typ->value] = JwtTypesEnum::EntityStatementJwt->value; + } + return new EntityStatement( $this->jwsDecoratorBuilder->fromData( $signingKey, diff --git a/src/Jwk.php b/src/Jwk.php new file mode 100644 index 0000000..2658124 --- /dev/null +++ b/src/Jwk.php @@ -0,0 +1,23 @@ +jwkDecoratorFactory ??= new JwkDecoratorFactory(); + } +} From d2731fa34302503df1794daad519fe8ae2aa0ba4 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Thu, 10 Apr 2025 14:04:29 +0200 Subject: [PATCH 07/66] Update type header handling when building specific JWS --- src/Federation/Factories/EntityStatementFactory.php | 5 +---- src/Federation/Factories/TrustMarkDelegationFactory.php | 4 ++++ src/Federation/Factories/TrustMarkFactory.php | 3 +++ src/Jwk.php | 6 ------ src/Jwks/Factories/SignedJwksFactory.php | 4 ++++ src/VerifiableCredentials/Factories/JwtVcJsonFactory.php | 3 +++ 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Federation/Factories/EntityStatementFactory.php b/src/Federation/Factories/EntityStatementFactory.php index eaf209f..4f43349 100644 --- a/src/Federation/Factories/EntityStatementFactory.php +++ b/src/Federation/Factories/EntityStatementFactory.php @@ -41,10 +41,7 @@ public function fromData( array $payload, array $header, ): EntityStatement { - - if (!array_key_exists(ClaimsEnum::Typ->value, $header)) { - $header[ClaimsEnum::Typ->value] = JwtTypesEnum::EntityStatementJwt->value; - } + $header[ClaimsEnum::Typ->value] = JwtTypesEnum::EntityStatementJwt->value; return new EntityStatement( $this->jwsDecoratorBuilder->fromData( diff --git a/src/Federation/Factories/TrustMarkDelegationFactory.php b/src/Federation/Factories/TrustMarkDelegationFactory.php index bcdb6c9..0696a6e 100644 --- a/src/Federation/Factories/TrustMarkDelegationFactory.php +++ b/src/Federation/Factories/TrustMarkDelegationFactory.php @@ -5,6 +5,8 @@ namespace SimpleSAML\OpenID\Federation\Factories; use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; +use SimpleSAML\OpenID\Codebooks\ClaimsEnum; +use SimpleSAML\OpenID\Codebooks\JwtTypesEnum; use SimpleSAML\OpenID\Federation\TrustMarkDelegation; use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; @@ -35,6 +37,8 @@ public function fromData( array $payload, array $header, ): TrustMarkDelegation { + $header[ClaimsEnum::Typ->value] = JwtTypesEnum::TrustMarkDelegationJwt->value; + return new TrustMarkDelegation( $this->jwsDecoratorBuilder->fromData( $signingKey, diff --git a/src/Federation/Factories/TrustMarkFactory.php b/src/Federation/Factories/TrustMarkFactory.php index 772672d..f3f8837 100644 --- a/src/Federation/Factories/TrustMarkFactory.php +++ b/src/Federation/Factories/TrustMarkFactory.php @@ -5,6 +5,7 @@ namespace SimpleSAML\OpenID\Federation\Factories; use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; +use SimpleSAML\OpenID\Codebooks\ClaimsEnum; use SimpleSAML\OpenID\Codebooks\JwtTypesEnum; use SimpleSAML\OpenID\Federation\TrustMark; use SimpleSAML\OpenID\Jwk\JwkDecorator; @@ -40,6 +41,8 @@ public function fromData( array $header, JwtTypesEnum $expectedJwtType = JwtTypesEnum::TrustMarkJwt, ): TrustMark { + $header[ClaimsEnum::Typ->value] = $expectedJwtType; + return new TrustMark( $this->jwsDecoratorBuilder->fromData( $signingKey, diff --git a/src/Jwk.php b/src/Jwk.php index 2658124..2a6d956 100644 --- a/src/Jwk.php +++ b/src/Jwk.php @@ -4,18 +4,12 @@ namespace SimpleSAML\OpenID; -use Psr\Log\LoggerInterface; use SimpleSAML\OpenID\Jwk\Factories\JwkDecoratorFactory; class Jwk { protected ?JwkDecoratorFactory $jwkDecoratorFactory = null; - public function __construct( - protected readonly ?LoggerInterface $logger = null, - ) { - } - public function jwkDecoratorFactory(): JwkDecoratorFactory { return $this->jwkDecoratorFactory ??= new JwkDecoratorFactory(); diff --git a/src/Jwks/Factories/SignedJwksFactory.php b/src/Jwks/Factories/SignedJwksFactory.php index 11c5176..0ca6fca 100644 --- a/src/Jwks/Factories/SignedJwksFactory.php +++ b/src/Jwks/Factories/SignedJwksFactory.php @@ -5,6 +5,8 @@ namespace SimpleSAML\OpenID\Jwks\Factories; use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; +use SimpleSAML\OpenID\Codebooks\ClaimsEnum; +use SimpleSAML\OpenID\Codebooks\JwtTypesEnum; use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\SignedJwks; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; @@ -38,6 +40,8 @@ public function fromData( array $payload, array $header, ): SignedJwks { + $header[ClaimsEnum::Typ->value] = JwtTypesEnum::JwkSetJwt->value; + return new SignedJwks( $this->jwsDecoratorBuilder->fromData( $signingKey, diff --git a/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php b/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php index a95234c..5c2ab80 100644 --- a/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php +++ b/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php @@ -35,6 +35,9 @@ public function fromData( array $payload, array $header, ): JwtVcJson { + // TODO mivanci Set type header +// $header[ClaimsEnum::Typ->value] = JwtTypesEnum::->value; + return new JwtVcJson( $this->jwsDecoratorBuilder->fromData( $signingKey, From 55c90e7ff671b096f200731b140bbba37d3b25a6 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Thu, 10 Apr 2025 15:50:18 +0200 Subject: [PATCH 08/66] WIP JwtVcJson --- src/Codebooks/ClaimsEnum.php | 2 ++ src/Exceptions/JwtVcJsonException.php | 9 ++++++++ src/VerifiableCredentials/JwtVcJson.php | 30 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 src/Exceptions/JwtVcJsonException.php diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index 144c51c..fe76e2f 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -148,4 +148,6 @@ enum ClaimsEnum: string case Use = 'use'; case UserAuthentication = 'user_authentication'; case UserinfoEndpoint = 'userinfo_endpoint'; + // VerifiableCredential + case Vc = 'vc'; } diff --git a/src/Exceptions/JwtVcJsonException.php b/src/Exceptions/JwtVcJsonException.php new file mode 100644 index 0000000..cdae351 --- /dev/null +++ b/src/Exceptions/JwtVcJsonException.php @@ -0,0 +1,9 @@ +value; + + $vc = $this->getPayloadClaim($claimKey) ?? throw new JwtVcJsonException('No vc claim found.'); + + if (is_array($vc)) { + return $vc; + } + + throw new JwtVcJsonException('Invalid vc claim.'); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + protected function validate(): void + { + $this->validateByCallbacks( + $this->getVc(...), + ); + } } From 8ef11c95f0dc86bb154f5cb2bbc6d7ca4c26ef24 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Fri, 11 Apr 2025 17:02:37 +0200 Subject: [PATCH 09/66] WIP JwtVcJson --- src/Codebooks/AtContextsEnum.php | 10 + src/Codebooks/ClaimsEnum.php | 5 + ...Exception.php => VcDataModelException.php} | 2 +- src/Factories/ClaimFactory.php | 11 + src/Helpers/Arr.php | 24 ++ src/Helpers/Type.php | 168 +++++++++++++- src/Jws/ParsedJws.php | 8 + src/VerifiableCredentials/JwtVcJson.php | 46 ---- .../Claims/VcAtContextClaimValue.php | 53 +++++ .../VcDataModel/Claims/VcClaimValue.php | 65 ++++++ .../Claims/VcCredentialSubjectClaimBag.php | 36 +++ .../Claims/VcCredentialSubjectClaimValue.php | 30 +++ .../VcDataModel/Claims/VcIssuerClaimValue.php | 63 ++++++ .../Factories/JwtVcJsonFactory.php | 4 +- .../Factories/VcDataModelClaimFactory.php | 97 ++++++++ .../VcDataModel/JwtVcJson.php | 208 ++++++++++++++++++ 16 files changed, 769 insertions(+), 61 deletions(-) create mode 100644 src/Codebooks/AtContextsEnum.php rename src/Exceptions/{JwtVcJsonException.php => VcDataModelException.php} (61%) delete mode 100644 src/VerifiableCredentials/JwtVcJson.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php rename src/VerifiableCredentials/{ => VcDataModel}/Factories/JwtVcJsonFactory.php (91%) create mode 100644 src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php create mode 100644 src/VerifiableCredentials/VcDataModel/JwtVcJson.php diff --git a/src/Codebooks/AtContextsEnum.php b/src/Codebooks/AtContextsEnum.php new file mode 100644 index 0000000..912e997 --- /dev/null +++ b/src/Codebooks/AtContextsEnum.php @@ -0,0 +1,10 @@ +vcDataModelClaimFactory ??= new VcDataModelClaimFactory( + $this->helpers, + $this, + ); + } + /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException */ diff --git a/src/Helpers/Arr.php b/src/Helpers/Arr.php index d04dacb..9b6ecf4 100644 --- a/src/Helpers/Arr.php +++ b/src/Helpers/Arr.php @@ -56,4 +56,28 @@ public function getNestedValue(array $array, int|string ...$keys): mixed return $this->getNestedValue($nestedArray, ...$keys); } + + /** + * @param mixed[] $array + */ + public function isAssociative(array $array): bool + { + // Has at least one string key or non-sequential numeric keys + return array_keys($array) !== range(0, count($array) - 1); + } + + /** + * Is array of arrays. + * @param mixed[] $array + */ + public function isOfArrays(array $array): bool + { + foreach ($array as $value) { + if (!is_array($value)) { + return false; + } + } + + return true; + } } diff --git a/src/Helpers/Type.php b/src/Helpers/Type.php index bca00af..02a4302 100644 --- a/src/Helpers/Type.php +++ b/src/Helpers/Type.php @@ -24,9 +24,11 @@ public function ensureString(mixed $value, ?string $context = null): string return (string)$value; } - $error = 'Unsafe string casting, aborting.'; - $error .= is_string($context) ? ' Context: ' . $context : ''; - $error .= ' Value was: ' . var_export($value, true); + $error = $this->prepareErrorMessage( + 'Unsafe string casting, aborting.', + $value, + $context, + ); throw new InvalidValueException($error); } @@ -43,9 +45,11 @@ public function ensureNonEmptyString(mixed $value, ?string $context = null): str return $value; } - $error = 'Empty string value encountered, aborting.'; - $error .= is_string($context) ? ' Context: ' . $context : ''; - $error .= ' Value was: ' . var_export($value, true); + $error = $this->prepareErrorMessage( + 'Empty string value encountered, aborting.', + $value, + $context, + ); throw new InvalidValueException($error); } @@ -73,9 +77,11 @@ public function ensureArray(mixed $value, ?string $context = null): array // Converts object properties to an array } - $error = 'Unsafe array casting, aborting.'; - $error .= is_string($context) ? 'Context: ' . $context : ''; - $error .= ' Value was: ' . var_export($value, true); + $error = $this->prepareErrorMessage( + 'Unsafe array casting, aborting.', + $value, + $context, + ); throw new InvalidValueException($error); } @@ -203,10 +209,148 @@ public function ensureInt(mixed $value, ?string $context = null): int return (int)$value; } - $error = 'Unsafe integer casting, aborting.'; - $error .= is_string($context) ? 'Context: ' . $context : ''; - $error .= ' Value was: ' . var_export($value, true); + $error = $this->prepareErrorMessage( + 'Unsafe integer casting, aborting.', + $value, + $context, + ); throw new InvalidValueException($error); } + + /** + * @return non-empty-string + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function enforceRegex( + mixed $value, + string $pattern, + ?string $context = null, + ): string { + $value = $this->ensureNonEmptyString($value, $context); + + $error = $this->prepareErrorMessage( + 'Regex match failed, aborting.', + $value, + $context, + ); + + preg_match($pattern, $value) || throw new InvalidValueException($error); + + return $value; + } + + /** + * @return non-empty-string + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function enforceUri( + mixed $value, + ?string $context = null, + string $pattern = '/^[^:]+:\/\/?([^\s\/$.?#].[^\s]*)?$/', + ): string { + try { + $value = $this->enforceRegex($value, $pattern, $context); + } catch (InvalidValueException) { + $error = $this->prepareErrorMessage( + 'URI regex match failed, aborting.', + $value, + $context, + ); + + throw new InvalidValueException($error); + } + + return $value; + } + + /** + * @param mixed[] $array + * @return array + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function enforceArrayOfArrays(array $array, ?string $context = null): array + { + foreach ($array as $value) { + if (!is_array($value)) { + $error = $this->prepareErrorMessage( + 'Non-array value encountered, aborting.', + $array, + $context, + ); + + throw new InvalidValueException($error); + } + } + + /** @var array $array */ + return $array; + } + + /** + * @param mixed[] $array + * @return non-empty-array + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function enforceNonEmptyArray(array $array, ?string $context = null): array + { + if ($array === []) { + $error = $this->prepareErrorMessage( + 'Empty array encountered, aborting.', + $array, + $context, + ); + throw new InvalidValueException($error); + } + + return $array; + } + + /** + * @param mixed[] $array + * @return non-empty-array + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function enforceNonEmptyArrayWithValuesAsNonEmptyStrings(array $array, ?string $context = null): array + { + $array = $this->ensureArrayWithValuesAsNonEmptyStrings($array, $context); + $array = $this->enforceNonEmptyArray($array, $context); + + /** @var non-empty-array $array */ + return $array; + } + + /** + * @param mixed[] $array + * @return non-empty-array + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function enforceNonEmptyArrayOfNonEmptyArrays(array $array, ?string $context = null): array + { + $array = $this->enforceNonEmptyArray($array, $context); + + foreach ($array as $value) { + if (!is_array($value)) { + $error = $this->prepareErrorMessage( + 'Non-array value encountered, aborting.', + $array, + $context, + ); + + throw new InvalidValueException($error); + } + + $this->enforceNonEmptyArray($value, $context); + } + + /** @var non-empty-array $array */ + return $array; + } + + protected function prepareErrorMessage(string $message, mixed $value, ?string $context = null): string + { + return $message . + (is_string($context) ? ' Context: ' . $context : '') . + ' Value was: ' . var_export($value, true); + } } diff --git a/src/Jws/ParsedJws.php b/src/Jws/ParsedJws.php index f1296dc..e0a4e6b 100644 --- a/src/Jws/ParsedJws.php +++ b/src/Jws/ParsedJws.php @@ -95,6 +95,14 @@ public function getPayloadClaim(string $key): mixed return $this->getPayload()[$key] ?? null; } + public function getNestedPayloadClaim(int|string ...$keys): mixed + { + return $this->helpers->arr()->getNestedValue( + $this->getPayload(), + ...$keys, + ); + } + public function getToken( JwsSerializerEnum $jwsSerializerEnum = JwsSerializerEnum::Compact, ?int $signatureIndex = null, diff --git a/src/VerifiableCredentials/JwtVcJson.php b/src/VerifiableCredentials/JwtVcJson.php deleted file mode 100644 index dbe4e05..0000000 --- a/src/VerifiableCredentials/JwtVcJson.php +++ /dev/null @@ -1,46 +0,0 @@ -value; - - $vc = $this->getPayloadClaim($claimKey) ?? throw new JwtVcJsonException('No vc claim found.'); - - if (is_array($vc)) { - return $vc; - } - - throw new JwtVcJsonException('Invalid vc claim.'); - } - - /** - * @throws \SimpleSAML\OpenID\Exceptions\JwsException - */ - protected function validate(): void - { - $this->validateByCallbacks( - $this->getVc(...), - ); - } -} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php new file mode 100644 index 0000000..9d89ba7 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php @@ -0,0 +1,53 @@ +baseContext !== AtContextsEnum::W3Org2018CredentialsV1->value) { + throw new VcDataModelException(sprintf( + 'Invalid VC @context claim. Base context should be %s, %s given.', + AtContextsEnum::W3Org2018CredentialsV1->value, + $this->baseContext, + )); + } + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + return [ + $this->baseContext, + ...$this->otherContexts, + ]; + } + + public function getBaseContext(): string + { + return $this->baseContext; + } + + /** + * @return mixed[] + */ + public function getOtherContexts(): array + { + return $this->otherContexts; + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php new file mode 100644 index 0000000..beb9fb4 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php @@ -0,0 +1,65 @@ + $type + */ + public function __construct( + protected readonly VcAtContextClaimValue $atContextClaimValue, + /** @var null|non-empty-string */ + protected readonly null|string $id, + /** @var non-empty-array */ + protected readonly array $type, + protected readonly VcCredentialSubjectClaimBag $vcCredentialSubjectClaimBag, + protected readonly VcIssuerClaimValue $issuerClaimValue, + ) { + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + // TODO: Implement jsonSerialize() method. + return []; + } + + public function getAtContext(): VcAtContextClaimValue + { + return $this->atContextClaimValue; + } + + /** + * @return non-empty-string|null + */ + public function getId(): ?string + { + return $this->id; + } + + /** + * @return non-empty-array + */ + public function getType(): array + { + return $this->type; + } + + public function getCredentialSubject(): VcCredentialSubjectClaimBag + { + return $this->vcCredentialSubjectClaimBag; + } + + public function getIssuer(): VcIssuerClaimValue + { + return $this->issuerClaimValue; + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php new file mode 100644 index 0000000..8326547 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php @@ -0,0 +1,36 @@ +vcCredentialSubjectClaimValueValues = [ + $vcCredentialSubjectClaimValue, + ...$vcCredentialSubjectClaimValueValues, + ]; + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + return array_map( + fn( + VcCredentialSubjectClaimValue $vcCredentialSubjectClaimValue, + ): array => $vcCredentialSubjectClaimValue->jsonSerialize(), + $this->vcCredentialSubjectClaimValueValues, + ); + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php new file mode 100644 index 0000000..dc9e699 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php @@ -0,0 +1,30 @@ + $data + */ + public function __construct(protected readonly array $data) + { + } + + public function get(int|string $key): mixed + { + return $this->data[$key] ?? null; + } + + /** + * @return non-empty-array + */ + public function jsonSerialize(): array + { + return $this->data; + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php new file mode 100644 index 0000000..8de4189 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php @@ -0,0 +1,63 @@ + */ + protected array $data; + + /** + * @param non-empty-string $id + * @param mixed[] $otherClaims + */ + public function __construct( + protected string $id, + array $otherClaims = [], + protected readonly string $name = ClaimsEnum::Issuer->value, + ) { + $this->data = array_merge( + $otherClaims, + [ClaimsEnum::Id->value => $this->id], + ); + } + + /** + * @return non-empty-string + */ + public function getId(): string + { + return $this->id; + } + + public function getKey(int|string $key): mixed + { + return $this->data[$key] ?? null; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return non-empty-array + */ + public function getValue(): array + { + return $this->data; + } + + /** + * @return non-empty-array + */ + public function jsonSerialize(): array + { + return $this->getValue(); + } +} diff --git a/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactory.php similarity index 91% rename from src/VerifiableCredentials/Factories/JwtVcJsonFactory.php rename to src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactory.php index 5c2ab80..a6294d0 100644 --- a/src/VerifiableCredentials/Factories/JwtVcJsonFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactory.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace SimpleSAML\OpenID\VerifiableCredentials\Factories; +namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Factories; use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; -use SimpleSAML\OpenID\VerifiableCredentials\JwtVcJson; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\JwtVcJson; class JwtVcJsonFactory extends ParsedJwsFactory { diff --git a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php new file mode 100644 index 0000000..b6d84de --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php @@ -0,0 +1,97 @@ + $vcType + */ + public function buildVcClaimValue( + VcAtContextClaimValue $vcAtContextClaimValue, + null|string $vcId, + array $vcType, + VcCredentialSubjectClaimBag $vcCredentialSubjectClaimBag, + VcIssuerClaimValue $vcIssuerClaimValue, + ): VcClaimValue { + return new VcClaimValue( + $vcAtContextClaimValue, + $vcId, + $vcType, + $vcCredentialSubjectClaimBag, + $vcIssuerClaimValue, + ); + } + + /** + * @param mixed[] $otherContexts + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function buildVcAtContextClaimValue(string $baseContext, array $otherContexts): VcAtContextClaimValue + { + return new VcAtContextClaimValue($baseContext, $otherContexts); + } + + /** + * @param non-empty-array $data + */ + public function buildVcCredentialSubjectClaimValue(array $data): VcCredentialSubjectClaimValue + { + return new VcCredentialSubjectClaimValue($data); + } + + /** + * @param non-empty-array> $data + */ + public function buildVcCredentialSubjectClaimBag(array $data): VcCredentialSubjectClaimBag + { + $vcCredentialSubjectClaimValueData = array_shift($data); + + $vcCredentialSubjectClaimValue = $this->buildVcCredentialSubjectClaimValue($vcCredentialSubjectClaimValueData); + + $vcCredentialSubjectClaimValues = array_map( + fn (array $data): VcCredentialSubjectClaimValue => $this->buildVcCredentialSubjectClaimValue($data), + $data, + ); + + return new VcCredentialSubjectClaimBag( + $vcCredentialSubjectClaimValue, + ...$vcCredentialSubjectClaimValues, + ); + } + + /** + * @param non-empty-array $data + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function buildVcIssuerClaimValue(array $data): VcIssuerClaimValue + { + $id = $data[ClaimsEnum::Id->value] ?? throw new VcDataModelException( + 'No Issuer ID claim value available.', + ); + + $id = $this->helpers->type()->enforceUri($id); + + return new VcIssuerClaimValue($id, $data); + } +} diff --git a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php new file mode 100644 index 0000000..08f54ff --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php @@ -0,0 +1,208 @@ + */ + protected ?array $vcType = null; + + protected ?VcCredentialSubjectClaimBag $vcCredentialSubjectClaimBag = null; + + protected ?VcIssuerClaimValue $vcIssuerClaimValue = null; + + public function getCredentialFormatIdentifier(): CredentialFormatIdentifiersEnum + { + return CredentialFormatIdentifiersEnum::JwtVcJson; + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getVc(): VcClaimValue + { + if ($this->vcClaimValue instanceof \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcClaimValue) { + return $this->vcClaimValue; + } + + $claimKey = ClaimsEnum::Vc->value; + + $vc = $this->getPayloadClaim($claimKey) ?? throw new VcDataModelException('No VC claim found.'); + + if (!is_array($vc)) { + throw new VcDataModelException('Invalid VC claim.'); + } + + return $this->vcClaimValue = $this->claimFactory->forVcDataModel()->buildVcClaimValue( + $this->getVcAtContext(), + $this->getVcId(), + $this->getVcType(), + $this->getVcCredentialSubject(), + $this->getVcIssuer(), + ); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function getVcAtContext(): VcAtContextClaimValue + { + if ($this->vcAtContextClaimValue instanceof VcAtContextClaimValue) { + return $this->vcAtContextClaimValue; + } + + $vcContext = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::AtContext->value); + + if (!is_array($vcContext)) { + throw new VcDataModelException('Invalid VC @context claim.'); + } + + if (!is_string($baseContext = array_shift($vcContext))) { + throw new VcDataModelException('Invalid VC @context claim.'); + } + + return $this->vcAtContextClaimValue = $this->claimFactory->forVcDataModel()->buildVcAtContextClaimValue( + $baseContext, + $vcContext, + ); + } + + /** + * @return ?non-empty-string + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getVcId(): ?string + { + if ($this->vcId === false) { + return null; + } + + $vcId = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Id->value); + + if (is_null($vcId)) { + $this->vcId = false; + return null; + } + + return $this->vcId = $this->helpers->type()->enforceUri($vcId); + } + + /** + * @return non-empty-array + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getVcType(): array + { + if ($this->vcType !== null) { + return $this->vcType; + } + + $claimKeys = [ClaimsEnum::Vc->value, ClaimsEnum::Type->value]; + $claimKeys2 = [ClaimsEnum::Vc->value, ClaimsEnum::AtType->value]; + + $vcType = $this->getNestedPayloadClaim(...$claimKeys) ?? $this->getNestedPayloadClaim(...$claimKeys2); + + if ((!is_array($vcType)) || $vcType === []) { + throw new VcDataModelException('Invalid VC Type claim.'); + } + + return $this->vcType = $this->helpers->type()->enforceNonEmptyArrayWithValuesAsNonEmptyStrings($vcType); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getVcCredentialSubject(): VcCredentialSubjectClaimBag + { + if ($this->vcCredentialSubjectClaimBag instanceof VcCredentialSubjectClaimBag) { + return $this->vcCredentialSubjectClaimBag; + } + + $claimKeys = [ClaimsEnum::Vc->value, ClaimsEnum::CredentialSubject->value]; + + $vcCredentialSubject = $this->getNestedPayloadClaim(...$claimKeys); + + if ((!is_array($vcCredentialSubject)) || $vcCredentialSubject === []) { + throw new VcDataModelException('Invalid VC Credential Subject claim.'); + } + + if ($this->helpers->arr()->isAssociative($vcCredentialSubject)) { + return $this->vcCredentialSubjectClaimBag = $this->claimFactory->forVcDataModel() + ->buildVcCredentialSubjectClaimBag([$vcCredentialSubject]); + } + + return $this->vcCredentialSubjectClaimBag = $this->claimFactory->forVcDataModel() + ->buildVcCredentialSubjectClaimBag( + $this->helpers->type()->enforceNonEmptyArrayOfNonEmptyArrays($vcCredentialSubject), + ); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function getVcIssuer(): VcIssuerClaimValue + { + if ($this->vcIssuerClaimValue instanceof VcIssuerClaimValue) { + return $this->vcIssuerClaimValue; + } + + $vcIssuer = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Issuer->value); + + if (is_null($vcIssuer)) { + throw new VcDataModelException('Invalid VC Issuer claim.'); + } + + if (is_string($vcIssuer)) { + return $this->vcIssuerClaimValue = $this->claimFactory->forVcDataModel()->buildVcIssuerClaimValue( + [ClaimsEnum::Id->value => $vcIssuer], + ); + } + + if (is_array($vcIssuer)) { + return $this->vcIssuerClaimValue = $this->claimFactory->forVcDataModel()->buildVcIssuerClaimValue( + $this->helpers->type()->enforceNonEmptyArray($vcIssuer), + ); + } + + throw new VcDataModelException('Invalid VC Issuer claim.'); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + protected function validate(): void + { + $this->validateByCallbacks( + $this->getVc(...), + $this->getVcAtContext(...), + $this->getVcId(...), + $this->getVcType(...), + $this->getVcCredentialSubject(...), + $this->getVcIssuer(...), + ); + } +} From 141e58c4c9f0c02ca4ba2d6c23cd0fb8cd9c11c9 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Wed, 23 Apr 2025 16:22:49 +0200 Subject: [PATCH 10/66] WIP JwtVcJson --- src/Codebooks/ClaimsEnum.php | 6 +- src/Helpers.php | 8 + src/Helpers/DateTime.php | 41 ++++++ .../VcDataModel/Claims/VcClaimValue.php | 29 +++- .../Claims/VcCredentialStatusClaimValue.php | 73 +++++++++ .../VcDataModel/Claims/VcProofClaimValue.php | 63 ++++++++ .../Factories/VcDataModelClaimFactory.php | 47 ++++++ .../VcDataModel/JwtVcJson.php | 139 +++++++++++++++++- 8 files changed, 400 insertions(+), 6 deletions(-) create mode 100644 src/Helpers/DateTime.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValue.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValue.php diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index fb4340b..eac3be1 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -46,7 +46,8 @@ enum ClaimsEnum: string case CredentialResponseEncryption = 'credential_response_encryption'; // CredentialSigningAlgorithmValuesSupported case CredentialSigningAlgValuesSupported = 'credential_signing_alg_values_supported'; - case CredentialSubject = 'credential_subject'; + case Credential_Status = 'credentialStatus'; + case Credential_Subject = 'credentialSubject'; case CryptographicBindingMethodsSupported = 'cryptographic_binding_methods_supported'; case DeferredCredentialEndpoint = 'deferred_credential_endpoint'; case Delegation = 'delegation'; @@ -55,6 +56,7 @@ enum ClaimsEnum: string case EndSessionEndpoint = 'end_session_endpoint'; // ExpirationTime case Exp = 'exp'; + case Expiration_Date = 'expirationDate'; case EncryptionRequired = 'encryption_required'; // EncryptionValuesSupported case EncValuesSupported = 'enc_values_supported'; @@ -76,6 +78,7 @@ enum ClaimsEnum: string 'introspection_endpoint_auth_signing_alg_values_supported'; // Issuer case Iss = 'iss'; + case Issuance_Date = 'issuanceDate'; case Issuer = 'issuer'; // JWT ID case Jti = 'jti'; @@ -107,6 +110,7 @@ enum ClaimsEnum: string case PolicyUri = 'policy_uri'; case PostLogoutRedirectUris = 'post_logout_redirect_uris'; case PreAuthorizedGrantAnonymousAccessSupported = 'pre-authorized_grant_anonymous_access_supported'; + case Proof = 'proof'; // ProofSigningAlgorithmValuesSupported case ProofSigningAlgValuesSupported = 'proof_signing_alg_values_supported'; case ProofTypesSupported = 'proof_types_supported'; diff --git a/src/Helpers.php b/src/Helpers.php index c3b3df6..d1e3474 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -5,6 +5,7 @@ namespace SimpleSAML\OpenID; use SimpleSAML\OpenID\Helpers\Arr; +use SimpleSAML\OpenID\Helpers\DateTime; use SimpleSAML\OpenID\Helpers\Json; use SimpleSAML\OpenID\Helpers\Type; use SimpleSAML\OpenID\Helpers\Url; @@ -19,6 +20,8 @@ class Helpers protected static ?Type $type = null; + protected static ?DateTime $dateTime = null; + public function url(): Url { return self::$url ??= new Url(); @@ -38,4 +41,9 @@ public function type(): Type { return self::$type ??= new Type(); } + + public function dateTime(): DateTime + { + return self::$dateTime ??= new DateTime(); + } } diff --git a/src/Helpers/DateTime.php b/src/Helpers/DateTime.php new file mode 100644 index 0000000..19b14c2 --- /dev/null +++ b/src/Helpers/DateTime.php @@ -0,0 +1,41 @@ + */ protected readonly array $type, - protected readonly VcCredentialSubjectClaimBag $vcCredentialSubjectClaimBag, + protected readonly VcCredentialSubjectClaimBag $credentialSubjectClaimBag, protected readonly VcIssuerClaimValue $issuerClaimValue, + protected readonly DateTimeImmutable $issuanceDate, + protected readonly ?VcProofClaimValue $proofClaimValue, + protected readonly ?DateTimeImmutable $expirationDate, + protected readonly ?VcCredentialStatusClaimValue $credentialStatusClaimValue, ) { } @@ -55,11 +60,31 @@ public function getType(): array public function getCredentialSubject(): VcCredentialSubjectClaimBag { - return $this->vcCredentialSubjectClaimBag; + return $this->credentialSubjectClaimBag; } public function getIssuer(): VcIssuerClaimValue { return $this->issuerClaimValue; } + + public function getIssuanceDate(): DateTimeImmutable + { + return $this->issuanceDate; + } + + public function getProof(): ?VcProofClaimValue + { + return $this->proofClaimValue; + } + + public function getExpirationDate(): ?DateTimeImmutable + { + return $this->expirationDate; + } + + public function getCredentialStatus(): ?VcCredentialStatusClaimValue + { + return $this->credentialStatusClaimValue; + } } diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValue.php new file mode 100644 index 0000000..a223154 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValue.php @@ -0,0 +1,73 @@ + */ + protected array $data; + + /** + * @param non-empty-string $id, + * @param non-empty-string $type + * @param mixed[] $otherClaims + */ + public function __construct( + protected string $id, + protected string $type, + array $otherClaims = [], + ) { + $this->data = array_merge( + $otherClaims, + [ClaimsEnum::Id->value => $this->$id], + [ClaimsEnum::Type->value => $this->type], + ); + } + + /** + * @return non-empty-string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @return non-empty-string + */ + public function getType(): string + { + return $this->type; + } + + public function getKey(int|string $key): mixed + { + return $this->data[$key] ?? null; + } + + public function getName(): string + { + return ClaimsEnum::Credential_Status->value; + } + + /** + * @return non-empty-array + */ + public function getValue(): array + { + return $this->data; + } + + /** + * @return non-empty-array + */ + public function jsonSerialize(): array + { + return $this->getValue(); + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValue.php new file mode 100644 index 0000000..205d1d9 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValue.php @@ -0,0 +1,63 @@ + */ + protected array $data; + + /** + * @param non-empty-string $type + * @param mixed[] $otherClaims + */ + public function __construct( + protected string $type, + array $otherClaims = [], + protected readonly string $name = ClaimsEnum::Proof->value, + ) { + $this->data = array_merge( + $otherClaims, + [ClaimsEnum::Type->value => $this->type], + ); + } + + /** + * @return non-empty-string + */ + public function getType(): string + { + return $this->type; + } + + public function getKey(int|string $key): mixed + { + return $this->data[$key] ?? null; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return non-empty-array + */ + public function getValue(): array + { + return $this->data; + } + + /** + * @return non-empty-array + */ + public function jsonSerialize(): array + { + return $this->getValue(); + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php index b6d84de..de215c0 100644 --- a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php @@ -4,15 +4,18 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Factories; +use DateTimeImmutable; use SimpleSAML\OpenID\Codebooks\ClaimsEnum; use SimpleSAML\OpenID\Exceptions\VcDataModelException; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcAtContextClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcClaimValue; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimBag; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcIssuerClaimValue; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcProofClaimValue; class VcDataModelClaimFactory { @@ -32,6 +35,10 @@ public function buildVcClaimValue( array $vcType, VcCredentialSubjectClaimBag $vcCredentialSubjectClaimBag, VcIssuerClaimValue $vcIssuerClaimValue, + DateTimeImmutable $vcIssuanceDate, + ?VcProofClaimValue $vcProofClaimValue, + ?DateTimeImmutable $vcExpirationDate, + ?VcCredentialStatusClaimValue $vcCredentialStatusClaimValue, ): VcClaimValue { return new VcClaimValue( $vcAtContextClaimValue, @@ -39,6 +46,10 @@ public function buildVcClaimValue( $vcType, $vcCredentialSubjectClaimBag, $vcIssuerClaimValue, + $vcIssuanceDate, + $vcProofClaimValue, + $vcExpirationDate, + $vcCredentialStatusClaimValue, ); } @@ -94,4 +105,40 @@ public function buildVcIssuerClaimValue(array $data): VcIssuerClaimValue return new VcIssuerClaimValue($id, $data); } + + /** + * @param non-empty-array $data + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function buildVcProofClaimValue(array $data): VcProofClaimValue + { + $type = $data[ClaimsEnum::Type->value] ?? throw new VcDataModelException( + 'No Type claim value available.', + ); + + $type = $this->helpers->type()->ensureNonEmptyString($type); + + return new VcProofClaimValue($type, $data); + } + + /** + * @param non-empty-array $data + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function buildVcCredentialStatusClaimValue(array $data): VcCredentialStatusClaimValue + { + $id = $data[ClaimsEnum::Id->value] ?? throw new VcDataModelException( + 'No Issuer ID claim value available.', + ); + $id = $this->helpers->type()->enforceUri($id); + + $type = $data[ClaimsEnum::Type->value] ?? throw new VcDataModelException( + 'No Type claim value available.', + ); + $type = $this->helpers->type()->ensureNonEmptyString($type); + + return new VcCredentialStatusClaimValue($id, $type, $data); + } } diff --git a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php index 08f54ff..4ec8cd2 100644 --- a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php +++ b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php @@ -4,14 +4,17 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel; +use DateTimeImmutable; use SimpleSAML\OpenID\Codebooks\ClaimsEnum; use SimpleSAML\OpenID\Codebooks\CredentialFormatIdentifiersEnum; use SimpleSAML\OpenID\Exceptions\VcDataModelException; use SimpleSAML\OpenID\Jws\ParsedJws; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcAtContextClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcClaimValue; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimBag; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcIssuerClaimValue; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcProofClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VerifiableCredentialInterface; class JwtVcJson extends ParsedJws implements VerifiableCredentialInterface @@ -30,15 +33,23 @@ class JwtVcJson extends ParsedJws implements VerifiableCredentialInterface protected ?VcIssuerClaimValue $vcIssuerClaimValue = null; + protected ?DateTimeImmutable $vcIssuanceDate = null; + + protected null|false|VcProofClaimValue $vcProofClaimValue = null; + + protected null|false|DateTimeImmutable $vcExpirationDate = null; + + protected null|false|VcCredentialStatusClaimValue $vcCredentialStatusClaimValue = null; + public function getCredentialFormatIdentifier(): CredentialFormatIdentifiersEnum { return CredentialFormatIdentifiersEnum::JwtVcJson; } /** - * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException - * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException */ public function getVc(): VcClaimValue { @@ -60,6 +71,10 @@ public function getVc(): VcClaimValue $this->getVcType(), $this->getVcCredentialSubject(), $this->getVcIssuer(), + $this->getVcIssuanceDate(), + $this->getVcProof(), + $this->getVcExpirationDate(), + $this->getVcCredentialStatus(), ); } @@ -141,7 +156,7 @@ public function getVcCredentialSubject(): VcCredentialSubjectClaimBag return $this->vcCredentialSubjectClaimBag; } - $claimKeys = [ClaimsEnum::Vc->value, ClaimsEnum::CredentialSubject->value]; + $claimKeys = [ClaimsEnum::Vc->value, ClaimsEnum::Credential_Subject->value]; $vcCredentialSubject = $this->getNestedPayloadClaim(...$claimKeys); @@ -191,6 +206,120 @@ public function getVcIssuer(): VcIssuerClaimValue throw new VcDataModelException('Invalid VC Issuer claim.'); } + /** + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function getVcIssuanceDate(): DateTimeImmutable + { + if ($this->vcIssuanceDate instanceof DateTimeImmutable) { + return $this->vcIssuanceDate; + } + + $issuanceDate = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Issuance_Date->value); + + if (!is_string($issuanceDate)) { + throw new VcDataModelException('Invalid VC Issuance Date claim.'); + } + + try { + return $this->vcIssuanceDate = $this->helpers->dateTime()->parseXsDateTime($issuanceDate); + } catch (\Exception $exception) { + throw new VcDataModelException('Error parsing VC Issuance Date claim: ' . $exception->getMessage()); + } + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function getVcProof(): ?VcProofClaimValue + { + if ($this->vcProofClaimValue === false) { + return null; + } + + if ($this->vcProofClaimValue instanceof VcProofClaimValue) { + return $this->vcProofClaimValue; + } + + $vcProof = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Proof->value); + + if (is_null($vcProof)) { + $this->vcProofClaimValue = false; + return null; + } + + if (is_array($vcProof)) { + return $this->vcProofClaimValue = $this->claimFactory->forVcDataModel()->buildVcProofClaimValue( + $this->helpers->type()->enforceNonEmptyArray($vcProof), + ); + } + + throw new VcDataModelException('Invalid VC Proof claim.'); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function getVcExpirationDate(): ?DateTimeImmutable + { + if ($this->vcExpirationDate === false) { + return null; + } + + if ($this->vcExpirationDate instanceof DateTimeImmutable) { + return $this->vcExpirationDate; + } + + $expirationDate = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Expiration_Date->value); + + if (is_null($expirationDate)) { + $this->vcExpirationDate = false; + return null; + } + + if (is_string($expirationDate)) { + try { + return $this->vcExpirationDate = $this->helpers->dateTime()->parseXsDateTime($expirationDate); + } catch (\Exception $exception) { + throw new VcDataModelException('Error parsing VC Expiration Date claim: ' . $exception->getMessage()); + } + } + + throw new VcDataModelException('Invalid VC Expiration Date claim.'); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getVcCredentialStatus(): ?VcCredentialStatusClaimValue + { + if ($this->vcCredentialStatusClaimValue === false) { + return null; + } + + if ($this->vcCredentialStatusClaimValue instanceof VcCredentialStatusClaimValue) { + return $this->vcCredentialStatusClaimValue; + } + + $credentialStatus = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Credential_Status->value); + + if (is_null($credentialStatus)) { + $this->vcCredentialStatusClaimValue = false; + return null; + } + + if (!is_array($credentialStatus)) { + throw new VcDataModelException('Invalid VC Credential Status Claim.'); + } + + return $this->vcCredentialStatusClaimValue = $this->claimFactory->forVcDataModel() + ->buildVcCredentialStatusClaimValue( + $this->helpers->type()->enforceNonEmptyArray($credentialStatus), + ); + } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ @@ -203,6 +332,10 @@ protected function validate(): void $this->getVcType(...), $this->getVcCredentialSubject(...), $this->getVcIssuer(...), + $this->getVcIssuanceDate(...), + $this->getVcProof(...), + $this->getVcExpirationDate(...), + $this->getVcCredentialStatus(...), ); } } From be52cdd9315de1db01fea2e5ae79a27230c4104e Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Wed, 23 Apr 2025 16:35:56 +0200 Subject: [PATCH 11/66] Move to ClaimInterface --- .../Claims/VcAtContextClaimValue.php | 26 ++++++++++++++----- .../VcDataModel/Claims/VcClaimValue.php | 22 +++++++++++++--- .../Claims/VcCredentialSubjectClaimBag.php | 18 +++++++++++-- .../Claims/VcCredentialSubjectClaimValue.php | 18 +++++++++++-- 4 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php index 9d89ba7..7ad3008 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php @@ -4,11 +4,12 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims; -use JsonSerializable; +use SimpleSAML\OpenID\Claims\ClaimInterface; use SimpleSAML\OpenID\Codebooks\AtContextsEnum; +use SimpleSAML\OpenID\Codebooks\ClaimsEnum; use SimpleSAML\OpenID\Exceptions\VcDataModelException; -class VcAtContextClaimValue implements JsonSerializable +class VcAtContextClaimValue implements ClaimInterface { /** * @param mixed[] $otherContexts @@ -32,10 +33,7 @@ public function __construct( */ public function jsonSerialize(): array { - return [ - $this->baseContext, - ...$this->otherContexts, - ]; + return $this->getValue(); } public function getBaseContext(): string @@ -50,4 +48,20 @@ public function getOtherContexts(): array { return $this->otherContexts; } + + public function getName(): string + { + return ClaimsEnum::AtContext->name; + } + + /** + * @return mixed[] + */ + public function getValue(): array + { + return[ + $this->baseContext, + ...$this->otherContexts, + ]; + } } diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php index aa44033..bbc8c24 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php @@ -5,9 +5,10 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims; use DateTimeImmutable; -use JsonSerializable; +use SimpleSAML\OpenID\Claims\ClaimInterface; +use SimpleSAML\OpenID\Codebooks\ClaimsEnum; -class VcClaimValue implements JsonSerializable +class VcClaimValue implements ClaimInterface { /** * @param null|non-empty-string $id @@ -33,8 +34,7 @@ public function __construct( */ public function jsonSerialize(): array { - // TODO: Implement jsonSerialize() method. - return []; + return $this->getValue(); } public function getAtContext(): VcAtContextClaimValue @@ -87,4 +87,18 @@ public function getCredentialStatus(): ?VcCredentialStatusClaimValue { return $this->credentialStatusClaimValue; } + + public function getName(): string + { + return ClaimsEnum::Vc->value; + } + + /** + * @return mixed[] + */ + public function getValue(): array + { + // TODO: Implement getValue() method. + return []; + } } diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php index 8326547..f10bcbf 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php @@ -4,9 +4,10 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims; -use JsonSerializable; +use SimpleSAML\OpenID\Claims\ClaimInterface; +use SimpleSAML\OpenID\Codebooks\ClaimsEnum; -class VcCredentialSubjectClaimBag implements JsonSerializable +class VcCredentialSubjectClaimBag implements ClaimInterface { /** @var \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimValue[] */ protected array $vcCredentialSubjectClaimValueValues; @@ -25,6 +26,19 @@ public function __construct( * @return mixed[] */ public function jsonSerialize(): array + { + return $this->getValue(); + } + + public function getName(): string + { + return ClaimsEnum::Credential_Subject->value; + } + + /** + * @return mixed[] + */ + public function getValue(): array { return array_map( fn( diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php index dc9e699..77811d7 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php @@ -4,9 +4,10 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims; -use JsonSerializable; +use SimpleSAML\OpenID\Claims\ClaimInterface; +use SimpleSAML\OpenID\Codebooks\ClaimsEnum; -class VcCredentialSubjectClaimValue implements JsonSerializable +class VcCredentialSubjectClaimValue implements ClaimInterface { /** * @param non-empty-array $data @@ -24,6 +25,19 @@ public function get(int|string $key): mixed * @return non-empty-array */ public function jsonSerialize(): array + { + return $this->getValue(); + } + + public function getName(): string + { + return ClaimsEnum::Credential_Subject->value; + } + + /** + * @return non-empty-array + */ + public function getValue(): array { return $this->data; } From 68b189da8f7dc46c453e52e7b9aa87e5969192b7 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Thu, 24 Apr 2025 16:12:42 +0200 Subject: [PATCH 12/66] WIP JwtVcJson --- src/Codebooks/ClaimsEnum.php | 1 + .../AbstractIdentifiedTypedClaimValue.php | 70 ++++++++++++++++++ .../VcDataModel/Claims/VcClaimValue.php | 6 ++ .../Claims/VcCredentialSchemaClaimBag.php | 50 +++++++++++++ .../Claims/VcCredentialSchemaClaimValue.php | 15 ++++ .../Claims/VcCredentialStatusClaimValue.php | 60 +--------------- .../Factories/VcDataModelClaimFactory.php | 71 +++++++++++++++++-- .../VcDataModel/JwtVcJson.php | 49 ++++++++++--- tests/src/Jws/JwsDecoratorTest.php | 2 +- 9 files changed, 249 insertions(+), 75 deletions(-) create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimValue.php diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index eac3be1..a9a5348 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -44,6 +44,7 @@ enum ClaimsEnum: string case CredentialEndpoint = 'credential_endpoint'; case CredentialIssuer = 'credential_issuer'; case CredentialResponseEncryption = 'credential_response_encryption'; + case Credential_Schema = 'credentialSchema'; // CredentialSigningAlgorithmValuesSupported case CredentialSigningAlgValuesSupported = 'credential_signing_alg_values_supported'; case Credential_Status = 'credentialStatus'; diff --git a/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php new file mode 100644 index 0000000..aff4cba --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php @@ -0,0 +1,70 @@ + */ + protected array $data; + + /** + * @param non-empty-string $id, + * @param non-empty-string $type + * @param mixed[] $otherClaims + */ + public function __construct( + protected string $id, + protected string $type, + array $otherClaims = [], + ) { + $this->data = array_merge( + $otherClaims, + [ClaimsEnum::Id->value => $this->$id], + [ClaimsEnum::Type->value => $this->type], + ); + } + + /** + * @return non-empty-string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @return non-empty-string + */ + public function getType(): string + { + return $this->type; + } + + public function getKey(int|string $key): mixed + { + return $this->data[$key] ?? null; + } + + abstract public function getName(): string; + + /** + * @return non-empty-array + */ + public function getValue(): array + { + return $this->data; + } + + /** + * @return non-empty-array + */ + public function jsonSerialize(): array + { + return $this->getValue(); + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php index bbc8c24..1049dba 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php @@ -26,6 +26,7 @@ public function __construct( protected readonly ?VcProofClaimValue $proofClaimValue, protected readonly ?DateTimeImmutable $expirationDate, protected readonly ?VcCredentialStatusClaimValue $credentialStatusClaimValue, + protected readonly ?VcCredentialSchemaClaimBag $credentialSchemaClaimBag, ) { } @@ -88,6 +89,11 @@ public function getCredentialStatus(): ?VcCredentialStatusClaimValue return $this->credentialStatusClaimValue; } + public function getCredentialSchema(): ?VcCredentialSchemaClaimBag + { + return $this->credentialSchemaClaimBag; + } + public function getName(): string { return ClaimsEnum::Vc->value; diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php new file mode 100644 index 0000000..995e440 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php @@ -0,0 +1,50 @@ +vcCredentialSchemaClaimValueValues = [ + $vcCredentialSchemaClaimValue, + ...$vcCredentialSchemaClaimValueValues, + ]; + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + return $this->getValue(); + } + + public function getName(): string + { + return ClaimsEnum::Credential_Schema->value; + } + + /** + * @return mixed[] + */ + public function getValue(): array + { + return array_map( + fn( + VcCredentialSchemaClaimValue $vcCredentialSchemaClaimValue, + ): array => $vcCredentialSchemaClaimValue->jsonSerialize(), + $this->vcCredentialSchemaClaimValueValues, + ); + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimValue.php new file mode 100644 index 0000000..027d946 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimValue.php @@ -0,0 +1,15 @@ +value; + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValue.php index a223154..1690386 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValue.php @@ -4,70 +4,12 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims; -use SimpleSAML\OpenID\Claims\ClaimInterface; use SimpleSAML\OpenID\Codebooks\ClaimsEnum; -class VcCredentialStatusClaimValue implements ClaimInterface +class VcCredentialStatusClaimValue extends AbstractIdentifiedTypedClaimValue { - /** @var non-empty-array */ - protected array $data; - - /** - * @param non-empty-string $id, - * @param non-empty-string $type - * @param mixed[] $otherClaims - */ - public function __construct( - protected string $id, - protected string $type, - array $otherClaims = [], - ) { - $this->data = array_merge( - $otherClaims, - [ClaimsEnum::Id->value => $this->$id], - [ClaimsEnum::Type->value => $this->type], - ); - } - - /** - * @return non-empty-string - */ - public function getId(): string - { - return $this->id; - } - - /** - * @return non-empty-string - */ - public function getType(): string - { - return $this->type; - } - - public function getKey(int|string $key): mixed - { - return $this->data[$key] ?? null; - } - public function getName(): string { return ClaimsEnum::Credential_Status->value; } - - /** - * @return non-empty-array - */ - public function getValue(): array - { - return $this->data; - } - - /** - * @return non-empty-array - */ - public function jsonSerialize(): array - { - return $this->getValue(); - } } diff --git a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php index de215c0..3dfcfa7 100644 --- a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php @@ -11,6 +11,8 @@ use SimpleSAML\OpenID\Helpers; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcAtContextClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcClaimValue; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSchemaClaimBag; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSchemaClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimBag; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimValue; @@ -39,6 +41,7 @@ public function buildVcClaimValue( ?VcProofClaimValue $vcProofClaimValue, ?DateTimeImmutable $vcExpirationDate, ?VcCredentialStatusClaimValue $vcCredentialStatusClaimValue, + ?VcCredentialSchemaClaimBag $vcCredentialSchemaClaimBag, ): VcClaimValue { return new VcClaimValue( $vcAtContextClaimValue, @@ -50,6 +53,7 @@ public function buildVcClaimValue( $vcProofClaimValue, $vcExpirationDate, $vcCredentialStatusClaimValue, + $vcCredentialSchemaClaimBag, ); } @@ -71,10 +75,17 @@ public function buildVcCredentialSubjectClaimValue(array $data): VcCredentialSub } /** - * @param non-empty-array> $data + * @param mixed[] $data + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException */ public function buildVcCredentialSubjectClaimBag(array $data): VcCredentialSubjectClaimBag { + if ($this->helpers->arr()->isAssociative($data)) { + $data = [$data]; + } + + $data = $this->helpers->type()->enforceNonEmptyArrayOfNonEmptyArrays($data); + $vcCredentialSubjectClaimValueData = array_shift($data); $vcCredentialSubjectClaimValue = $this->buildVcCredentialSubjectClaimValue($vcCredentialSubjectClaimValueData); @@ -91,14 +102,14 @@ public function buildVcCredentialSubjectClaimBag(array $data): VcCredentialSubje } /** - * @param non-empty-array $data + * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException */ public function buildVcIssuerClaimValue(array $data): VcIssuerClaimValue { $id = $data[ClaimsEnum::Id->value] ?? throw new VcDataModelException( - 'No Issuer ID claim value available.', + 'No ID claim value available.', ); $id = $this->helpers->type()->enforceUri($id); @@ -107,7 +118,7 @@ public function buildVcIssuerClaimValue(array $data): VcIssuerClaimValue } /** - * @param non-empty-array $data + * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException */ @@ -123,14 +134,14 @@ public function buildVcProofClaimValue(array $data): VcProofClaimValue } /** - * @param non-empty-array $data + * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException */ public function buildVcCredentialStatusClaimValue(array $data): VcCredentialStatusClaimValue { $id = $data[ClaimsEnum::Id->value] ?? throw new VcDataModelException( - 'No Issuer ID claim value available.', + 'No ID claim value available.', ); $id = $this->helpers->type()->enforceUri($id); @@ -141,4 +152,52 @@ public function buildVcCredentialStatusClaimValue(array $data): VcCredentialStat return new VcCredentialStatusClaimValue($id, $type, $data); } + + /** + * @param non-empty-array $data + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function buildVcCredentialSchemaClaimValue(array $data): VcCredentialSchemaClaimValue + { + $id = $data[ClaimsEnum::Id->value] ?? throw new VcDataModelException( + 'No ID claim value available.', + ); + $id = $this->helpers->type()->enforceUri($id); + + $type = $data[ClaimsEnum::Type->value] ?? throw new VcDataModelException( + 'No Type claim value available.', + ); + $type = $this->helpers->type()->ensureNonEmptyString($type); + + return new VcCredentialSchemaClaimValue($id, $type, $data); + } + + /** + * @param mixed[] $data + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function buildVcCredentialSchemaClaimBag(array $data): VcCredentialSchemaClaimBag + { + if ($this->helpers->arr()->isAssociative($data)) { + $data = [$data]; + } + + $data = $this->helpers->type()->enforceNonEmptyArrayOfNonEmptyArrays($data); + + $vcCredentialSchemaClaimValueData = array_shift($data); + + $vcCredentialSchemaClaimValue = $this->buildVcCredentialSchemaClaimValue($vcCredentialSchemaClaimValueData); + + $vcCredentialSchemaClaimValues = array_map( + fn (array $data): VcCredentialSchemaClaimValue => $this->buildVcCredentialSchemaClaimValue($data), + $data, + ); + + return new VcCredentialSchemaClaimBag( + $vcCredentialSchemaClaimValue, + ...$vcCredentialSchemaClaimValues, + ); + } } diff --git a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php index 4ec8cd2..b0dbbb6 100644 --- a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php +++ b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php @@ -11,6 +11,7 @@ use SimpleSAML\OpenID\Jws\ParsedJws; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcAtContextClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcClaimValue; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSchemaClaimBag; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimBag; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcIssuerClaimValue; @@ -41,6 +42,8 @@ class JwtVcJson extends ParsedJws implements VerifiableCredentialInterface protected null|false|VcCredentialStatusClaimValue $vcCredentialStatusClaimValue = null; + protected null|false|VcCredentialSchemaClaimBag $vcCredentialSchemaClaimBag = null; + public function getCredentialFormatIdentifier(): CredentialFormatIdentifiersEnum { return CredentialFormatIdentifiersEnum::JwtVcJson; @@ -75,6 +78,7 @@ public function getVc(): VcClaimValue $this->getVcProof(), $this->getVcExpirationDate(), $this->getVcCredentialStatus(), + $this->getVcCredentialSchema(), ); } @@ -164,14 +168,9 @@ public function getVcCredentialSubject(): VcCredentialSubjectClaimBag throw new VcDataModelException('Invalid VC Credential Subject claim.'); } - if ($this->helpers->arr()->isAssociative($vcCredentialSubject)) { - return $this->vcCredentialSubjectClaimBag = $this->claimFactory->forVcDataModel() - ->buildVcCredentialSubjectClaimBag([$vcCredentialSubject]); - } - return $this->vcCredentialSubjectClaimBag = $this->claimFactory->forVcDataModel() ->buildVcCredentialSubjectClaimBag( - $this->helpers->type()->enforceNonEmptyArrayOfNonEmptyArrays($vcCredentialSubject), + $vcCredentialSubject, ); } @@ -199,7 +198,7 @@ public function getVcIssuer(): VcIssuerClaimValue if (is_array($vcIssuer)) { return $this->vcIssuerClaimValue = $this->claimFactory->forVcDataModel()->buildVcIssuerClaimValue( - $this->helpers->type()->enforceNonEmptyArray($vcIssuer), + $vcIssuer, ); } @@ -251,7 +250,7 @@ public function getVcProof(): ?VcProofClaimValue if (is_array($vcProof)) { return $this->vcProofClaimValue = $this->claimFactory->forVcDataModel()->buildVcProofClaimValue( - $this->helpers->type()->enforceNonEmptyArray($vcProof), + $vcProof, ); } @@ -316,7 +315,38 @@ public function getVcCredentialStatus(): ?VcCredentialStatusClaimValue return $this->vcCredentialStatusClaimValue = $this->claimFactory->forVcDataModel() ->buildVcCredentialStatusClaimValue( - $this->helpers->type()->enforceNonEmptyArray($credentialStatus), + $credentialStatus, + ); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getVcCredentialSchema(): ?VcCredentialSchemaClaimBag + { + if ($this->vcCredentialSchemaClaimBag === false) { + return null; + } + + if ($this->vcCredentialSchemaClaimBag instanceof VcCredentialSchemaClaimBag) { + return $this->vcCredentialSchemaClaimBag; + } + + $credentialSchema = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Credential_Schema->value); + + if (is_null($credentialSchema)) { + $this->vcCredentialSchemaClaimBag = false; + return null; + } + + if (!is_array($credentialSchema)) { + throw new VcDataModelException('Invalid VC Credential Schema claim.'); + } + + return $this->vcCredentialSchemaClaimBag = $this->claimFactory->forVcDataModel() + ->buildVcCredentialSchemaClaimBag( + $credentialSchema, ); } @@ -336,6 +366,7 @@ protected function validate(): void $this->getVcProof(...), $this->getVcExpirationDate(...), $this->getVcCredentialStatus(...), + $this->getVcCredentialSchema(...), ); } } diff --git a/tests/src/Jws/JwsDecoratorTest.php b/tests/src/Jws/JwsDecoratorTest.php index 0ec78d6..4532cb3 100644 --- a/tests/src/Jws/JwsDecoratorTest.php +++ b/tests/src/Jws/JwsDecoratorTest.php @@ -12,7 +12,7 @@ #[CoversClass(JwsDecorator::class)] final class JwsDecoratorTest extends TestCase { - protected JWS $jwsMock; + protected \PHPUnit\Framework\MockObject\MockObject $jwsMock; protected function setUp(): void { From 06cafe819785b3d686863a6c1d1ea3f2a6392805 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Fri, 25 Apr 2025 17:17:43 +0200 Subject: [PATCH 13/66] WIP JwtVcJson --- src/Codebooks/ClaimsEnum.php | 9 +- .../AbstractIdentifiedTypedClaimValue.php | 18 +- .../Claims/AbstractTypedClaimValue.php | 55 ++++++ .../VcDataModel/Claims/TypeClaimValue.php | 54 +++++ .../VcDataModel/Claims/VcClaimValue.php | 14 +- .../VcDataModel/Claims/VcEvidenceClaimBag.php | 50 +++++ .../Claims/VcEvidenceClaimValue.php | 15 ++ .../VcDataModel/Claims/VcIssuerClaimValue.php | 3 +- .../VcDataModel/Claims/VcProofClaimValue.php | 52 +---- .../Claims/VcRefreshServiceClaimBag.php | 50 +++++ .../Claims/VcRefreshServiceClaimValue.php | 15 ++ .../Claims/VcTermsOfUseClaimBag.php | 50 +++++ .../Claims/VcTermsOfUseClaimValue.php | 15 ++ .../Factories/VcDataModelClaimFactory.php | 187 +++++++++++++++++- .../VcDataModel/JwtVcJson.php | 125 +++++++++++- 15 files changed, 620 insertions(+), 92 deletions(-) create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValue.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValue.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimValue.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimValue.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php create mode 100644 src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimValue.php diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index a9a5348..9226384 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -55,12 +55,13 @@ enum ClaimsEnum: string case Description = 'description'; case Display = 'display'; case EndSessionEndpoint = 'end_session_endpoint'; - // ExpirationTime - case Exp = 'exp'; - case Expiration_Date = 'expirationDate'; case EncryptionRequired = 'encryption_required'; // EncryptionValuesSupported case EncValuesSupported = 'enc_values_supported'; + case Evidence = 'evidence'; + // ExpirationTime + case Exp = 'exp'; + case Expiration_Date = 'expirationDate'; case FederationFetchEndpoint = 'federation_fetch_endpoint'; case FederationListEndpoint = 'federation_list_endpoint'; case FederationTrustMarkEndpoint = 'federation_trust_mark_endpoint'; @@ -117,6 +118,7 @@ enum ClaimsEnum: string case ProofTypesSupported = 'proof_types_supported'; // Reference case Ref = 'ref'; + case Refresh_Service = 'refreshService'; // PublicKeyUse case RedirectUris = 'redirect_uris'; case RegistrationEndpoint = 'registration_endpoint'; @@ -139,6 +141,7 @@ enum ClaimsEnum: string // Subject case Sub = 'sub'; case SubjectTypesSupported = 'subject_types_supported'; + case Terms_Of_Use = 'termsOfUse'; case TextColor = 'text_color'; case TokenEndpoint = 'token_endpoint'; case TokenEndpointAuthMethod = 'token_endpoint_auth_method'; diff --git a/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php index aff4cba..7c3149a 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php @@ -10,22 +10,21 @@ abstract class AbstractIdentifiedTypedClaimValue implements ClaimInterface { /** @var non-empty-array */ - protected array $data; + protected readonly array $data; /** * @param non-empty-string $id, - * @param non-empty-string $type * @param mixed[] $otherClaims */ public function __construct( - protected string $id, - protected string $type, + protected readonly string $id, + protected readonly TypeClaimValue $typeClaimValue, array $otherClaims = [], ) { $this->data = array_merge( $otherClaims, - [ClaimsEnum::Id->value => $this->$id], - [ClaimsEnum::Type->value => $this->type], + [ClaimsEnum::Id->value => $this->id], + [ClaimsEnum::Type->value => $this->typeClaimValue->jsonSerialize()], ); } @@ -37,12 +36,9 @@ public function getId(): string return $this->id; } - /** - * @return non-empty-string - */ - public function getType(): string + public function getType(): TypeClaimValue { - return $this->type; + return $this->typeClaimValue; } public function getKey(int|string $key): mixed diff --git a/src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValue.php new file mode 100644 index 0000000..f48698d --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValue.php @@ -0,0 +1,55 @@ + */ + protected readonly array $data; + + /** + * @param mixed[] $otherClaims + */ + public function __construct( + protected readonly TypeClaimValue $typeClaimValue, + array $otherClaims = [], + ) { + $this->data = array_merge( + $otherClaims, + [ClaimsEnum::Type->value => $this->typeClaimValue->jsonSerialize()], + ); + } + + public function getType(): TypeClaimValue + { + return $this->typeClaimValue; + } + + public function getKey(int|string $key): mixed + { + return $this->data[$key] ?? null; + } + + abstract public function getName(): string; + + /** + * @return non-empty-array + */ + public function getValue(): array + { + return $this->data; + } + + /** + * @return non-empty-array + */ + public function jsonSerialize(): array + { + return $this->getValue(); + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValue.php new file mode 100644 index 0000000..f08b92a --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValue.php @@ -0,0 +1,54 @@ +value; + } + + /** + * @return non-empty-string[] + */ + public function getValue(): array + { + return $this->types; + } + + /** + * @return non-empty-string|non-empty-string[] + */ + public function jsonSerialize(): string|array + { + $value = $this->getValue(); + + if (count($value) === 1) { + return $value[0]; + } + + return $value; + } + + /** + * @param non-empty-string $type + */ + public function has(string $type): bool + { + return in_array($type, $this->types); + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php index 1049dba..9646e89 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php @@ -12,14 +12,12 @@ class VcClaimValue implements ClaimInterface { /** * @param null|non-empty-string $id - * @param non-empty-array $type */ public function __construct( protected readonly VcAtContextClaimValue $atContextClaimValue, /** @var null|non-empty-string */ protected readonly null|string $id, - /** @var non-empty-array */ - protected readonly array $type, + protected readonly TypeClaimValue $typeClaimValue, protected readonly VcCredentialSubjectClaimBag $credentialSubjectClaimBag, protected readonly VcIssuerClaimValue $issuerClaimValue, protected readonly DateTimeImmutable $issuanceDate, @@ -27,6 +25,9 @@ public function __construct( protected readonly ?DateTimeImmutable $expirationDate, protected readonly ?VcCredentialStatusClaimValue $credentialStatusClaimValue, protected readonly ?VcCredentialSchemaClaimBag $credentialSchemaClaimBag, + protected readonly ?VcRefreshServiceClaimBag $refreshServiceClaimBag, + protected readonly ?VcTermsOfUseClaimBag $termsOfUseClaimBag, + protected readonly ?VcEvidenceClaimBag $evidenceClaimBag, ) { } @@ -51,12 +52,9 @@ public function getId(): ?string return $this->id; } - /** - * @return non-empty-array - */ - public function getType(): array + public function getType(): TypeClaimValue { - return $this->type; + return $this->typeClaimValue; } public function getCredentialSubject(): VcCredentialSubjectClaimBag diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php new file mode 100644 index 0000000..fe75861 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php @@ -0,0 +1,50 @@ +vcEvidenceClaimValueValues = [ + $vcEvidenceClaimValue, + ...$vcEvidenceClaimValueValues, + ]; + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + return $this->getValue(); + } + + public function getName(): string + { + return ClaimsEnum::Evidence->value; + } + + /** + * @return mixed[] + */ + public function getValue(): array + { + return array_map( + fn( + VcEvidenceClaimValue $vcEvidenceClaimValue, + ): array => $vcEvidenceClaimValue->jsonSerialize(), + $this->vcEvidenceClaimValueValues, + ); + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimValue.php new file mode 100644 index 0000000..d55fca2 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimValue.php @@ -0,0 +1,15 @@ +value; + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php index 8de4189..7de1117 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php @@ -19,7 +19,6 @@ class VcIssuerClaimValue implements ClaimInterface public function __construct( protected string $id, array $otherClaims = [], - protected readonly string $name = ClaimsEnum::Issuer->value, ) { $this->data = array_merge( $otherClaims, @@ -42,7 +41,7 @@ public function getKey(int|string $key): mixed public function getName(): string { - return $this->name; + return ClaimsEnum::Issuer->value; } /** diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValue.php index 205d1d9..ede1e1e 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValue.php @@ -4,60 +4,12 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims; -use SimpleSAML\OpenID\Claims\ClaimInterface; use SimpleSAML\OpenID\Codebooks\ClaimsEnum; -class VcProofClaimValue implements ClaimInterface +class VcProofClaimValue extends AbstractTypedClaimValue { - /** @var non-empty-array */ - protected array $data; - - /** - * @param non-empty-string $type - * @param mixed[] $otherClaims - */ - public function __construct( - protected string $type, - array $otherClaims = [], - protected readonly string $name = ClaimsEnum::Proof->value, - ) { - $this->data = array_merge( - $otherClaims, - [ClaimsEnum::Type->value => $this->type], - ); - } - - /** - * @return non-empty-string - */ - public function getType(): string - { - return $this->type; - } - - public function getKey(int|string $key): mixed - { - return $this->data[$key] ?? null; - } - public function getName(): string { - return $this->name; - } - - /** - * @return non-empty-array - */ - public function getValue(): array - { - return $this->data; - } - - /** - * @return non-empty-array - */ - public function jsonSerialize(): array - { - return $this->getValue(); + return ClaimsEnum::Proof->value; } } diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php new file mode 100644 index 0000000..3e6c4f7 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php @@ -0,0 +1,50 @@ +vcRefreshServiceClaimValueValues = [ + $vcRefreshServiceClaimValue, + ...$vcRefreshServiceClaimValueValues, + ]; + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + return $this->getValue(); + } + + public function getName(): string + { + return ClaimsEnum::Refresh_Service->value; + } + + /** + * @return mixed[] + */ + public function getValue(): array + { + return array_map( + fn( + VcRefreshServiceClaimValue $vcRefreshServiceClaimValue, + ): array => $vcRefreshServiceClaimValue->jsonSerialize(), + $this->vcRefreshServiceClaimValueValues, + ); + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimValue.php new file mode 100644 index 0000000..3973d89 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimValue.php @@ -0,0 +1,15 @@ +value; + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php new file mode 100644 index 0000000..177d4e8 --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php @@ -0,0 +1,50 @@ +vcTermsOfUseClaimValueValues = [ + $vcTermsOfUseClaimValue, + ...$vcTermsOfUseClaimValueValues, + ]; + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + return $this->getValue(); + } + + public function getName(): string + { + return ClaimsEnum::Terms_Of_Use->value; + } + + /** + * @return mixed[] + */ + public function getValue(): array + { + return array_map( + fn( + VcTermsOfUseClaimValue $vcTermsOfUseClaimValue, + ): array => $vcTermsOfUseClaimValue->jsonSerialize(), + $this->vcTermsOfUseClaimValueValues, + ); + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimValue.php new file mode 100644 index 0000000..16310ce --- /dev/null +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimValue.php @@ -0,0 +1,15 @@ +value; + } +} diff --git a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php index 3dfcfa7..7b562ef 100644 --- a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php @@ -9,6 +9,7 @@ use SimpleSAML\OpenID\Exceptions\VcDataModelException; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\TypeClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcAtContextClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSchemaClaimBag; @@ -16,8 +17,14 @@ use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimBag; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimValue; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcEvidenceClaimBag; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcEvidenceClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcIssuerClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcProofClaimValue; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcRefreshServiceClaimBag; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcRefreshServiceClaimValue; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcTermsOfUseClaimBag; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcTermsOfUseClaimValue; class VcDataModelClaimFactory { @@ -29,12 +36,11 @@ public function __construct( /** * @param non-empty-string|null $vcId - * @param non-empty-array $vcType */ public function buildVcClaimValue( VcAtContextClaimValue $vcAtContextClaimValue, null|string $vcId, - array $vcType, + TypeClaimValue $vcTypeClaimValue, VcCredentialSubjectClaimBag $vcCredentialSubjectClaimBag, VcIssuerClaimValue $vcIssuerClaimValue, DateTimeImmutable $vcIssuanceDate, @@ -42,11 +48,14 @@ public function buildVcClaimValue( ?DateTimeImmutable $vcExpirationDate, ?VcCredentialStatusClaimValue $vcCredentialStatusClaimValue, ?VcCredentialSchemaClaimBag $vcCredentialSchemaClaimBag, + ?VcRefreshServiceClaimBag $vcRefreshServiceClaimBag, + ?VcTermsOfUseClaimBag $vcTermsOfUseClaimBag, + ?VcEvidenceClaimBag $vcEvidenceClaimBag, ): VcClaimValue { return new VcClaimValue( $vcAtContextClaimValue, $vcId, - $vcType, + $vcTypeClaimValue, $vcCredentialSubjectClaimBag, $vcIssuerClaimValue, $vcIssuanceDate, @@ -54,6 +63,9 @@ public function buildVcClaimValue( $vcExpirationDate, $vcCredentialStatusClaimValue, $vcCredentialSchemaClaimBag, + $vcRefreshServiceClaimBag, + $vcTermsOfUseClaimBag, + $vcEvidenceClaimBag, ); } @@ -66,6 +78,27 @@ public function buildVcAtContextClaimValue(string $baseContext, array $otherCont return new VcAtContextClaimValue($baseContext, $otherContexts); } + /** + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function buildTypeClaimValue(mixed $data): TypeClaimValue + { + if (is_string($data)) { + $data = [$data]; + } + + if (!is_array($data)) { + throw new VcDataModelException('Invalid Type claim value.'); + } + + return new TypeClaimValue( + $this->helpers->type()->enforceNonEmptyArrayWithValuesAsNonEmptyStrings( + $data, + ), + ); + } + /** * @param non-empty-array $data */ @@ -128,9 +161,9 @@ public function buildVcProofClaimValue(array $data): VcProofClaimValue 'No Type claim value available.', ); - $type = $this->helpers->type()->ensureNonEmptyString($type); + $typeClaimValue = $this->buildTypeClaimValue($type); - return new VcProofClaimValue($type, $data); + return new VcProofClaimValue($typeClaimValue, $data); } /** @@ -148,9 +181,9 @@ public function buildVcCredentialStatusClaimValue(array $data): VcCredentialStat $type = $data[ClaimsEnum::Type->value] ?? throw new VcDataModelException( 'No Type claim value available.', ); - $type = $this->helpers->type()->ensureNonEmptyString($type); + $typeClaimValue = $this->buildTypeClaimValue($type); - return new VcCredentialStatusClaimValue($id, $type, $data); + return new VcCredentialStatusClaimValue($id, $typeClaimValue, $data); } /** @@ -168,9 +201,9 @@ public function buildVcCredentialSchemaClaimValue(array $data): VcCredentialSche $type = $data[ClaimsEnum::Type->value] ?? throw new VcDataModelException( 'No Type claim value available.', ); - $type = $this->helpers->type()->ensureNonEmptyString($type); + $typeClaimValue = $this->buildTypeClaimValue($type); - return new VcCredentialSchemaClaimValue($id, $type, $data); + return new VcCredentialSchemaClaimValue($id, $typeClaimValue, $data); } /** @@ -200,4 +233,140 @@ public function buildVcCredentialSchemaClaimBag(array $data): VcCredentialSchema ...$vcCredentialSchemaClaimValues, ); } + + /** + * @param non-empty-array $data + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function buildVcRefreshServiceClaimValue(array $data): VcRefreshServiceClaimValue + { + $id = $data[ClaimsEnum::Id->value] ?? throw new VcDataModelException( + 'No ID claim value available.', + ); + $id = $this->helpers->type()->enforceUri($id); + + $type = $data[ClaimsEnum::Type->value] ?? throw new VcDataModelException( + 'No Type claim value available.', + ); + $typeClaimValue = $this->buildTypeClaimValue($type); + + return new VcRefreshServiceClaimValue($id, $typeClaimValue, $data); + } + + /** + * @param mixed[] $data + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function buildVcRefreshServiceClaimBag(array $data): VcRefreshServiceClaimBag + { + if ($this->helpers->arr()->isAssociative($data)) { + $data = [$data]; + } + + $data = $this->helpers->type()->enforceNonEmptyArrayOfNonEmptyArrays($data); + + $vcRefreshServiceClaimValueData = array_shift($data); + + $vcRefreshServiceClaimValue = $this->buildVcRefreshServiceClaimValue($vcRefreshServiceClaimValueData); + + $vcRefreshServiceClaimValues = array_map( + fn (array $data): VcRefreshServiceClaimValue => $this->buildVcRefreshServiceClaimValue($data), + $data, + ); + + return new VcRefreshServiceClaimBag( + $vcRefreshServiceClaimValue, + ...$vcRefreshServiceClaimValues, + ); + } + + /** + * @param mixed[] $data + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function buildVcTermsOfUseClaimValue(array $data): VcTermsOfUseClaimValue + { + $type = $data[ClaimsEnum::Type->value] ?? throw new VcDataModelException( + 'No Type claim value available.', + ); + + $typeClaimValue = $this->buildTypeClaimValue($type); + + return new VcTermsOfUseClaimValue($typeClaimValue, $data); + } + + /** + * @param mixed[] $data + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function buildVcTermsOfUseClaimBag(array $data): VcTermsOfUseClaimBag + { + if ($this->helpers->arr()->isAssociative($data)) { + $data = [$data]; + } + + $data = $this->helpers->type()->enforceNonEmptyArrayOfNonEmptyArrays($data); + + $vcTermsOfUseClaimValueData = array_shift($data); + + $vcTermsOfUseClaimValue = $this->buildVcTermsOfUseClaimValue($vcTermsOfUseClaimValueData); + + $vcTermsOfUseClaimValues = array_map( + fn (array $data): VcTermsOfUseClaimValue => $this->buildVcTermsOfUseClaimValue($data), + $data, + ); + + return new VcTermsOfUseClaimBag( + $vcTermsOfUseClaimValue, + ...$vcTermsOfUseClaimValues, + ); + } + + /** + * @param mixed[] $data + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function buildVcEvidenceClaimValue(array $data): VcEvidenceClaimValue + { + $type = $data[ClaimsEnum::Type->value] ?? throw new VcDataModelException( + 'No Type claim value available.', + ); + + $typeClaimValue = $this->buildTypeClaimValue($type); + + return new VcEvidenceClaimValue($typeClaimValue, $data); + } + + /** + * @param mixed[] $data + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + */ + public function buildVcEvidenceClaimBag(array $data): VcEvidenceClaimBag + { + if ($this->helpers->arr()->isAssociative($data)) { + $data = [$data]; + } + + $data = $this->helpers->type()->enforceNonEmptyArrayOfNonEmptyArrays($data); + + $vcEvidenceClaimValueData = array_shift($data); + + $vcEvidenceClaimValue = $this->buildVcEvidenceClaimValue($vcEvidenceClaimValueData); + + $vcEvidenceClaimValues = array_map( + fn (array $data): VcEvidenceClaimValue => $this->buildVcEvidenceClaimValue($data), + $data, + ); + + return new VcEvidenceClaimBag( + $vcEvidenceClaimValue, + ...$vcEvidenceClaimValues, + ); + } } diff --git a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php index b0dbbb6..5c80cab 100644 --- a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php +++ b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php @@ -9,13 +9,17 @@ use SimpleSAML\OpenID\Codebooks\CredentialFormatIdentifiersEnum; use SimpleSAML\OpenID\Exceptions\VcDataModelException; use SimpleSAML\OpenID\Jws\ParsedJws; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\TypeClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcAtContextClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSchemaClaimBag; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialStatusClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimBag; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcEvidenceClaimBag; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcIssuerClaimValue; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcProofClaimValue; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcRefreshServiceClaimBag; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcTermsOfUseClaimBag; use SimpleSAML\OpenID\VerifiableCredentials\VerifiableCredentialInterface; class JwtVcJson extends ParsedJws implements VerifiableCredentialInterface @@ -27,8 +31,7 @@ class JwtVcJson extends ParsedJws implements VerifiableCredentialInterface /** @var null|false|non-empty-string */ protected null|false|string $vcId = null; - /** @var ?non-empty-array */ - protected ?array $vcType = null; + protected ?TypeClaimValue $vcTypeClaimValue = null; protected ?VcCredentialSubjectClaimBag $vcCredentialSubjectClaimBag = null; @@ -44,6 +47,12 @@ class JwtVcJson extends ParsedJws implements VerifiableCredentialInterface protected null|false|VcCredentialSchemaClaimBag $vcCredentialSchemaClaimBag = null; + protected null|false|VcRefreshServiceClaimBag $vcRefreshServiceClaimBag = null; + + protected null|false|VcTermsOfUseClaimBag $vcTermsOfUseClaimBag = null; + + protected null|false|VcEvidenceClaimBag $vcEvidenceClaimBag = null; + public function getCredentialFormatIdentifier(): CredentialFormatIdentifiersEnum { return CredentialFormatIdentifiersEnum::JwtVcJson; @@ -56,7 +65,7 @@ public function getCredentialFormatIdentifier(): CredentialFormatIdentifiersEnum */ public function getVc(): VcClaimValue { - if ($this->vcClaimValue instanceof \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcClaimValue) { + if ($this->vcClaimValue instanceof VcClaimValue) { return $this->vcClaimValue; } @@ -79,6 +88,9 @@ public function getVc(): VcClaimValue $this->getVcExpirationDate(), $this->getVcCredentialStatus(), $this->getVcCredentialSchema(), + $this->getVcRefreshService(), + $this->getVcTermsOfUse(), + $this->getVcEvidence(), ); } @@ -128,14 +140,13 @@ public function getVcId(): ?string } /** - * @return non-empty-array * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException */ - public function getVcType(): array + public function getVcType(): TypeClaimValue { - if ($this->vcType !== null) { - return $this->vcType; + if ($this->vcTypeClaimValue instanceof TypeClaimValue) { + return $this->vcTypeClaimValue; } $claimKeys = [ClaimsEnum::Vc->value, ClaimsEnum::Type->value]; @@ -143,11 +154,11 @@ public function getVcType(): array $vcType = $this->getNestedPayloadClaim(...$claimKeys) ?? $this->getNestedPayloadClaim(...$claimKeys2); - if ((!is_array($vcType)) || $vcType === []) { + if (is_null($vcType)) { throw new VcDataModelException('Invalid VC Type claim.'); } - return $this->vcType = $this->helpers->type()->enforceNonEmptyArrayWithValuesAsNonEmptyStrings($vcType); + return $this->vcTypeClaimValue = $this->claimFactory->forVcDataModel()->buildTypeClaimValue($vcType); } /** @@ -350,6 +361,99 @@ public function getVcCredentialSchema(): ?VcCredentialSchemaClaimBag ); } + /** + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getVcRefreshService(): ?VcRefreshServiceClaimBag + { + if ($this->vcRefreshServiceClaimBag === false) { + return null; + } + + if ($this->vcRefreshServiceClaimBag instanceof VcRefreshServiceClaimBag) { + return $this->vcRefreshServiceClaimBag; + } + + $refreshService = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Refresh_Service->value); + + if (is_null($refreshService)) { + $this->vcRefreshServiceClaimBag = false; + return null; + } + + if (!is_array($refreshService)) { + throw new VcDataModelException('Invalid VC Refresh Service claim.'); + } + + return $this->vcRefreshServiceClaimBag = $this->claimFactory->forVcDataModel() + ->buildVcRefreshServiceClaimBag( + $refreshService, + ); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getVcTermsOfUse(): ?VcTermsOfUseClaimBag + { + if ($this->vcTermsOfUseClaimBag === false) { + return null; + } + + if ($this->vcTermsOfUseClaimBag instanceof VcTermsOfUseClaimBag) { + return $this->vcTermsOfUseClaimBag; + } + + $termsOfUse = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Terms_Of_Use->value); + + if (is_null($termsOfUse)) { + $this->vcTermsOfUseClaimBag = false; + return null; + } + + if (!is_array($termsOfUse)) { + throw new VcDataModelException('Invalid VC Terms Of Use claim.'); + } + + return $this->vcTermsOfUseClaimBag = $this->claimFactory->forVcDataModel() + ->buildVcTermsOfUseClaimBag( + $termsOfUse, + ); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getVcEvidence(): ?VcEvidenceClaimBag + { + if ($this->vcEvidenceClaimBag === false) { + return null; + } + + if ($this->vcEvidenceClaimBag instanceof VcEvidenceClaimBag) { + return $this->vcEvidenceClaimBag; + } + + $evidence = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Evidence->value); + + if (is_null($evidence)) { + $this->vcEvidenceClaimBag = false; + return null; + } + + if (!is_array($evidence)) { + throw new VcDataModelException('Invalid VC Evidence claim.'); + } + + return $this->vcEvidenceClaimBag = $this->claimFactory->forVcDataModel() + ->buildVcEvidenceClaimBag( + $evidence, + ); + } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ @@ -367,6 +471,9 @@ protected function validate(): void $this->getVcExpirationDate(...), $this->getVcCredentialStatus(...), $this->getVcCredentialSchema(...), + $this->getVcRefreshService(...), + $this->getVcTermsOfUse(...), + $this->getVcEvidence(...), ); } } From 498b7592753a985c4718020562cab481bd651e87 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Tue, 29 Apr 2025 20:53:09 +0200 Subject: [PATCH 14/66] WIP JwtVcJson --- composer.json | 2 +- src/Codebooks/ClaimsEnum.php | 3 + src/Helpers/DateTime.php | 18 +- src/Jws/ParsedJws.php | 31 +++- .../Claims/VcCredentialSchemaClaimBag.php | 16 +- .../Claims/VcCredentialSubjectClaimBag.php | 16 +- .../VcDataModel/Claims/VcEvidenceClaimBag.php | 16 +- .../Claims/VcRefreshServiceClaimBag.php | 16 +- .../Claims/VcTermsOfUseClaimBag.php | 16 +- .../Factories/JwtVcJsonFactory.php | 5 +- .../Factories/VcDataModelClaimFactory.php | 27 ++- .../VcDataModel/JwtVcJson.php | 60 ++++++- .../Factories/EntityStatementFactoryTest.php | 2 - .../Factories/JwtVcJsonFactoryTest.php | 164 ++++++++++++++++++ 14 files changed, 336 insertions(+), 56 deletions(-) create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php diff --git a/composer.json b/composer.json index e6f7e24..86d8d7d 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "vendor/bin/phpcbf", "vendor/bin/phpcs -p", "composer update web-token/jwt-framework --with web-token/jwt-framework:^3.0", - "vendor/bin/phpstan", + "vendor/bin/phpstan --memory-limit=1024M", "vendor/bin/phpunit --no-coverage", "composer update web-token/jwt-framework --with web-token/jwt-framework:^4.0", "vendor/bin/phpstan", diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index 9226384..b4bb407 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -102,6 +102,9 @@ enum ClaimsEnum: string case MetadataPolicyCrit = 'metadata_policy_crit'; case Name = 'name'; case NonceEndpoint = 'nonce_endpoint'; + // NotBefore + case Nbf = 'nbf'; + case Notification = 'notification'; case NotificationEndpoint = 'notification_endpoint'; // OpenIDProviderPolicyUri case OpPolicyUri = 'op_policy_uri'; diff --git a/src/Helpers/DateTime.php b/src/Helpers/DateTime.php index 19b14c2..d003972 100644 --- a/src/Helpers/DateTime.php +++ b/src/Helpers/DateTime.php @@ -13,7 +13,7 @@ class DateTime /** * @throws \Exception */ - public function parseXsDateTime(string $input, ?DateTimeZone $tz = null): DateTimeImmutable + public function fromXsDateTime(string $input, ?DateTimeZone $tz = null): DateTimeImmutable { // Try extended RFC3339 (with microseconds) $dt = DateTimeImmutable::createFromFormat( @@ -38,4 +38,20 @@ public function parseXsDateTime(string $input, ?DateTimeZone $tz = null): DateTi // Finally, let the constructor attempt flexible ISO8601 parsing return new DateTimeImmutable($input, $tz); } + + /** + * @throws \Exception + */ + public function getUtc(string $datetime = 'now'): DateTimeImmutable + { + return new DateTimeImmutable($datetime, new DateTimeZone('UTC')); + } + + /** + * @throws \Exception + */ + public function fromTimestamp(int $timestamp): DateTimeImmutable + { + return $this->getUtc()->setTimestamp($timestamp); + } } diff --git a/src/Jws/ParsedJws.php b/src/Jws/ParsedJws.php index e0a4e6b..4bcebce 100644 --- a/src/Jws/ParsedJws.php +++ b/src/Jws/ParsedJws.php @@ -156,8 +156,9 @@ public function verifyWithKeySet(array $jwks, int $signatureIndex = 0): void } /** - * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @return ?non-empty-string + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ public function getIssuer(): ?string { @@ -171,8 +172,9 @@ public function getIssuer(): ?string } /** - * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @return ?non-empty-string + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ public function getSubject(): ?string { @@ -184,8 +186,9 @@ public function getSubject(): ?string } /** - * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @return ?string[] + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ public function getAudience(): ?array { @@ -210,6 +213,7 @@ public function getAudience(): ?array /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\ClientAssertionException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @return ?non-empty-string */ public function getJwtId(): ?string @@ -242,6 +246,27 @@ public function getExpirationTime(): ?int return $exp; } + /** + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getNotBefore(): ?int + { + $nbf = $this->getPayloadClaim(ClaimsEnum::Nbf->value); + + if (is_null($nbf)) { + return null; + } + + $nbf = $this->helpers->type()->ensureInt($nbf); + + if ($nbf - $this->timestampValidationLeeway->getInSeconds() > time()) { + throw new JwsException(sprintf('Not Before claim (%d) is higher than current time.', $nbf)); + } + + return $nbf; + } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php index 995e440..dc3b2ad 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php @@ -27,7 +27,12 @@ public function __construct( */ public function jsonSerialize(): array { - return $this->getValue(); + return array_map( + fn( + VcCredentialSchemaClaimValue $vcCredentialSchemaClaimValue, + ): array => $vcCredentialSchemaClaimValue->jsonSerialize(), + $this->getValue(), + ); } public function getName(): string @@ -36,15 +41,10 @@ public function getName(): string } /** - * @return mixed[] + * @return \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSchemaClaimValue[] */ public function getValue(): array { - return array_map( - fn( - VcCredentialSchemaClaimValue $vcCredentialSchemaClaimValue, - ): array => $vcCredentialSchemaClaimValue->jsonSerialize(), - $this->vcCredentialSchemaClaimValueValues, - ); + return $this->vcCredentialSchemaClaimValueValues; } } diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php index f10bcbf..89389c1 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php @@ -27,7 +27,12 @@ public function __construct( */ public function jsonSerialize(): array { - return $this->getValue(); + return array_map( + fn( + VcCredentialSubjectClaimValue $vcCredentialSubjectClaimValue, + ): array => $vcCredentialSubjectClaimValue->jsonSerialize(), + $this->getValue(), + ); } public function getName(): string @@ -36,15 +41,10 @@ public function getName(): string } /** - * @return mixed[] + * @return \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimValue[] */ public function getValue(): array { - return array_map( - fn( - VcCredentialSubjectClaimValue $vcCredentialSubjectClaimValue, - ): array => $vcCredentialSubjectClaimValue->jsonSerialize(), - $this->vcCredentialSubjectClaimValueValues, - ); + return $this->vcCredentialSubjectClaimValueValues; } } diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php index fe75861..142e33e 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php @@ -27,7 +27,12 @@ public function __construct( */ public function jsonSerialize(): array { - return $this->getValue(); + return array_map( + fn( + VcEvidenceClaimValue $vcEvidenceClaimValue, + ): array => $vcEvidenceClaimValue->jsonSerialize(), + $this->getValue(), + ); } public function getName(): string @@ -36,15 +41,10 @@ public function getName(): string } /** - * @return mixed[] + * @return \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcEvidenceClaimValue[] */ public function getValue(): array { - return array_map( - fn( - VcEvidenceClaimValue $vcEvidenceClaimValue, - ): array => $vcEvidenceClaimValue->jsonSerialize(), - $this->vcEvidenceClaimValueValues, - ); + return $this->vcEvidenceClaimValueValues; } } diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php index 3e6c4f7..cf515af 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php @@ -27,7 +27,12 @@ public function __construct( */ public function jsonSerialize(): array { - return $this->getValue(); + return array_map( + fn( + VcRefreshServiceClaimValue $vcRefreshServiceClaimValue, + ): array => $vcRefreshServiceClaimValue->jsonSerialize(), + $this->getValue(), + ); } public function getName(): string @@ -36,15 +41,10 @@ public function getName(): string } /** - * @return mixed[] + * @return \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcRefreshServiceClaimValue[] */ public function getValue(): array { - return array_map( - fn( - VcRefreshServiceClaimValue $vcRefreshServiceClaimValue, - ): array => $vcRefreshServiceClaimValue->jsonSerialize(), - $this->vcRefreshServiceClaimValueValues, - ); + return $this->vcRefreshServiceClaimValueValues; } } diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php index 177d4e8..542754f 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php @@ -27,7 +27,12 @@ public function __construct( */ public function jsonSerialize(): array { - return $this->getValue(); + return array_map( + fn( + VcTermsOfUseClaimValue $vcTermsOfUseClaimValue, + ): array => $vcTermsOfUseClaimValue->jsonSerialize(), + $this->getValue(), + ); } public function getName(): string @@ -36,15 +41,10 @@ public function getName(): string } /** - * @return mixed[] + * @return \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcTermsOfUseClaimValue[] */ public function getValue(): array { - return array_map( - fn( - VcTermsOfUseClaimValue $vcTermsOfUseClaimValue, - ): array => $vcTermsOfUseClaimValue->jsonSerialize(), - $this->vcTermsOfUseClaimValueValues, - ); + return $this->vcTermsOfUseClaimValueValues; } } diff --git a/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactory.php index a6294d0..612e909 100644 --- a/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactory.php @@ -5,6 +5,8 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Factories; use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; +use SimpleSAML\OpenID\Codebooks\ClaimsEnum; +use SimpleSAML\OpenID\Codebooks\JwtTypesEnum; use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\JwtVcJson; @@ -35,8 +37,7 @@ public function fromData( array $payload, array $header, ): JwtVcJson { - // TODO mivanci Set type header -// $header[ClaimsEnum::Typ->value] = JwtTypesEnum::->value; + $header[ClaimsEnum::Typ->value] = JwtTypesEnum::Jwt->value; return new JwtVcJson( $this->jwsDecoratorBuilder->fromData( diff --git a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php index 7b562ef..e510023 100644 --- a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php @@ -102,7 +102,7 @@ public function buildTypeClaimValue(mixed $data): TypeClaimValue /** * @param non-empty-array $data */ - public function buildVcCredentialSubjectClaimValue(array $data): VcCredentialSubjectClaimValue + public function buildVcCredentialSubjectClaimValue(array $data, ?string $id = null): VcCredentialSubjectClaimValue { return new VcCredentialSubjectClaimValue($data); } @@ -110,17 +110,38 @@ public function buildVcCredentialSubjectClaimValue(array $data): VcCredentialSub /** * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException */ - public function buildVcCredentialSubjectClaimBag(array $data): VcCredentialSubjectClaimBag - { + public function buildVcCredentialSubjectClaimBag( + array $data, + ?string $subClaimValue = null, + ): VcCredentialSubjectClaimBag { if ($this->helpers->arr()->isAssociative($data)) { $data = [$data]; } $data = $this->helpers->type()->enforceNonEmptyArrayOfNonEmptyArrays($data); + if (is_string($subClaimValue) && (count($data) !== 1)) { + throw new VcDataModelException( + 'Refusing to set credentialSubject ID claim value for multiple subjects.', + ); + } + $vcCredentialSubjectClaimValueData = array_shift($data); + // If we have the 'sub' claim in JWT, we must use it as the credentialSubject ID value. However, we can't do + // that if we have more than one credentialSubject. + if (is_string($subClaimValue)) { + if ($data === []) { + $vcCredentialSubjectClaimValueData[ClaimsEnum::Id->value] = $subClaimValue; + } else { + throw new VcDataModelException( + 'Refusing to set credentialSubject ID claim value for multiple subjects.', + ); + } + } + $vcCredentialSubjectClaimValue = $this->buildVcCredentialSubjectClaimValue($vcCredentialSubjectClaimValueData); $vcCredentialSubjectClaimValues = array_map( diff --git a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php index 5c80cab..b8e9eed 100644 --- a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php +++ b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php @@ -5,6 +5,7 @@ namespace SimpleSAML\OpenID\VerifiableCredentials\VcDataModel; use DateTimeImmutable; +use Exception; use SimpleSAML\OpenID\Codebooks\ClaimsEnum; use SimpleSAML\OpenID\Codebooks\CredentialFormatIdentifiersEnum; use SimpleSAML\OpenID\Exceptions\VcDataModelException; @@ -121,7 +122,9 @@ public function getVcAtContext(): VcAtContextClaimValue /** * @return ?non-empty-string + * @throws \SimpleSAML\OpenID\Exceptions\ClientAssertionException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ public function getVcId(): ?string { @@ -129,6 +132,12 @@ public function getVcId(): ?string return null; } + $jti = $this->getJwtId(); + + if (is_string($jti)) { + return $this->vcId = $this->helpers->type()->enforceUri($jti); + } + $vcId = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Id->value); if (is_null($vcId)) { @@ -164,6 +173,7 @@ public function getVcType(): TypeClaimValue /** * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ public function getVcCredentialSubject(): VcCredentialSubjectClaimBag { @@ -182,12 +192,14 @@ public function getVcCredentialSubject(): VcCredentialSubjectClaimBag return $this->vcCredentialSubjectClaimBag = $this->claimFactory->forVcDataModel() ->buildVcCredentialSubjectClaimBag( $vcCredentialSubject, + $this->getSubject(), ); } /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ public function getVcIssuer(): VcIssuerClaimValue { @@ -195,6 +207,14 @@ public function getVcIssuer(): VcIssuerClaimValue return $this->vcIssuerClaimValue; } + $iss = $this->getIssuer(); + + if (is_string($iss)) { + return $this->vcIssuerClaimValue = $this->claimFactory->forVcDataModel()->buildVcIssuerClaimValue( + [ClaimsEnum::Id->value => $iss], + ); + } + $vcIssuer = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Issuer->value); if (is_null($vcIssuer)) { @@ -217,6 +237,8 @@ public function getVcIssuer(): VcIssuerClaimValue } /** + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException */ public function getVcIssuanceDate(): DateTimeImmutable @@ -225,6 +247,16 @@ public function getVcIssuanceDate(): DateTimeImmutable return $this->vcIssuanceDate; } + $nbf = $this->getNotBefore(); + + if (is_int($nbf)) { + try { + return $this->vcIssuanceDate = $this->helpers->dateTime()->fromTimestamp($nbf); + } catch (Exception $e) { + throw new VcDataModelException('Error parsing Not Before claim: ' . $e->getMessage()); + } + } + $issuanceDate = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Issuance_Date->value); if (!is_string($issuanceDate)) { @@ -232,8 +264,8 @@ public function getVcIssuanceDate(): DateTimeImmutable } try { - return $this->vcIssuanceDate = $this->helpers->dateTime()->parseXsDateTime($issuanceDate); - } catch (\Exception $exception) { + return $this->vcIssuanceDate = $this->helpers->dateTime()->fromXsDateTime($issuanceDate); + } catch (Exception $exception) { throw new VcDataModelException('Error parsing VC Issuance Date claim: ' . $exception->getMessage()); } } @@ -269,6 +301,8 @@ public function getVcProof(): ?VcProofClaimValue } /** + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException */ public function getVcExpirationDate(): ?DateTimeImmutable @@ -281,6 +315,18 @@ public function getVcExpirationDate(): ?DateTimeImmutable return $this->vcExpirationDate; } + // Try to get it from the exp claim. + $exp = $this->getExpirationTime(); + + if (is_int($exp)) { + try { + return $this->vcExpirationDate = $this->helpers->dateTime()->fromTimestamp($exp); + } catch (Exception $e) { + throw new VcDataModelException('Error parsing Expiration Time date claim: ' . $e->getMessage()); + } + } + + // Try to get it from the vc claim. $expirationDate = $this->getNestedPayloadClaim(ClaimsEnum::Vc->value, ClaimsEnum::Expiration_Date->value); if (is_null($expirationDate)) { @@ -290,8 +336,8 @@ public function getVcExpirationDate(): ?DateTimeImmutable if (is_string($expirationDate)) { try { - return $this->vcExpirationDate = $this->helpers->dateTime()->parseXsDateTime($expirationDate); - } catch (\Exception $exception) { + return $this->vcExpirationDate = $this->helpers->dateTime()->fromXsDateTime($expirationDate); + } catch (Exception $exception) { throw new VcDataModelException('Error parsing VC Expiration Date claim: ' . $exception->getMessage()); } } @@ -474,6 +520,12 @@ protected function validate(): void $this->getVcRefreshService(...), $this->getVcTermsOfUse(...), $this->getVcEvidence(...), + $this->getExpirationTime(...), + $this->getIssuer(...), + $this->getNotBefore(...), + $this->getJwtId(...), + $this->getSubject(...), + $this->getAudience(...), ); } } diff --git a/tests/src/Federation/Factories/EntityStatementFactoryTest.php b/tests/src/Federation/Factories/EntityStatementFactoryTest.php index 3a24be8..f0c9d50 100644 --- a/tests/src/Federation/Factories/EntityStatementFactoryTest.php +++ b/tests/src/Federation/Factories/EntityStatementFactoryTest.php @@ -31,8 +31,6 @@ final class EntityStatementFactoryTest extends TestCase { protected MockObject $signatureMock; - - protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; diff --git a/tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php b/tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php new file mode 100644 index 0000000..345ebb0 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php @@ -0,0 +1,164 @@ + 'RS256', + 'typ' => 'JWT', + 'kid' => 'did:example:abfe13f712120431c276e12ecab#keys-1', + ]; + + // https://www.w3.org/TR/vc-data-model/#example-jwt-payload-of-a-jwt-based-verifiable-credential-using-jws-as-a-proof-non-normative + protected array $expiredPayload = [ + 'sub' => 'did:example:ebfeb1f712ebc6f1c276e12ec21', + 'jti' => 'http://example.edu/credentials/3732', + 'iss' => 'https://example.com/keys/foo.jwk', + 'nbf' => 1541493724, + 'iat' => 1541493724, + 'exp' => 1573029723, + 'nonce' => '660!6345FSer', + 'vc' => [ + '@context' => [ + 'https://www.w3.org/2018/credentials/v1', + 'https://www.w3.org/2018/credentials/examples/v1', + ], + 'type' => ['VerifiableCredential', 'UniversityDegreeCredential'], + 'credentialSubject' => [ + 'degree' => [ + 'type' => 'BachelorDegree', + 'name' => 'Bachelor of Science and Arts', + ], + ], + ], + ]; + + protected array $validPayload; + + protected function setUp(): void + { + $this->signatureMock = $this->createMock(Signature::class); + + $jwsMock = $this->createMock(JWS::class); + $jwsMock->method('getPayload') + ->willReturn('json-payload-string'); // Just so we have non-empty value. + $jwsMock->method('getSignature')->willReturn($this->signatureMock); + + $jwsDecoratorMock = $this->createMock(JwsDecorator::class); + $jwsDecoratorMock->method('jws')->willReturn($jwsMock); + + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + + $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); + $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); + $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); + + $this->helpersMock = $this->createMock(Helpers::class); + $this->jsonHelperMock = $this->createMock(Helpers\Json::class); + $this->helpersMock->method('json')->willReturn($this->jsonHelperMock); + $typeHelperMock = $this->createMock(Helpers\Type::class); + $this->helpersMock->method('type')->willReturn($typeHelperMock); + + $this->helpersMock->method('arr')->willReturn(new Helpers\Arr()); + + $typeHelperMock->method('ensureNonEmptyString')->willReturnArgument(0); + $typeHelperMock->method('ensureInt')->willReturnArgument(0); + + $this->claimFactoryMock = $this->createMock(ClaimFactory::class); + + $this->validPayload = $this->expiredPayload; + $this->validPayload['exp'] = time() + 3600; + } + + protected function sut( + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, + ?JwsVerifierDecorator $jwsVerifierDecorator = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, + ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, + ?DateIntervalDecorator $dateIntervalDecorator = null, + ?Helpers $helpers = null, + ?ClaimFactory $claimFactory = null, + ): JwtVcJsonFactory { + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; + $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; + $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; + $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; + $helpers ??= $this->helpersMock; + $claimFactory ??= $this->claimFactoryMock; + + return new JwtVcJsonFactory( + $jwsDecoratorBuilder, + $jwsVerifierDecorator, + $jwksDecoratorFactory, + $jwsSerializerManagerDecorator, + $dateIntervalDecorator, + $helpers, + $claimFactory, + ); + } + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(JwtVcJsonFactory::class, $this->sut()); + } + + public function testCanBuildFromToken(): void + { + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + + $this->assertInstanceOf( + JwtVcJson::class, + $this->sut()->fromToken('token'), + ); + } +} From 1c101aebec56037a16a16e50fff012f055c812a5 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Thu, 1 May 2025 14:59:36 +0200 Subject: [PATCH 15/66] Add ClaimsPathPointerResolver --- src/Codebooks/WellKnownEnum.php | 2 + src/Exceptions/ClaimsPathPointerException.php | 11 ++ .../ClaimsPathPointerResolver.php | 96 +++++++++++++ .../src/Federation/TrustChainResolverTest.php | 14 +- .../ClaimsPathPointerResolverTest.php | 136 ++++++++++++++++++ 5 files changed, 252 insertions(+), 7 deletions(-) create mode 100644 src/Exceptions/ClaimsPathPointerException.php create mode 100644 src/VerifiableCredentials/ClaimsPathPointerResolver.php create mode 100644 tests/src/VerifiableCredentials/ClaimsPathPointerResolverTest.php diff --git a/src/Codebooks/WellKnownEnum.php b/src/Codebooks/WellKnownEnum.php index f41e758..13f6373 100644 --- a/src/Codebooks/WellKnownEnum.php +++ b/src/Codebooks/WellKnownEnum.php @@ -7,7 +7,9 @@ enum WellKnownEnum: string { case Prefix = '.well-known'; + case OAuthAuthorizationServer = 'oauth-authorization-server'; case OpenIdFederation = 'openid-federation'; + case OpenIdCredentialIssuer = 'openid-credential-issuer'; public function path(): string { diff --git a/src/Exceptions/ClaimsPathPointerException.php b/src/Exceptions/ClaimsPathPointerException.php new file mode 100644 index 0000000..a509913 --- /dev/null +++ b/src/Exceptions/ClaimsPathPointerException.php @@ -0,0 +1,11 @@ + $path + * @return mixed[] + * @throws \SimpleSAML\OpenID\Exceptions\ClaimsPathPointerException + */ + public function forJsonBased(array $data, array $path): array + { + // Start with the root element as the only “selected” element + $selected = [$data]; + + foreach ($path as $pathComponent) { + $nextSelected = []; + + // Process each currently selected element + foreach ($selected as $selectedElement) { + if (is_string($pathComponent)) { + // String -> object key + if ( + (!is_array($selectedElement)) || + (!$this->helpers->arr()->isAssociative($selectedElement)) + ) { + throw new ClaimsPathPointerException('Expected object for string path component.'); + } + + if (array_key_exists($pathComponent, $selectedElement)) { + $nextSelected[] = $selectedElement[$pathComponent]; + } + + // else: drop this element + } elseif (is_null($pathComponent)) { + // Null -> all elements of an array + if ( + (!is_array($selectedElement)) || + $this->helpers->arr()->isAssociative($selectedElement) + ) { + throw new ClaimsPathPointerException('Expected array for null path component.'); + } + + foreach ($selectedElement as $item) { + $nextSelected[] = $item; + } + } elseif (is_int($pathComponent) && $pathComponent >= 0) { + // Integer → array index + if ( + (!is_array($selectedElement)) || + $this->helpers->arr()->isAssociative($selectedElement) + ) { + throw new ClaimsPathPointerException('Expected array for integer path component.'); + } + + if (array_key_exists($pathComponent, $selectedElement)) { + $nextSelected[] = $selectedElement[$pathComponent]; + } + + // else: drop this element + } else { + throw new ClaimsPathPointerException( + 'Path component must be string, null, or non-negative integer.', + ); + } + } + + // If nothing remains, error out + if ($nextSelected === []) { + throw new ClaimsPathPointerException('No elements selected at path component'); + } + + $selected = $nextSelected; + } + + // The final result is the set of selected JSON elements. + return $selected; + } +} diff --git a/tests/src/Federation/TrustChainResolverTest.php b/tests/src/Federation/TrustChainResolverTest.php index 3870876..6bf4e14 100644 --- a/tests/src/Federation/TrustChainResolverTest.php +++ b/tests/src/Federation/TrustChainResolverTest.php @@ -172,7 +172,7 @@ public function testCanDetectLoopInConfigurationChains(): void { $this->entityStatementFetcherMock ->method('fromCacheOrWellKnownEndpoint') - ->willReturnCallback(fn(string $entityId) => + ->willReturnCallback(fn(string $entityId): \SimpleSAML\OpenID\Federation\EntityStatement => $this->configChainSample[$entityId] ?? throw new \Exception('No entity.')); $this->leafEntityConfigurationMock @@ -201,7 +201,7 @@ public function testCanBailOnMaxAuthorityHintsRule(): void $this->entityStatementFetcherMock ->method('fromCacheOrWellKnownEndpoint') - ->willReturnCallback(fn(string $entityId) => + ->willReturnCallback(fn(string $entityId): \SimpleSAML\OpenID\Federation\EntityStatement => $this->configChainSample[$entityId] ?? throw new \Exception('No entity.')); $this->loggerMock @@ -220,7 +220,7 @@ public function testCanResolveTrustChain(): void { $this->entityStatementFetcherMock ->method('fromCacheOrWellKnownEndpoint') - ->willReturnCallback(fn(string $entityId) => + ->willReturnCallback(fn(string $entityId): \SimpleSAML\OpenID\Federation\EntityStatement => $this->configChainSample[$entityId] ?? throw new \Exception('No entity.')); $this->leafEntityConfigurationMock @@ -245,7 +245,7 @@ public function testCanResolveMultipleTrustChains(): void { $this->entityStatementFetcherMock ->method('fromCacheOrWellKnownEndpoint') - ->willReturnCallback(fn(string $entityId) => + ->willReturnCallback(fn(string $entityId): \SimpleSAML\OpenID\Federation\EntityStatement => $this->configChainSample[$entityId] ?? throw new \Exception('No entity.')); $this->leafEntityConfigurationMock @@ -263,7 +263,7 @@ public function testCanResolveTrustChainForTrustAnchorOnly(): void { $this->entityStatementFetcherMock ->method('fromCacheOrWellKnownEndpoint') - ->willReturnCallback(fn(string $entityId) => + ->willReturnCallback(fn(string $entityId): \SimpleSAML\OpenID\Federation\EntityStatement => $this->configChainSample[$entityId] ?? throw new \Exception('No entity.')); $this->trustChainFactoryMock->expects($this->once())->method('forTrustAnchor'); @@ -320,7 +320,7 @@ public function testCanWarnOnTrustChainResolutionSubordinateStatementFetchError( { $this->entityStatementFetcherMock ->method('fromCacheOrWellKnownEndpoint') - ->willReturnCallback(fn(string $entityId) => + ->willReturnCallback(fn(string $entityId): \SimpleSAML\OpenID\Federation\EntityStatement => $this->configChainSample[$entityId] ?? throw new \Exception('No entity.')); $this->entityStatementFetcherMock @@ -349,7 +349,7 @@ public function testTrustChainResolveThrowsOnTrustChainBagFactoryError(): void { $this->entityStatementFetcherMock ->method('fromCacheOrWellKnownEndpoint') - ->willReturnCallback(fn(string $entityId) => + ->willReturnCallback(fn(string $entityId): \SimpleSAML\OpenID\Federation\EntityStatement => $this->configChainSample[$entityId] ?? throw new \Exception('No entity.')); $this->leafEntityConfigurationMock diff --git a/tests/src/VerifiableCredentials/ClaimsPathPointerResolverTest.php b/tests/src/VerifiableCredentials/ClaimsPathPointerResolverTest.php new file mode 100644 index 0000000..30df2a3 --- /dev/null +++ b/tests/src/VerifiableCredentials/ClaimsPathPointerResolverTest.php @@ -0,0 +1,136 @@ + 'Arthur Dent', + 'address' => [ + 'street_address' => '42 Market Street', + 'locality' => 'Milliways', + 'postal_code' => '12345', + ], + 'degrees' => [ + [ + 'type' => 'Bachelor of Science', + 'university' => 'University of Betelgeuse', + ], + [ + 'type' => 'Master of Science', + 'university' => 'University of Betelgeuse', + ], + ], + 'nationalities' => ['British', 'Betelgeusian'], + ]; + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(ClaimsPathPointerResolver::class, $this->sut()); + } + + protected function sut( + ?Helpers $helpers = null, + ): ClaimsPathPointerResolver { + $helpers ??= $this->helpersMock; + + return new ClaimsPathPointerResolver($helpers); + } + + protected function setUp(): void + { + $this->helpersMock = $this->createMock(Helpers::class); + $this->helpersMock->method('arr')->willReturn(new Helpers\Arr()); + } + + public function testCanResolveForJsonBased(): void + { + $sut = $this->sut(); + + $this->assertSame( + ['Arthur Dent'], + $sut->forJsonBased($this->jsonDataSample, ['name']), + ); + + $this->assertSame( + [ + [ + 'street_address' => '42 Market Street', + 'locality' => 'Milliways', + 'postal_code' => '12345', + ], + ], + $sut->forJsonBased($this->jsonDataSample, ['address']), + ); + + $this->assertSame( + ['42 Market Street'], + $sut->forJsonBased($this->jsonDataSample, ['address', 'street_address']), + ); + + $this->assertSame( + ['Bachelor of Science', 'Master of Science'], + $sut->forJsonBased($this->jsonDataSample, ['degrees', null, 'type']), + ); + + $this->assertSame( + ['Betelgeusian'], + $sut->forJsonBased($this->jsonDataSample, ['nationalities', 1]), + ); + } + + public function testThrowsForInvalidPathComponent(): void + { + $this->expectException(ClaimsPathPointerException::class); + $this->expectExceptionMessage('Path component'); + + $this->sut()->forJsonBased($this->jsonDataSample, [false]); + } + + public function testThrowsForNonObjectInStringPathComponent(): void + { + $this->expectException(ClaimsPathPointerException::class); + $this->expectExceptionMessage('Expected object'); + + $this->sut()->forJsonBased($this->jsonDataSample, ['nationalities', 'invalid']); + } + + public function testThrowsForNonArrayInNullPathComponent(): void + { + $this->expectException(ClaimsPathPointerException::class); + $this->expectExceptionMessage('Expected array'); + + $this->sut()->forJsonBased($this->jsonDataSample, ['address', null]); + } + + public function testThrowsForNonArrayInIntegerPathComponent(): void + { + $this->expectException(ClaimsPathPointerException::class); + $this->expectExceptionMessage('Expected array'); + + $this->sut()->forJsonBased($this->jsonDataSample, ['address', 0]); + } + + public function testThrowsForEmptySelection(): void + { + $this->expectException(ClaimsPathPointerException::class); + $this->expectExceptionMessage('No elements'); + + $this->sut()->forJsonBased($this->jsonDataSample, ['invalid']); + } +} From 5e13306b3a55568530c0ad2b5424a969af87df07 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Thu, 1 May 2025 15:33:02 +0200 Subject: [PATCH 16/66] Add LanguageTagsEnum --- src/Codebooks/LanguageTagsEnum.php | 144 +++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/Codebooks/LanguageTagsEnum.php diff --git a/src/Codebooks/LanguageTagsEnum.php b/src/Codebooks/LanguageTagsEnum.php new file mode 100644 index 0000000..6b7a8a3 --- /dev/null +++ b/src/Codebooks/LanguageTagsEnum.php @@ -0,0 +1,144 @@ + Date: Tue, 6 May 2025 13:33:19 +0200 Subject: [PATCH 17/66] Start with VerifiableCredentials manager --- src/Codebooks/CredentialTypesEnum.php | 10 ++ src/VerifiableCredentials.php | 155 ++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 src/Codebooks/CredentialTypesEnum.php create mode 100644 src/VerifiableCredentials.php diff --git a/src/Codebooks/CredentialTypesEnum.php b/src/Codebooks/CredentialTypesEnum.php new file mode 100644 index 0000000..742234e --- /dev/null +++ b/src/Codebooks/CredentialTypesEnum.php @@ -0,0 +1,10 @@ +timestampValidationLeewayDecorator = $this->dateIntervalDecoratorFactory() + ->build($timestampValidationLeeway); + } + + public function dateIntervalDecoratorFactory(): DateIntervalDecoratorFactory + { + return $this->dateIntervalDecoratorFactory ??= new DateIntervalDecoratorFactory(); + } + + public function helpers(): Helpers + { + return $this->helpers ??= new Helpers(); + } + + public function claimsPathPointerResolver(): ClaimsPathPointerResolver + { + return $this->claimsPathPointerResolver ??= new ClaimsPathPointerResolver( + $this->helpers(), + ); + } + + public function jwsDecoratorBuilderFactory(): JwsDecoratorBuilderFactory + { + return $this->jwsDecoratorBuilderFactory ??= new JwsDecoratorBuilderFactory(); + } + + public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDecoratorFactory + { + return $this->jwsSerializerManagerDecoratorFactory ??= new JwsSerializerManagerDecoratorFactory(); + } + + public function jwsSerializerManagerDecorator(): JwsSerializerManagerDecorator + { + return $this->jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorFactory() + ->build($this->supportedSerializers); + } + + public function algorithmManagerDecoratorFactory(): AlgorithmManagerDecoratorFactory + { + return $this->algorithmManagerDecoratorFactory ??= new AlgorithmManagerDecoratorFactory(); + } + + public function algorithmManagerDecorator(): AlgorithmManagerDecorator + { + return $this->algorithmManagerDecorator ??= $this->algorithmManagerDecoratorFactory() + ->build($this->supportedAlgorithms); + } + + public function jwsDecoratorBuilder(): JwsDecoratorBuilder + { + return $this->jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderFactory()->build( + $this->jwsSerializerManagerDecorator(), + $this->algorithmManagerDecorator(), + $this->helpers(), + ); + } + + public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory + { + return $this->jwsVerifierDecoratorFactory ??= new JwsVerifierDecoratorFactory(); + } + + public function jwsVerifierDecorator(): JwsVerifierDecorator + { + return $this->jwsVerifierDecorator ??= $this->jwsVerifierDecoratorFactory()->build( + $this->algorithmManagerDecorator(), + ); + } + + public function jwksDecoratorFactory(): JwksDecoratorFactory + { + return $this->jwksDecoratorFactory ??= new JwksDecoratorFactory(); + } + + public function claimFactory(): ClaimFactory + { + return $this->claimFactory ??= new ClaimFactory( + $this->helpers(), + ); + } + + public function jwtVcJsonFactory(): JwtVcJsonFactory + { + return $this->jwtVcJsonFactory ??= new JwtVcJsonFactory( + $this->jwsDecoratorBuilder(), + $this->jwsVerifierDecorator(), + $this->jwksDecoratorFactory(), + $this->jwsSerializerManagerDecorator(), + $this->timestampValidationLeewayDecorator, + $this->helpers(), + $this->claimFactory(), + ); + } +} From 420bd4ef9626183e21dc0dc8d518cd7025fda9ca Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Fri, 9 May 2025 18:06:44 +0200 Subject: [PATCH 18/66] WIP --- src/Codebooks/ClaimsEnum.php | 9 ++ src/Exceptions/CredentialOfferException.php | 11 +++ src/VerifiableCredentials.php | 10 ++ src/VerifiableCredentials/CredentialOffer.php | 16 ++++ .../CredentialOfferGrantsBag.php | 32 +++++++ .../CredentialOfferGrantsValue.php | 95 +++++++++++++++++++ .../CredentialOfferParameters.php | 38 ++++++++ .../Factories/CredentialOfferFactory.php | 85 +++++++++++++++++ 8 files changed, 296 insertions(+) create mode 100644 src/Exceptions/CredentialOfferException.php create mode 100644 src/VerifiableCredentials/CredentialOffer.php create mode 100644 src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsBag.php create mode 100644 src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValue.php create mode 100644 src/VerifiableCredentials/CredentialOffer/CredentialOfferParameters.php create mode 100644 src/VerifiableCredentials/Factories/CredentialOfferFactory.php diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index b4bb407..3445203 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -22,6 +22,7 @@ enum ClaimsEnum: string case Aud = 'aud'; case AuthorityHints = 'authority_hints'; case AuthorizationEndpoint = 'authorization_endpoint'; + case AuthorizationServer = 'authorization_server'; case AuthorizationServers = 'authorization_servers'; case BackChannelLogoutSessionSupported = 'backchannel_logout_session_supported'; case BackChannelLogoutSupported = 'backchannel_logout_supported'; @@ -39,6 +40,7 @@ enum ClaimsEnum: string case ClientRegistrationTypesSupported = 'client_registration_types_supported'; case CodeChallengeMethodsSupported = 'code_challenge_methods_supported'; case Contacts = 'contacts'; + case CredentialConfigurationIds = 'credential_configuration_ids'; case CredentialConfigurationsSupported = 'credential_configurations_supported'; case CredentialDefinition = 'credential_definition'; case CredentialEndpoint = 'credential_endpoint'; @@ -66,6 +68,7 @@ enum ClaimsEnum: string case FederationListEndpoint = 'federation_list_endpoint'; case FederationTrustMarkEndpoint = 'federation_trust_mark_endpoint'; case Format = 'format'; + case Grants = 'grants'; case GrantTypes = 'grant_types'; case GrantTypesSupported = 'grant_types_supported'; case HomepageUri = 'homepage_uri'; @@ -73,6 +76,7 @@ enum ClaimsEnum: string case Iat = 'iat'; // Identifier case Id = 'id'; + case InputMode = 'input_mode'; case IdTokenSigningAlgValuesSupported = 'id_token_signing_alg_values_supported'; case IntrospectionEndpoint = 'introspection_endpoint'; case IntrospectionEndpointAuthMethodsSupported = 'introspection_endpoint_auth_methods_supported'; @@ -82,6 +86,7 @@ enum ClaimsEnum: string case Iss = 'iss'; case Issuance_Date = 'issuanceDate'; case Issuer = 'issuer'; + case IssuerState = 'issuer_state'; // JWT ID case Jti = 'jti'; // JsonWebKeySet @@ -92,6 +97,7 @@ enum ClaimsEnum: string case KeyAttestationsRequired = 'key_attestations_required'; case KeyStorage = 'key_storage'; case Keys = 'keys'; + case Length = 'length'; case Locale = 'locale'; case Logo = 'logo'; case LogoUri = 'logo_uri'; @@ -114,6 +120,7 @@ enum ClaimsEnum: string case Path = 'path'; case PolicyUri = 'policy_uri'; case PostLogoutRedirectUris = 'post_logout_redirect_uris'; + case PreAuthorizedCode = 'pre-authorized_code'; case PreAuthorizedGrantAnonymousAccessSupported = 'pre-authorized_grant_anonymous_access_supported'; case Proof = 'proof'; // ProofSigningAlgorithmValuesSupported @@ -158,6 +165,8 @@ enum ClaimsEnum: string case TrustMarkId = 'trust_mark_id'; case TrustMarkOwners = 'trust_mark_owners'; case TrustMarks = 'trust_marks'; + // TransactionCode + case TxCode = 'tx_code'; // UserInterfaceLocalesSupported case UiLocalesSupported = 'ui_locales_supported'; case Uri = 'uri'; diff --git a/src/Exceptions/CredentialOfferException.php b/src/Exceptions/CredentialOfferException.php new file mode 100644 index 0000000..c130344 --- /dev/null +++ b/src/Exceptions/CredentialOfferException.php @@ -0,0 +1,11 @@ +claimFactory(), ); } + + public function credentialOfferFactory(): CredentialOfferFactory + { + return $this->credentialOfferFactory ??= new CredentialOfferFactory( + $this->helpers(), + ); + } } diff --git a/src/VerifiableCredentials/CredentialOffer.php b/src/VerifiableCredentials/CredentialOffer.php new file mode 100644 index 0000000..d04a260 --- /dev/null +++ b/src/VerifiableCredentials/CredentialOffer.php @@ -0,0 +1,16 @@ +credentialOfferGrantsValues = $credentialOfferGrantsValues; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + $value = []; + + foreach ($this->credentialOfferGrantsValues as $credentialOfferGrantsValue) { + $value = array_merge($value, $credentialOfferGrantsValue->jsonSerialize()); + } + + return $value; + } +} diff --git a/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValue.php b/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValue.php new file mode 100644 index 0000000..5aff2e1 --- /dev/null +++ b/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValue.php @@ -0,0 +1,95 @@ +type === GrantTypesEnum::AuthorizationCode->value) { + if ( + isset($this->claims[ClaimsEnum::IssuerState->value]) && + !is_string($this->claims[ClaimsEnum::IssuerState->value]) + ) { + throw new CredentialOfferException('Invalid Issuer State claim.'); + } + + if ( + isset($this->claims[ClaimsEnum::AuthorizationServer->value]) && + !is_string($this->claims[ClaimsEnum::AuthorizationServer->value]) + ) { + throw new CredentialOfferException('Invalid Authorization Server claim.'); + } + } + + if ($this->type === GrantTypesEnum::PreAuthorizedCode->value) { + if ( + !isset($this->claims[ClaimsEnum::PreAuthorizedCode->value]) || + !is_string($this->claims[ClaimsEnum::PreAuthorizedCode->value]) + ) { + throw new CredentialOfferException('Invalid Pre-authorized Code claim.'); + } + + if (isset($this->claims[ClaimsEnum::TxCode->value])) { + if (!is_array($this->claims[ClaimsEnum::TxCode->value])) { + throw new CredentialOfferException('Invalid TxCode claim.'); + } + + if ( + isset($this->claims[ClaimsEnum::TxCode->value][ClaimsEnum::InputMode->value]) && + !in_array( + $this->claims[ClaimsEnum::TxCode->value][ClaimsEnum::InputMode->value], + ['numeric', 'text'], + ) + ) { + throw new CredentialOfferException('Invalid Transaction Code Input Mode claim.'); + } + + if ( + isset($this->claims[ClaimsEnum::TxCode->value][ClaimsEnum::Length->value]) && + !is_int($this->claims[ClaimsEnum::TxCode->value][ClaimsEnum::Length->value]) + ) { + throw new CredentialOfferException('Invalid Transaction Code Length claim.'); + } + + if ( + isset($this->claims[ClaimsEnum::TxCode->value][ClaimsEnum::Description->value]) && + !is_string($this->claims[ClaimsEnum::TxCode->value][ClaimsEnum::Description->value]) + ) { + throw new CredentialOfferException('Invalid Transaction Code Description claim.'); + } + + if ( + isset($this->claims[ClaimsEnum::AuthorizationServer->value]) && + !is_string($this->claims[ClaimsEnum::AuthorizationServer->value]) + ) { + throw new CredentialOfferException('Invalid Authorization Server claim.'); + } + } + } + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + $this->type => $this->claims, + ]; + } +} diff --git a/src/VerifiableCredentials/CredentialOffer/CredentialOfferParameters.php b/src/VerifiableCredentials/CredentialOffer/CredentialOfferParameters.php new file mode 100644 index 0000000..3855daa --- /dev/null +++ b/src/VerifiableCredentials/CredentialOffer/CredentialOfferParameters.php @@ -0,0 +1,38 @@ + + */ + public function jsonSerialize(): array + { + $value = [ + ClaimsEnum::CredentialIssuer->value => $this->credentialIssuer, + ClaimsEnum::CredentialConfigurationIds->value => $this->credentialConfigurationIds, + ]; + + if ($this->credentialOfferGrantsBag instanceof CredentialOfferGrantsBag) { + $value[ClaimsEnum::Grants->value] = $this->credentialOfferGrantsBag->jsonSerialize(); + } + + return $value; + } +} diff --git a/src/VerifiableCredentials/Factories/CredentialOfferFactory.php b/src/VerifiableCredentials/Factories/CredentialOfferFactory.php new file mode 100644 index 0000000..02fc673 --- /dev/null +++ b/src/VerifiableCredentials/Factories/CredentialOfferFactory.php @@ -0,0 +1,85 @@ +value] ?? null; + $credentialIssuer = $this->helpers->type()->ensureNonEmptyString( + $credentialIssuer, + ClaimsEnum::CredentialIssuer->value, + ); + + $credentialConfigurationIds = $parameters[ClaimsEnum::CredentialConfigurationIds->value] ?? null; + $credentialConfigurationIds = $this->helpers->type()->enforceNonEmptyArrayWithValuesAsNonEmptyStrings( + $this->helpers->type()->ensureArray($credentialConfigurationIds), + ClaimsEnum::CredentialConfigurationIds->value, + ); + + $grants = $parameters[ClaimsEnum::Grants->value] ?? null; + $credentialOfferGrantsBag = null; + + if (is_array($grants)) { + $credentialOfferGrantsValues = []; + + foreach ($grants as $type => $claims) { + if (!is_string($type) || !is_array($claims)) { + throw new CredentialOfferException('Invalid Grants claim.'); + } + + $credentialOfferGrantsValues[] = new CredentialOfferGrantsValue($type, $claims); + } + + $credentialOfferGrantsBag = new CredentialOfferGrantsBag(...$credentialOfferGrantsValues); + } + + return new CredentialOffer( + credentialOfferParameters: new CredentialOfferParameters( + credentialIssuer: $credentialIssuer, + credentialConfigurationIds: $credentialConfigurationIds, + credentialOfferGrantsBag: $credentialOfferGrantsBag, + ), + ); + } + + if ($uri !== null) { + return new CredentialOffer( + uri: $uri, + ); + } + + throw new CredentialOfferException('Invalid parameters or uri.'); + } +} From 70b7f108bf0558ef5396f1162ea24c7c0202ce62 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Fri, 9 May 2025 18:32:22 +0200 Subject: [PATCH 19/66] WIP --- src/VerifiableCredentials/CredentialOffer.php | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/VerifiableCredentials/CredentialOffer.php b/src/VerifiableCredentials/CredentialOffer.php index d04a260..cfc62fe 100644 --- a/src/VerifiableCredentials/CredentialOffer.php +++ b/src/VerifiableCredentials/CredentialOffer.php @@ -4,13 +4,31 @@ namespace SimpleSAML\OpenID\VerifiableCredentials; +use SimpleSAML\OpenID\Exceptions\CredentialOfferException; use SimpleSAML\OpenID\VerifiableCredentials\CredentialOffer\CredentialOfferParameters; -class CredentialOffer +class CredentialOffer implements \JsonSerializable { public function __construct( protected readonly ?CredentialOfferParameters $credentialOfferParameters = null, protected readonly ?string $uri = null, ) { } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\CredentialOfferException + * @return string|mixed[] + */ + public function jsonSerialize(): string|array + { + if ($this->credentialOfferParameters instanceof CredentialOfferParameters) { + return $this->credentialOfferParameters->jsonSerialize(); + } + + if ($this->uri !== null) { + return $this->uri; + } + + throw new CredentialOfferException('Invalid parameters or uri.'); + } } From 4f48a3f15a0fac7bba72808c0f8e1f7d4fe13f8c Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Tue, 13 May 2025 10:33:55 +0200 Subject: [PATCH 20/66] WIP --- src/Codebooks/ParamsEnum.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Codebooks/ParamsEnum.php b/src/Codebooks/ParamsEnum.php index 9f96f48..9a99e69 100644 --- a/src/Codebooks/ParamsEnum.php +++ b/src/Codebooks/ParamsEnum.php @@ -27,6 +27,7 @@ enum ParamsEnum: string case MaxAge = 'max_age'; case Nonce = 'nonce'; case PostLogoutRedirectUri = 'post_logout_redirect_uri'; + case PreAuthorizedCode = 'pre-authorized_code'; case Prompt = 'prompt'; case RedirectUri = 'redirect_uri'; case Request = 'request'; @@ -36,5 +37,7 @@ enum ParamsEnum: string case State = 'state'; case TrustMarked = 'trust_marked'; case TrustMarkId = 'trust_mark_id'; + // TransactionCode + case TxCode = 'tx_code'; case UiLocales = 'ui_locales'; } From f696bf93460d0a7cf9cdbda1d66591fd0bd034f8 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Tue, 13 May 2025 11:58:49 +0200 Subject: [PATCH 21/66] WIP --- src/Codebooks/ParamsEnum.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Codebooks/ParamsEnum.php b/src/Codebooks/ParamsEnum.php index 9a99e69..41bf759 100644 --- a/src/Codebooks/ParamsEnum.php +++ b/src/Codebooks/ParamsEnum.php @@ -8,6 +8,7 @@ enum ParamsEnum: string { case AcrValues = 'acr_values'; case Assertion = 'assertion'; + case AuthorizationDetails = 'authorization_details'; case Claims = 'claims'; case ClientAssertion = 'client_assertion'; case ClientAssertionType = 'client_assertion_type'; From 561b297963ed04f32d363b34191f009dd65ae288 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Thu, 15 May 2025 13:55:41 +0200 Subject: [PATCH 22/66] WIP --- src/VerifiableCredentials/VcDataModel/JwtVcJson.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php index b8e9eed..b10ddaa 100644 --- a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php +++ b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php @@ -145,7 +145,8 @@ public function getVcId(): ?string return null; } - return $this->vcId = $this->helpers->type()->enforceUri($vcId); +// return $this->vcId = $this->helpers->type()->enforceUri($vcId); + return $this->vcId = $this->helpers->type()->ensureNonEmptyString($vcId); } /** From 2402df0fe5243fbd389a824cecf307b155fe0049 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Thu, 15 May 2025 14:03:29 +0200 Subject: [PATCH 23/66] WIP --- src/Jws/ParsedJws.php | 2 +- .../VcDataModel/Factories/VcDataModelClaimFactory.php | 3 ++- src/VerifiableCredentials/VcDataModel/JwtVcJson.php | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Jws/ParsedJws.php b/src/Jws/ParsedJws.php index 4bcebce..19bdfdf 100644 --- a/src/Jws/ParsedJws.php +++ b/src/Jws/ParsedJws.php @@ -216,7 +216,7 @@ public function getAudience(): ?array * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @return ?non-empty-string */ - public function getJwtId(): ?string + public function getJwtId(): ?string { $claimKey = ClaimsEnum::Jti->value; diff --git a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php index e510023..98a7732 100644 --- a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php @@ -166,7 +166,8 @@ public function buildVcIssuerClaimValue(array $data): VcIssuerClaimValue 'No ID claim value available.', ); - $id = $this->helpers->type()->enforceUri($id); +// $id = $this->helpers->type()->enforceUri($id); + $id = $this->helpers->type()->ensureNonEmptyString($id); return new VcIssuerClaimValue($id, $data); } diff --git a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php index b10ddaa..b8e9eed 100644 --- a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php +++ b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php @@ -145,8 +145,7 @@ public function getVcId(): ?string return null; } -// return $this->vcId = $this->helpers->type()->enforceUri($vcId); - return $this->vcId = $this->helpers->type()->ensureNonEmptyString($vcId); + return $this->vcId = $this->helpers->type()->enforceUri($vcId); } /** From 9946a95d0290c4dbd0a02ad0969760de1a3bc63d Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Thu, 15 May 2025 14:14:54 +0200 Subject: [PATCH 24/66] WIP --- src/Helpers/Type.php | 2 +- .../VcDataModel/Factories/VcDataModelClaimFactory.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Helpers/Type.php b/src/Helpers/Type.php index 02a4302..028e362 100644 --- a/src/Helpers/Type.php +++ b/src/Helpers/Type.php @@ -247,7 +247,7 @@ public function enforceRegex( public function enforceUri( mixed $value, ?string $context = null, - string $pattern = '/^[^:]+:\/\/?([^\s\/$.?#].[^\s]*)?$/', + string $pattern = '/^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/', ): string { try { $value = $this->enforceRegex($value, $pattern, $context); diff --git a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php index 98a7732..e510023 100644 --- a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php @@ -166,8 +166,7 @@ public function buildVcIssuerClaimValue(array $data): VcIssuerClaimValue 'No ID claim value available.', ); -// $id = $this->helpers->type()->enforceUri($id); - $id = $this->helpers->type()->ensureNonEmptyString($id); + $id = $this->helpers->type()->enforceUri($id); return new VcIssuerClaimValue($id, $data); } From c711fa47316760cb571f6c62d98adecca416e4d9 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Tue, 3 Jun 2025 15:43:48 +0200 Subject: [PATCH 25/66] WIP --- src/Codebooks/ClaimsEnum.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index 3445203..b1fef8b 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -40,10 +40,12 @@ enum ClaimsEnum: string case ClientRegistrationTypesSupported = 'client_registration_types_supported'; case CodeChallengeMethodsSupported = 'code_challenge_methods_supported'; case Contacts = 'contacts'; + case CredentialConfigurationId = 'credential_configuration_id'; case CredentialConfigurationIds = 'credential_configuration_ids'; case CredentialConfigurationsSupported = 'credential_configurations_supported'; case CredentialDefinition = 'credential_definition'; case CredentialEndpoint = 'credential_endpoint'; + case CredentialIdentifier = 'credential_identifier'; case CredentialIssuer = 'credential_issuer'; case CredentialResponseEncryption = 'credential_response_encryption'; case Credential_Schema = 'credentialSchema'; @@ -123,6 +125,7 @@ enum ClaimsEnum: string case PreAuthorizedCode = 'pre-authorized_code'; case PreAuthorizedGrantAnonymousAccessSupported = 'pre-authorized_grant_anonymous_access_supported'; case Proof = 'proof'; + case Proofs = 'proofs'; // ProofSigningAlgorithmValuesSupported case ProofSigningAlgValuesSupported = 'proof_signing_alg_values_supported'; case ProofTypesSupported = 'proof_types_supported'; From 04fdda9ea41e8ed14cc40c682295f4faffc04f8c Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Fri, 6 Jun 2025 16:24:31 +0200 Subject: [PATCH 26/66] WIP --- composer.json | 1 + src/Did.php | 26 + src/Did/DidKeyJwkResolver.php | 414 ++++++++++++++ src/Exceptions/DidException.php | 9 + src/Helpers.php | 8 + src/Helpers/Base64Url.php | 55 ++ src/Jws/ParsedJws.php | 2 +- tests/src/Did/DidKeyJwkResolverTest.php | 690 ++++++++++++++++++++++++ 8 files changed, 1204 insertions(+), 1 deletion(-) create mode 100644 src/Did.php create mode 100644 src/Did/DidKeyJwkResolver.php create mode 100644 src/Exceptions/DidException.php create mode 100644 src/Helpers/Base64Url.php create mode 100644 tests/src/Did/DidKeyJwkResolverTest.php diff --git a/composer.json b/composer.json index 86d8d7d..af7ecfa 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ }, "require": { "php": "^8.2", + "ext-gmp": "*", "ext-filter": "*", "guzzlehttp/guzzle": "^7.8", diff --git a/src/Did.php b/src/Did.php new file mode 100644 index 0000000..07cf44f --- /dev/null +++ b/src/Did.php @@ -0,0 +1,26 @@ +didKeyResolver ??= new DidKeyJwkResolver( + $this->helpers(), + ); + } + + public function helpers(): Helpers + { + return $this->helpers ??= new Helpers(); + } +} diff --git a/src/Did/DidKeyJwkResolver.php b/src/Did/DidKeyJwkResolver.php new file mode 100644 index 0000000..377ef69 --- /dev/null +++ b/src/Did/DidKeyJwkResolver.php @@ -0,0 +1,414 @@ +base58BtcDecode($base58Key); + + // Get the multicodec identifier and its length + [$multicodecIdentifier, $prefixLength] = $this->varintDecode($decodedKey); + + // Extract the actual key bytes (skip the multicodec bytes) + $keyBytes = substr($decodedKey, $prefixLength); + + // Determine the key type based on the multicodec identifier + // See: https://github.com/multiformats/multicodec/blob/master/table.csv + return match ($multicodecIdentifier) { + // Ed25519 public key (0xed in multicodec table) + 0xed => $this->createEd25519Jwk($keyBytes), + // X25519 public key (0xec in multicodec table) + 0xec => $this->createX25519Jwk($keyBytes), + // Secp256k1 public key (multicodec 0xe7) + // The original code had `0xe70, 0x01e7`. If the varint bytes are `\xe7\x01`, + // varintDecode will return 231 (0xe7). If the intended multicodec value is 0x01e7 (487), + // its varint encoding would be different (e.g., \xDF\x03). + // Assuming the multicodec code itself is 0xe7 (231). + 0xe7 => $this->createSecp256k1Jwk($keyBytes), + // P-256 (NIST) public key (multicodec 0x1200 for uncompressed, 0x1201 for compressed - typically + // 0x1200 used with JWK). Also adding 0x1102 as another possible identifier for P-256 keys + 0x1200, 0x1201, 0x1102 => $this->createP256Jwk($keyBytes), + // P-384 (NIST) public key (multicodec 0x1202) + 0x1202 => $this->createP384Jwk($keyBytes), + // P-521 (NIST) public key (multicodec 0x1203) + 0x1203 => $this->createP521Jwk($keyBytes), + // JSON JWK public key (0xeb51 in multicodec table) + 0xeb51 => $this->createJwkFromRawJson($keyBytes), + default => throw new DidException( + sprintf('Unsupported key type with multicodec identifier: 0x%04x', $multicodecIdentifier), + ), + }; + } catch (\Exception $exception) { + // It's good practice to re-throw with context, but avoid concatenating messages directly + // if the original exception message might contain sensitive info or be too verbose. + // Wrapping it is generally better. + throw new DidException('Error processing did:key: ' . $exception->getMessage(), 0, $exception); + } + } + + /** + * Decode a variable integer (varint) from bytes. + * + * This follows the multiformats varint specification where each byte uses 7 bits for the value + * and 1 bit to indicate if there are more bytes in the varint. + * As per the specification, varints must be encoded with the minimum number of bytes necessary. + * + * @see https://github.com/multiformats/unsigned-varint + * + * @param string $bytes Binary string containing a varint + * @return array{0: int, 1: int} Array containing [decoded value, number of bytes consumed] + * @throws \SimpleSAML\OpenID\Exceptions\DidException If the varint has invalid format + */ + public function varintDecode(string $bytes): array + { + if ($bytes === '') { + throw new DidException('Invalid varint: input is empty'); + } + + $result = 0; + $shift = 0; + $bytesRead = 0; + + for ($i = 0; $i < strlen($bytes); ++$i) { + $byte = ord($bytes[$i]); + ++$bytesRead; + + // Implementations typically support up to 10 bytes for u64. + // This implementation limits to 9 bytes, fitting PHP_INT_MAX (2^63-1). + if ($bytesRead > 9) { + throw new DidException('Invalid varint: too many bytes (max 9 for this implementation).'); + } + + $valuePart = $byte & 0x7F; + + // Add value part to result. + // PHP integers are signed 64-bit. Max shift is 7*8=56 for the 9th byte. + // (X << 56) is safe. Result should not overflow PHP_INT_MAX for valid multicodec IDs. + $result |= ($valuePart << $shift); + + if (($byte & 0x80) === 0) { // This is the last byte in the varint sequence + // Check for overlong encoding (violates minimality constraint): + // 1. If the varint is multi-byte (bytesRead > 1) and the last byte is 0x00. + // This covers cases like `\x81\x00` for 1, or `\x80\x00` for 0. + // 2. The value 0 must be encoded as a single `\x00`. If result is 0 and bytesRead > 1, + // it's an overlong encoding of 0 (e.g. `\x80\x00`), which is caught by the first condition. + if ($byte === 0x00 && $bytesRead > 1) { + throw new DidException('Invalid varint: overlong encoding (minimality constraint violated).'); + } + + return [$result, $bytesRead]; + } + + $shift += 7; + // Check if the next shift would overflow standard integer types (more relevant for fixed-size integers). + // For PHP's arbitrary precision integers (when they become floats/GMP objects), this is less of an issue, + // but for multicodec IDs, values are small and fit in standard integers. + } + + // If the loop finishes, it means the last byte had its MSB set, indicating an incomplete sequence. + throw new DidException('Invalid varint: incomplete sequence (unterminated).'); + } + + /** + * Decode a base58 encoded string. + */ + // In SimpleSAML\OpenID\Did\DidKeyJwkResolver.php + public function base58BtcDecode(string $base58encodedString): string + { + $alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + $base = strlen($alphabet); + +// if ($base58encodedString === 'ABnLTmg5e1PhaB9S2qAvL9L3Q') { +// error_log("[base58BtcDecode] Input: " . $base58encodedString); +// error_log("[base58BtcDecode] Alphabet: " . $alphabet); +// error_log("[base58BtcDecode] Alphabet Length (base): " . $base); +// } + + $num = gmp_init(0); + for ($i = 0; $i < strlen($base58encodedString); ++$i) { + $char = $base58encodedString[$i]; + $pos = strpos($alphabet, $char); + + if ($pos === false) { + throw new \InvalidArgumentException('Invalid character in base58 string: ' . $char); + } + +// if ($base58encodedString === 'ABnLTmg5e1PhaB9S2qAvL9L3Q') { +// error_log("[base58BtcDecode] char: '{$char}', pos: {$pos}"); +// } + + $num = gmp_add(gmp_mul($num, $base), $pos); + } + +// if ($base58encodedString === 'ABnLTmg5e1PhaB9S2qAvL9L3Q') { +// error_log("[base58BtcDecode] Calculated GMP num (hex): " . gmp_strval($num, 16)); +// } + + // ... rest of the method ... + $result = ''; + /** @phpstan-ignore argument.type */ + while (gmp_cmp($num, 0) > 0) { + /** @phpstan-ignore argument.type */ + [$numQuotient, $remainder] = gmp_div_qr($num, 256); // Use a different variable for quotient + $num = $numQuotient; // Reassign $num + /** @phpstan-ignore argument.type */ + $result = chr(gmp_intval($remainder)) . $result; + } + + // Add leading zeros + for ($j = 0; $j < strlen($base58encodedString) && $base58encodedString[$j] === '1'; ++$j) { + $result = "\0" . $result; + } + + return $result; + } + + + /** + * Create a JWK for an Ed25519 public key. + * + * @param string $rawKeyBytes The raw key bytes + * @return mixed[] The JWK representation + */ + public function createEd25519Jwk(string $rawKeyBytes): array + { + return [ + 'kty' => 'OKP', + 'crv' => 'Ed25519', + 'x' => $this->helpers->base64Url()->encode($rawKeyBytes), + 'use' => 'sig', + ]; + } + + /** + * Create a JWK for an X25519 public key. + * + * @param string $rawKeyBytes The raw key bytes + * @return mixed[] The JWK representation + */ + public function createX25519Jwk(string $rawKeyBytes): array + { + return [ + 'kty' => 'OKP', + 'crv' => 'X25519', + 'x' => $this->helpers->base64Url()->encode($rawKeyBytes), + 'use' => 'enc', + ]; + } + + /** + * Create a JWK for a Secp256k1 public key. + * + * @param string $rawKeyBytes The raw key bytes + * @return mixed[] The JWK representation + * @throws \SimpleSAML\OpenID\Exceptions\DidException + */ + public function createSecp256k1Jwk(string $rawKeyBytes): array + { + // For Secp256k1, we need to extract x and y coordinates from the compressed or uncompressed point + $firstByte = ord($rawKeyBytes[0]); + + if ($firstByte === 0x04 && strlen($rawKeyBytes) === 65) { + // Uncompressed point format (0x04 || x || y) + $x = substr($rawKeyBytes, 1, 32); + $y = substr($rawKeyBytes, 33, 32); + } elseif (($firstByte === 0x02 || $firstByte === 0x03) && strlen($rawKeyBytes) === 33) { + // Compressed point format - would need to decompress + // This is complex and requires secp256k1 library support + throw new DidException('Compressed Secp256k1 keys are not currently supported'); + } else { + throw new DidException('Invalid Secp256k1 public key format'); + } + + return [ + 'kty' => 'EC', + 'crv' => 'secp256k1', + 'x' => $this->helpers->base64Url()->encode($x), + 'y' => $this->helpers->base64Url()->encode($y), + 'use' => 'sig', + ]; + } + + /** + * Create a JWK for a P-256 (NIST) public key. + * + * @param string $rawKeyBytes The raw key bytes + * @return mixed[] The JWK representation + * @throws \SimpleSAML\OpenID\Exceptions\DidException + */ + public function createP256Jwk(string $rawKeyBytes): array + { + // Similar to Secp256k1, we need to extract x and y coordinates + $firstByte = ord($rawKeyBytes[0]); + + if ($firstByte === 0x04 && strlen($rawKeyBytes) === 65) { + // Uncompressed point format (0x04 || x || y) + $x = substr($rawKeyBytes, 1, 32); + $y = substr($rawKeyBytes, 33, 32); + } elseif (($firstByte === 0x02 || $firstByte === 0x03) && strlen($rawKeyBytes) === 33) { + // Compressed point format - would need to decompress + // This is complex and requires specific library support + throw new DidException('Compressed P-256 keys are not currently supported'); + } elseif (strlen($rawKeyBytes) === 64) { + // Some implementations might not include the leading 0x04 byte + // Try to interpret as raw x || y coordinates + $x = substr($rawKeyBytes, 0, 32); + $y = substr($rawKeyBytes, 32, 32); + } else { + throw new DidException('Invalid P-256 public key format'); + } + + return [ + 'kty' => 'EC', + 'crv' => 'P-256', + 'x' => $this->helpers->base64Url()->encode($x), + 'y' => $this->helpers->base64Url()->encode($y), + 'use' => 'sig', + ]; + } + + /** + * Create a JWK for a P-384 (NIST) public key. + * + * @param string $rawKeyBytes The raw key bytes + * @return mixed[] The JWK representation + * @throws \SimpleSAML\OpenID\Exceptions\DidException + */ + public function createP384Jwk(string $rawKeyBytes): array + { + $firstByte = ord($rawKeyBytes[0]); + + if ($firstByte === 0x04 && strlen($rawKeyBytes) === 97) { + // Uncompressed point format (0x04 || x || y) + $x = substr($rawKeyBytes, 1, 48); + $y = substr($rawKeyBytes, 49, 48); + } else { + throw new DidException('Invalid P-384 public key format'); + } + + return [ + 'kty' => 'EC', + 'crv' => 'P-384', + 'x' => $this->helpers->base64Url()->encode($x), + 'y' => $this->helpers->base64Url()->encode($y), + 'use' => 'sig', + ]; + } + + /** + * Create a JWK for a P-521 (NIST) public key. + * + * @return mixed[] The JWK representation + * @throws \SimpleSAML\OpenID\Exceptions\DidException + */ + public function createP521Jwk(string $rawKeyBytes): array + { + $firstByte = ord($rawKeyBytes[0]); + + if ($firstByte === 0x04 && strlen($rawKeyBytes) === 133) { + // Uncompressed point format (0x04 || x || y) + $x = substr($rawKeyBytes, 1, 66); + $y = substr($rawKeyBytes, 67, 66); + } else { + throw new DidException('Invalid P-521 public key format'); + } + + return [ + 'kty' => 'EC', + 'crv' => 'P-521', + 'x' => $this->helpers->base64Url()->encode($x), + 'y' => $this->helpers->base64Url()->encode($y), + 'use' => 'sig', + ]; + } + + /** + * Create a JWK from raw JSON data. + * + * Used for multicodec identifier 0xeb51, which represents a JSON object containing + * only the required members of a JWK (RFC 7518 and RFC 7517) representing the public key. + * Serialization is based on JCS (RFC 8785). + * + * @param string $rawJsonBytes The raw JSON bytes + * @return mixed[] The JWK representation + * @throws \SimpleSAML\OpenID\Exceptions\DidException + */ + public function createJwkFromRawJson(string $rawJsonBytes): array + { + try { + $jwk = $this->helpers->json()->decode($rawJsonBytes); + + // Validate that this is a valid JWK + if (!is_array($jwk) || !isset($jwk['kty'])) { + throw new DidException('Invalid JWK format: missing required "kty" property'); + } + + // For EC keys, validate required parameters + if ($jwk['kty'] === 'EC') { + if (!isset($jwk['crv'], $jwk['x'], $jwk['y'])) { + throw new DidException('Invalid EC JWK format: missing required properties'); + } + } elseif ($jwk['kty'] === 'OKP') { + if (!isset($jwk['crv'], $jwk['x'])) { + throw new DidException('Invalid OKP JWK format: missing required properties'); + } + } elseif ($jwk['kty'] === 'RSA') { + if (!isset($jwk['n'], $jwk['e'])) { + throw new DidException('Invalid RSA JWK format: missing required properties'); + } + } + + // Default to 'sig' use if not specified + if (!isset($jwk['use'])) { + $jwk['use'] = 'sig'; + } + + return $jwk; + } catch (\JsonException $jsonException) { + throw new DidException('Failed to parse JWK JSON: ' . $jsonException->getMessage()); + } + } +} diff --git a/src/Exceptions/DidException.php b/src/Exceptions/DidException.php new file mode 100644 index 0000000..fffccb4 --- /dev/null +++ b/src/Exceptions/DidException.php @@ -0,0 +1,9 @@ +value; diff --git a/tests/src/Did/DidKeyJwkResolverTest.php b/tests/src/Did/DidKeyJwkResolverTest.php new file mode 100644 index 0000000..43b2ac3 --- /dev/null +++ b/tests/src/Did/DidKeyJwkResolverTest.php @@ -0,0 +1,690 @@ +createMock(Base64Url::class); + + $base64UrlMock->method('encode') + ->willReturnCallback(fn(string $data): string => base64_encode($data)); + + $this->helpersMock = $this->createMock(Helpers::class); + + $this->helpersMock->method('base64Url') + ->willReturn($base64UrlMock); + } + + protected function sut( + ?Helpers $helpers = null, + ): DidKeyJwkResolver { + $helpers ??= $this->helpersMock; + + return new DidKeyJwkResolver($helpers); + } + + /** + * Test that invalid did:key format throws an exception + */ + public function testInvalidDidKeyFormatThrowsException(): void + { + $this->expectException(DidException::class); + $this->expectExceptionMessage('Invalid did:key format. Must start with "did:key:"'); + + $this->sut()->extractJwkFromDidKey('invalid:key:value'); + } + + /** + * Test that unsupported multibase encoding throws an exception + */ + public function testUnsupportedMultibaseEncodingThrowsException(): void + { + $this->expectException(DidException::class); + $this->expectExceptionMessage( + 'Unsupported multibase encoding. Only base58btc (z-prefixed) is currently supported.', + ); + + $this->sut()->extractJwkFromDidKey('did:key:a123'); // 'a' prefix is not supported + } + + /** + * Test base58BtcDecode method with invalid character + */ + public function testBase58BtcDecodeWithInvalidCharacter(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid character in base58 string'); + + $this->sut()->base58BtcDecode('invalid*string'); // '*' is not in the base58 alphabet + } + + /** + * Test base58BtcDecode method with valid input + */ + public function testBase58BtcDecodeWithValidInput(): void + { + // Test with a known Base58 encoding + // Example: '111' in Base58 decodes to bytes representing 0 + $result = $this->sut()->base58BtcDecode('111'); + + $this->assertSame("\0\0\0", $result); + } + + /** + * Test createEd25519Jwk method + */ + public function testCreateEd25519Jwk(): void + { + $rawKeyBytes = "test-key-data"; + $encodedKey = base64_encode($rawKeyBytes); + + $expectedJwk = [ + 'kty' => 'OKP', + 'crv' => 'Ed25519', + 'x' => $encodedKey, + 'use' => 'sig', + ]; + + $jwk = $this->sut()->createEd25519Jwk($rawKeyBytes); + + $this->assertSame($expectedJwk, $jwk); + } + + /** + * Test createX25519Jwk method + */ + public function testCreateX25519Jwk(): void + { + $rawKeyBytes = "test-key-data"; + $encodedKey = base64_encode($rawKeyBytes); + + $expectedJwk = [ + 'kty' => 'OKP', + 'crv' => 'X25519', + 'x' => $encodedKey, + 'use' => 'enc', + ]; + + $jwk = $this->sut()->createX25519Jwk($rawKeyBytes); + + $this->assertSame($expectedJwk, $jwk); + } + + /** + * Test createSecp256k1Jwk method with valid uncompressed point + */ + public function testCreateSecp256k1JwkWithValidUncompressedPoint(): void + { + // Create a mock uncompressed point format (0x04 || x || y) + // 0x04 byte followed by 32 bytes for x and 32 bytes for y + $x = str_repeat('x', 32); + $y = str_repeat('y', 32); + $rawKeyBytes = "\x04" . $x . $y; + + $expectedJwk = [ + 'kty' => 'EC', + 'crv' => 'secp256k1', + 'x' => base64_encode($x), + 'y' => base64_encode($y), + 'use' => 'sig', + ]; + + $jwk = $this->sut()->createSecp256k1Jwk($rawKeyBytes); + + $this->assertSame($expectedJwk, $jwk); + } + + /** + * Test createSecp256k1Jwk method with invalid key format + */ + public function testCreateSecp256k1JwkWithInvalidKeyFormat(): void + { + $this->expectException(DidException::class); + $this->expectExceptionMessage('Invalid Secp256k1 public key format'); + + // Invalid key format - wrong length + $rawKeyBytes = "\x04" . str_repeat('x', 10); + + $this->sut()->createSecp256k1Jwk($rawKeyBytes); + } + + /** + * Test createSecp256k1Jwk method with compressed point format + */ + public function testCreateSecp256k1JwkWithCompressedPoint(): void + { + $this->expectException(DidException::class); + $this->expectExceptionMessage('Compressed Secp256k1 keys are not currently supported'); + + // Compressed point format (0x02 or 0x03 followed by 32 bytes for x) + $rawKeyBytes = "\x02" . str_repeat('x', 32); + + $this->sut()->createSecp256k1Jwk($rawKeyBytes); + } + + /** + * Test createP256Jwk method with valid uncompressed point + */ + public function testCreateP256JwkWithValidUncompressedPoint(): void + { + // Create a mock uncompressed point format (0x04 || x || y) + $x = str_repeat('x', 32); + $y = str_repeat('y', 32); + $rawKeyBytes = "\x04" . $x . $y; + + $expectedJwk = [ + 'kty' => 'EC', + 'crv' => 'P-256', + 'x' => base64_encode($x), + 'y' => base64_encode($y), + 'use' => 'sig', + ]; + + $jwk = $this->sut()->createP256Jwk($rawKeyBytes); + + $this->assertSame($expectedJwk, $jwk); + } + + /** + * Test createP256Jwk method with invalid key format + */ + public function testCreateP256JwkWithInvalidKeyFormat(): void + { + $this->expectException(DidException::class); + $this->expectExceptionMessage('Invalid P-256 public key format'); + + // Invalid key format - wrong first byte + $rawKeyBytes = "\x02" . str_repeat('x', 64); + + $this->sut()->createP256Jwk($rawKeyBytes); + } + + /** + * Test createP384Jwk method with valid uncompressed point + */ + public function testCreateP384JwkWithValidUncompressedPoint(): void + { + // Create a mock uncompressed point format (0x04 || x || y) + $x = str_repeat('x', 48); + $y = str_repeat('y', 48); + $rawKeyBytes = "\x04" . $x . $y; + + $expectedJwk = [ + 'kty' => 'EC', + 'crv' => 'P-384', + 'x' => base64_encode($x), + 'y' => base64_encode($y), + 'use' => 'sig', + ]; + + $jwk = $this->sut()->createP384Jwk($rawKeyBytes); + + $this->assertSame($expectedJwk, $jwk); + } + + /** + * Test createP384Jwk method with invalid key format + */ + public function testCreateP384JwkWithInvalidKeyFormat(): void + { + $this->expectException(DidException::class); + $this->expectExceptionMessage('Invalid P-384 public key format'); + + // Invalid key format - wrong length + $rawKeyBytes = "\x04" . str_repeat('x', 50); + + $this->sut()->createP384Jwk($rawKeyBytes); + } + + /** + * Test createP521Jwk method with valid uncompressed point + */ + public function testCreateP521JwkWithValidUncompressedPoint(): void + { + // Create a mock uncompressed point format (0x04 || x || y) + $x = str_repeat('x', 66); + $y = str_repeat('y', 66); + $rawKeyBytes = "\x04" . $x . $y; + + $expectedJwk = [ + 'kty' => 'EC', + 'crv' => 'P-521', + 'x' => base64_encode($x), + 'y' => base64_encode($y), + 'use' => 'sig', + ]; + + $jwk = $this->sut()->createP521Jwk($rawKeyBytes); + + $this->assertSame($expectedJwk, $jwk); + } + + /** + * Test createP521Jwk method with invalid key format + */ + public function testCreateP521JwkWithInvalidKeyFormat(): void + { + $this->expectException(DidException::class); + $this->expectExceptionMessage('Invalid P-521 public key format'); + + // Invalid key format - wrong length + $rawKeyBytes = "\x04" . str_repeat('x', 70); + + $this->sut()->createP521Jwk($rawKeyBytes); + } + + public function testExtractJwkFromDidKeyWithUnsupportedMulticodecIdentifier(): void + { + $resolverMock = $this->getMockBuilder(DidKeyJwkResolver::class) + ->setConstructorArgs([$this->helpersMock]) + ->onlyMethods(['base58BtcDecode']) + ->getMock(); + + $decodedKey = "\xFF\xFF" . str_repeat('x', 32); + $resolverMock->method('base58BtcDecode') + ->willReturn($decodedKey); + + $this->expectException(DidException::class); + $this->expectExceptionMessage('Unsupported'); + + $resolverMock->extractJwkFromDidKey('did:key:z123'); + } + + /** + * Test extractJwkFromDidKey with an Ed25519 key + */ + public function testExtractJwkFromDidKeyWithEd25519Key(): void + { + // Create a partial mock of the resolver + $resolverMock = $this->getMockBuilder(DidKeyJwkResolver::class) + ->setConstructorArgs([$this->helpersMock]) + ->onlyMethods(['base58BtcDecode', 'createEd25519Jwk']) + ->getMock(); + + // Set up the mock to return a key with Ed25519 multicodec identifier (0xed01) + $keyBytes = str_repeat('x', 32); + $decodedKey = "\xED\x01" . $keyBytes; // 0xED01 is Ed25519 + $resolverMock->method('base58BtcDecode') + ->willReturn($decodedKey); + + // Set up the expected JWK + $expectedJwk = [ + 'kty' => 'OKP', + 'crv' => 'Ed25519', + 'x' => 'test-encoded-key', + 'use' => 'sig', + ]; + + // Set up the createEd25519Jwk mock + $resolverMock->method('createEd25519Jwk') + ->with($keyBytes) + ->willReturn($expectedJwk); + + // Call the method + $jwk = $resolverMock->extractJwkFromDidKey('did:key:z123'); + + // Assert + $this->assertEquals($expectedJwk, $jwk); + } + + /** + * Test the integrated flow of extractJwkFromDidKey + */ + public function testExtractJwkFromDidKeyIntegrated(): void + { + // This test demonstrates how all the pieces fit together + // Create a mock with minimal stubbing - just enough to make the test predictable + $resolverMock = $this->getMockBuilder(DidKeyJwkResolver::class) + ->setConstructorArgs([$this->helpersMock]) + ->onlyMethods(['base58BtcDecode']) + ->getMock(); + + // For an Ed25519 key (multicodec 0xed01) + $keyBytes = str_repeat('x', 32); + $decodedKey = "\xED\x01" . $keyBytes; + $resolverMock->method('base58BtcDecode') + ->willReturn($decodedKey); + + $jwk = $resolverMock->extractJwkFromDidKey('did:key:z123'); + + $this->assertEquals('OKP', $jwk['kty']); + $this->assertEquals('Ed25519', $jwk['crv']); + $this->assertEquals('sig', $jwk['use']); + $this->assertArrayHasKey('x', $jwk); + } + + /** + * Test that an exception in base58BtcDecode is properly wrapped + */ + public function testExtractJwkFromDidKeyWithDecodingException(): void + { + // Create a partial mock of the resolver + $resolverMock = $this->getMockBuilder(DidKeyJwkResolver::class) + ->setConstructorArgs([$this->helpersMock]) + ->onlyMethods(['base58BtcDecode']) + ->getMock(); + + // Set up the mock to throw an exception + $resolverMock->method('base58BtcDecode') + ->willThrowException(new \InvalidArgumentException('Test exception')); + + $this->expectException(DidException::class); + $this->expectExceptionMessage('Error processing did:key: Test exception'); + + $resolverMock->extractJwkFromDidKey('did:key:z123'); + } + + /** + * Test extraction with real Ed25519 did:key value + */ + public function testExtractJwkFromRealEd25519DidKey(): void + { + // Create a real Helpers instance for this test instead of a mock + $helpers = new Helpers(); + $resolver = new DidKeyJwkResolver($helpers); + + // This is a real Ed25519 did:key + $didKey = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK'; + + $jwk = $resolver->extractJwkFromDidKey($didKey); + + $this->assertEquals('OKP', $jwk['kty']); + $this->assertEquals('Ed25519', $jwk['crv']); + $this->assertEquals('sig', $jwk['use']); + $this->assertArrayHasKey('x', $jwk); + } + + /** + * Test extraction with the provided sample did:key value + */ + public function testExtractJwkFromProvidedSample(): void + { + // Set up a real Helpers instance for this test + $helpers = new Helpers(); + $resolver = new DidKeyJwkResolver($helpers); + + // This is the provided sample did:key + // phpcs:ignore + $didKey = 'did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbp7R1FUvzP1s9pLTKP21oYQNWMJFzgVGWYb5WmD3ngVmjMeTABs9MjYUaRfzTWg9dLdPw6o16UeakmtE7tHDMug3XgcJptPxRYuwFdVJXa6KAMUBhkmouMZisDJYMGbaGAp'; + + // This test might fail until the multicodec issue is fixed + // We'll use try/catch to provide more diagnostic information + try { + $jwk = $resolver->extractJwkFromDidKey($didKey); + + // If we get here, verify the JWK structure is correct + $this->assertArrayHasKey('kty', $jwk); + $this->assertArrayHasKey('crv', $jwk); + $this->assertArrayHasKey('use', $jwk); + } catch (DidException $didException) { + $this->markTestIncomplete('Failed to process the sample did:key: ' . $didException->getMessage()); + } + } + + /** + * Test multiple real did:key values for different key types + */ + #[DataProvider('provideRealDidKeys')] + public function testMultipleRealDidKeys(string $didKey, string $expectedCrv, string $expectedKty): void + { + // Create a real Helpers instance for this test + $helpers = new Helpers(); + $resolver = new DidKeyJwkResolver($helpers); + + try { + $jwk = $resolver->extractJwkFromDidKey($didKey); + + $this->assertEquals($expectedKty, $jwk['kty']); + $this->assertEquals($expectedCrv, $jwk['crv']); + $this->assertArrayHasKey('use', $jwk); + } catch (DidException $didException) { + $this->markTestSkipped('Failed to process did:key: ' . $didException->getMessage()); + } + } + + /** + * Data provider for real did:key values + */ + public static function provideRealDidKeys(): \Iterator + { + yield 'Ed25519 key' => [ + 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK', + 'Ed25519', + 'OKP', + ]; + yield 'X25519 key' => [ + 'did:key:z6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc', + 'X25519', + 'OKP', + ]; + } + + public function testRealDidKeys(): void + { + $sut = $this->sut(new Helpers()); + + // phpcs:ignore + $didKey = 'did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbo1sB3YbioCwNzDxGk58kUKzV4gzuhvVPFSxRefivZgKeZnifmxtbkM91FGgofu6ivysQw4Um9baehyFHtbyqBNWNFvgrHXxLLA6MRqt3TvcqBcDHXiyQAGGk9mBFPEFt1J'; + + $jwk = $sut->extractJwkFromDidKey($didKey); + + $this->assertEquals('EC', $jwk['kty']); + $this->assertEquals('P-256', $jwk['crv']); + $this->assertEquals('8Yp_v1yn57IPjCDhS-xjQnIt8WR8UgJO_gNDwvlwwvI', $jwk['x']); + $this->assertEquals('bURU-YbrZLmNob1ecG4obRvs4RRQV4u0PWiR3j8qgoQ', $jwk['y']); + $this->assertEquals('sig', $jwk['use']); + } + + public function testCreateJwkFromRawJson(): void + { + $sut = $this->sut(new Helpers()); + + // phpcs:ignore + $jsonData = '{"crv":"P-256","kty":"EC","x":"dDfIibQM-949qf4jj-8mBY4Azq34ygSGhzd8AT2mx6s","y":"VUyI8G-OYirMrrsCB9lvUbr6Wjq2ef73ne_paBqLPxw"}'; + + $jwk = $sut->createJwkFromRawJson($jsonData); + + $this->assertEquals('EC', $jwk['kty']); + $this->assertEquals('P-256', $jwk['crv']); + $this->assertEquals('dDfIibQM-949qf4jj-8mBY4Azq34ygSGhzd8AT2mx6s', $jwk['x']); + $this->assertEquals('VUyI8G-OYirMrrsCB9lvUbr6Wjq2ef73ne_paBqLPxw', $jwk['y']); + $this->assertEquals('sig', $jwk['use']); + } + + public function testCreateJwkFromInvalidJsonThrows(): void + { + $sut = $this->sut(new Helpers()); + + $this->expectException(DidException::class); + $this->expectExceptionMessage('Failed to parse JWK JSON'); + + $invalidJson = '{"crv":"P-256","kty":"EC",'; + $sut->createJwkFromRawJson($invalidJson); + } + + public function testCreateJwkFromInvalidJwkFormatThrows(): void + { + $sut = $this->sut(new Helpers()); + + $this->expectException(DidException::class); + $this->expectExceptionMessage('Invalid JWK format: missing required "kty" property'); + + $invalidJwk = '{"crv":"P-256"}'; + $sut->createJwkFromRawJson($invalidJwk); + } + + public function testCreateJwkFromInvalidEcJwkFormatThrows(): void + { + $sut = $this->sut(new Helpers()); + + $this->expectException(DidException::class); + $this->expectExceptionMessage('Invalid EC JWK format: missing required properties'); + + $invalidEcJwk = '{"kty":"EC","crv":"P-256"}'; + $sut->createJwkFromRawJson($invalidEcJwk); + } + + #[DataProvider('varintValidDataProvider')] + public function testVarintDecodeValid(string $bytes, int $expectedValue, int $expectedLength): void + { + [$value, $length] = $this->sut()->varintDecode($bytes); + $this->assertSame($expectedValue, $value, "Decoded value mismatch for input: " . bin2hex($bytes)); + $this->assertSame($expectedLength, $length, "Decoded length mismatch for input: " . bin2hex($bytes)); + } + + public static function varintValidDataProvider(): \Iterator + { + // Single byte + yield 'zero' => ["\x00", 0, 1]; + yield 'one' => ["\x01", 1, 1]; + yield 'max single byte' => ["\x7F", 127, 1]; + // Multi-byte + yield '128' => ["\x80\x01", 128, 2]; + // Min two-byte + yield '150' => ["\x96\x01", 150, 2]; + // (150-128=22) -> 0x96 = 0x80 | 22 + yield '300' => ["\xAC\x02", 300, 2]; + // 300 = (2 * 128) + 44. 44=0x2C. \xAC = 0x80|0x2C. \x02 + yield 'multicodec Ed25519 (0xed)' => ["\xED\x01", 0xed, 2]; + yield 'multicodec secp256k1-pub (0xe7 from table, encoded as \xe7\x01 by some conventions)' => [ + "\xE7\x01", + 0xE7, + 2, + ]; + // Decodes to 231 + // Max 9-byte value (2^63 - 1, which is PHP_INT_MAX on 64-bit systems) + yield 'max 9-byte value (PHP_INT_MAX)' => [ + str_repeat("\xFF", 8) . "\x7F", + PHP_INT_MAX, // (2^63 - 1) + 9, + ]; + } + + + #[DataProvider('varintInvalidDataProvider')] + public function testVarintDecodeInvalid(string $bytes, string $expectedExceptionMessage): void + { + $this->expectException(DidException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + $this->sut()->varintDecode($bytes); + } + + public static function varintInvalidDataProvider(): \Iterator + { + yield 'empty string' => ['', 'Invalid varint: input is empty']; + yield 'unterminated sequence (single byte)' => [ + "\x80", + 'Invalid varint: incomplete sequence (unterminated).', + ]; + yield 'unterminated sequence (multi-byte)' => [ + "\xFF\xFF", + 'Invalid varint: incomplete sequence (unterminated).', + ]; + yield 'overlong encoding of 0' => [ + "\x80\x00", + 'Invalid varint: overlong encoding (minimality constraint violated).', + ]; + yield 'overlong encoding of 1' => [ + "\x81\x00", + 'Invalid varint: overlong encoding (minimality constraint violated).', + ]; + yield 'too many bytes (10th byte)' => [ + str_repeat("\x80", 9) . "\x01", + 'Invalid varint: too many bytes (max 9 for this implementation).', + ]; + yield 'unterminated, 9 bytes with MSB set' => [ + str_repeat("\xFF", 9), + 'Invalid varint: incomplete sequence (unterminated).', + ]; + } + + #[DataProvider('base58DecodeValidDataProvider')] + public function testBase58BtcDecodeValidInputs(string $base58encoded, string $expectedDecoded): void + { + $actualDecoded = $this->sut()->base58BtcDecode($base58encoded); + +// if ($base58encoded === 'ABnLTmg5e1PhaB9S2qAvL9L3Q') { +// $expectedHex = bin2hex($expectedDecoded); +// $actualHex = bin2hex($actualDecoded); +// if ($expectedHex !== $actualHex) { +// error_log("Debug Info for: " . $base58encoded); +// error_log("Expected Hex: " . $expectedHex); +// error_log("Actual Hex: " . $actualHex); +// // You might also want to add logging inside base58BtcDecode +// // to see the intermediate GMP number ($num) +// } +// } + + $this->assertSame($expectedDecoded, $actualDecoded, "Failed for input: " . $base58encoded); + } + + public static function base58DecodeValidDataProvider(): \Iterator + { + yield 'empty string' => ['', '']; + yield 'single char "z" (value 57)' => ['z', '9']; + // chr(57) + yield 'single char "6" (value 5)' => ['6', chr(5)]; + yield 'single char "L" (value 25)' => ['L', chr(19)]; + yield 'two chars "2g" (value 88)' => ['2g', 'a']; + // chr(97) + yield 'leading zero "1"' => ['1', "\0"]; + yield 'multiple leading zeros "111"' => ['111', "\0\0\0"]; + yield 'leading zero and char "1z"' => ['1z', '9']; + // 'z' is value 57, chr(57) is '9' + yield 'leading zero and char "16"' => ['16', "\0" . chr(5)]; + yield 'leading zero and char "1L"' => ['1L', "\0" . chr(19)]; + yield 'leading zero and two chars "12g"' => ['12g', "\0a"]; + // "\0" . chr(97) + yield 'value 256 ("5R")' => ['5R', "\x01\x00"]; + // Decodes to bytes representing 256 + yield 'string "Hello World"' => [ // "Hello World" as ASCII bytes + 'JxF12TrwUP45BMd', // Base58 of "Hello World" + "Hello World", // Equivalent to "\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64" + ]; + yield 'three leading ones then value 1 ("1112")' => ['1112', "\0\0\0\x01"]; + // Test vector from cryptocoinjs/bs58 test suite + // hex "000000287fb4cd" -> bytes "\x00\x00\x00\x28\x7f\xb4\xcd" + yield 'bs58 lib vector 1' => ['111233QC4', "\x00\x00\x00\x28\x7f\xb4\xcd"]; + } + + #[DataProvider('base58DecodeInvalidCharDataProvider')] + public function testBase58BtcDecodeThrowsOnInvalidCharacter(string $base58invalid): void + { + $this->expectException(\InvalidArgumentException::class); + // Check that the message starts with the expected prefix and mentions a character + $this->expectExceptionMessage('Invalid'); + $this->sut()->base58BtcDecode($base58invalid); + } + + public static function base58DecodeInvalidCharDataProvider(): \Iterator + { + yield 'invalid char "0" (zero)' => ['0']; + yield 'invalid char "I" (capital i)' => ['I']; + yield 'invalid char "O" (capital o)' => ['O']; + yield 'invalid char "l" (lowercase L)' => ['l']; + yield 'valid prefix, invalid suffix "abc0def"' => ['abc0def']; + yield 'invalid char with space "ab c"' => ['ab c']; + yield 'invalid char with newline "ab\nc"' => ["ab\nc"]; + yield 'invalid char with tab "ab\tc"' => ["ab\tc"]; + } +} From 5db5adb77bf1b0aaf9b85669d796ad8dfe00b104 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Fri, 6 Jun 2025 18:10:58 +0200 Subject: [PATCH 27/66] WIP --- src/Algorithms/SignatureAlgorithmEnum.php | 5 + src/Codebooks/ClaimsEnum.php | 5 + src/Codebooks/JwtTypesEnum.php | 1 + src/Exceptions/OpenId4VciProofException.php | 9 ++ src/Federation/TrustMarkDelegation.php | 2 + src/Jws/ParsedJws.php | 18 +++ src/VerifiableCredentials.php | 16 +++ .../Factories/OpenId4VciProofFactory.php | 57 ++++++++ src/VerifiableCredentials/OpenId4VciProof.php | 132 ++++++++++++++++++ 9 files changed, 245 insertions(+) create mode 100644 src/Exceptions/OpenId4VciProofException.php create mode 100644 src/VerifiableCredentials/Factories/OpenId4VciProofFactory.php create mode 100644 src/VerifiableCredentials/OpenId4VciProof.php diff --git a/src/Algorithms/SignatureAlgorithmEnum.php b/src/Algorithms/SignatureAlgorithmEnum.php index 38e9a4d..35a87b7 100644 --- a/src/Algorithms/SignatureAlgorithmEnum.php +++ b/src/Algorithms/SignatureAlgorithmEnum.php @@ -47,4 +47,9 @@ public function instance(): SignatureAlgorithm self::RS512 => new RS512(), }; } + + public function isNone(): bool + { + return $this === SignatureAlgorithmEnum::none; + } } diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index b1fef8b..74faed4 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -91,6 +91,8 @@ enum ClaimsEnum: string case IssuerState = 'issuer_state'; // JWT ID case Jti = 'jti'; + // JsonWebKey + case Jwk = 'jwk'; // JsonWebKeySet case Jwks = 'jwks'; case JwksUri = 'jwks_uri'; @@ -109,6 +111,7 @@ enum ClaimsEnum: string // MetadataPolicyCritical case MetadataPolicyCrit = 'metadata_policy_crit'; case Name = 'name'; + case Nonce = 'nonce'; case NonceEndpoint = 'nonce_endpoint'; // NotBefore case Nbf = 'nbf'; @@ -178,4 +181,6 @@ enum ClaimsEnum: string case UserinfoEndpoint = 'userinfo_endpoint'; // VerifiableCredential case Vc = 'vc'; + // X509certificateChain + case X5c = 'x5c'; } diff --git a/src/Codebooks/JwtTypesEnum.php b/src/Codebooks/JwtTypesEnum.php index 95e96a1..6808cbf 100644 --- a/src/Codebooks/JwtTypesEnum.php +++ b/src/Codebooks/JwtTypesEnum.php @@ -9,6 +9,7 @@ enum JwtTypesEnum: string case EntityStatementJwt = 'entity-statement+jwt'; case JwkSetJwt = 'jwk-set+jwt'; case Jwt = 'JWT'; + case OpenId4VciProofJwt = 'openid4vci-proof+jwt'; case TrustMarkJwt = 'trust-mark+jwt'; case TrustMarkDelegationJwt = 'trust-mark-delegation+jwt'; } diff --git a/src/Exceptions/OpenId4VciProofException.php b/src/Exceptions/OpenId4VciProofException.php new file mode 100644 index 0000000..c592f6d --- /dev/null +++ b/src/Exceptions/OpenId4VciProofException.php @@ -0,0 +1,9 @@ +helpers->type()->ensureNonEmptyString($typ, $claimKey); } + + /** + * @return ?non-empty-string + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getAlgorithm(): ?string + { + $claimKey = ClaimsEnum::Alg->value; + + $typ = $this->getHeaderClaim($claimKey); + + return is_null($typ) ? null : $this->helpers->type()->ensureNonEmptyString($typ, $claimKey); + } } diff --git a/src/VerifiableCredentials.php b/src/VerifiableCredentials.php index 85012e1..5a96526 100644 --- a/src/VerifiableCredentials.php +++ b/src/VerifiableCredentials.php @@ -20,6 +20,7 @@ use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; use SimpleSAML\OpenID\VerifiableCredentials\ClaimsPathPointerResolver; use SimpleSAML\OpenID\VerifiableCredentials\Factories\CredentialOfferFactory; +use SimpleSAML\OpenID\VerifiableCredentials\Factories\OpenId4VciProofFactory; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Factories\JwtVcJsonFactory; class VerifiableCredentials @@ -56,6 +57,8 @@ class VerifiableCredentials protected ?CredentialOfferFactory $credentialOfferFactory = null; + protected ?OpenId4VciProofFactory $openId4VciProofFactory = null; + public function __construct( protected readonly SupportedSerializers $supportedSerializers = new SupportedSerializers(), protected readonly SupportedAlgorithms $supportedAlgorithms = new SupportedAlgorithms(), @@ -162,4 +165,17 @@ public function credentialOfferFactory(): CredentialOfferFactory $this->helpers(), ); } + + public function openId4VciProofFactory(): OpenId4VciProofFactory + { + return $this->openId4VciProofFactory ??= new OpenId4VciProofFactory( + $this->jwsDecoratorBuilder(), + $this->jwsVerifierDecorator(), + $this->jwksDecoratorFactory(), + $this->jwsSerializerManagerDecorator(), + $this->timestampValidationLeewayDecorator, + $this->helpers(), + $this->claimFactory(), + ); + } } diff --git a/src/VerifiableCredentials/Factories/OpenId4VciProofFactory.php b/src/VerifiableCredentials/Factories/OpenId4VciProofFactory.php new file mode 100644 index 0000000..cca265e --- /dev/null +++ b/src/VerifiableCredentials/Factories/OpenId4VciProofFactory.php @@ -0,0 +1,57 @@ +jwsDecoratorBuilder->fromToken($token), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + ); + } + + /** + * @param array $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ): OpenId4VciProof { + $header[ClaimsEnum::Typ->value] = JwtTypesEnum::OpenId4VciProofJwt->value; + + return new OpenId4VciProof( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + ); + } +} diff --git a/src/VerifiableCredentials/OpenId4VciProof.php b/src/VerifiableCredentials/OpenId4VciProof.php new file mode 100644 index 0000000..88d1ca9 --- /dev/null +++ b/src/VerifiableCredentials/OpenId4VciProof.php @@ -0,0 +1,132 @@ +isNone()) { + throw new OpenId4VciProofException('Invalid Algorithm header claim (none).'); + } + + return $alg; + } + + /** + * @return non-empty-string + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\OpenId4VciProofException + * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getType(): string + { + $typ = parent::getType() ?? throw new OpenId4VciProofException('No Type header claim found.'); + + if ($typ !== JwtTypesEnum::OpenId4VciProofJwt->value) { + throw new OpenId4VciProofException('Invalid Type header claim.'); + } + + return $typ; + } + + /** + * @return ?mixed[] + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getJsonWebKey(): ?array + { + $claimKey = ClaimsEnum::Jwk->value; + + $jwk = $this->getHeaderClaim($claimKey); + + return is_null($jwk) ? null : $this->helpers->type()->ensureArray($jwk, $claimKey); + } + + /** + * @return ?non-empty-string[] + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getX509CertificateChain(): ?array + { + $claimKey = ClaimsEnum::X5c->value; + + $x5c = $this->getHeaderClaim($claimKey); + + return is_null($x5c) ? null : $this->helpers->type()->ensureArrayWithValuesAsNonEmptyStrings($x5c, $claimKey); + } + + /** + * @return string[] + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\OpenId4VciProofException + */ + public function getAudience(): array + { + return parent::getAudience() ?? throw new OpenId4VciProofException('No Audience claim found.'); + } + + public function getIssuedAt(): int + { + return parent::getIssuedAt() ?? throw new OpenId4VciProofException('No IssuedAt claim found.'); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + */ + public function getNonce(): ?string + { + $claimKey = ClaimsEnum::Nonce->value; + + $nonce = $this->getHeaderClaim($claimKey); + + return is_null($nonce) ? null : $this->helpers->type()->ensureNonEmptyString($nonce, $claimKey); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException + */ + protected function validate(): void + { + $this->validateByCallbacks( + $this->getAlgorithm(...), + $this->getType(...), + $this->getKeyId(...), + $this->getJsonWebKey(...), + $this->getX509CertificateChain(...), + $this->getIssuer(...), + $this->getAudience(...), + $this->getIssuedAt(...), + // TODO + // key_attestation: OPTIONAL + // trust_chain: OPTIONAL + ); + } +} From 5e62beaa5c1664c7216159dd8efb067ba0317bd6 Mon Sep 17 00:00:00 2001 From: Marko Ivancic Date: Fri, 6 Jun 2025 20:08:22 +0200 Subject: [PATCH 28/66] WIP --- src/Jwks/Factories/JwksDecoratorFactory.php | 2 +- src/Jwks/JwksFetcher.php | 6 +++--- src/Jws/ParsedJws.php | 16 +++++++++++++++- src/VerifiableCredentials/OpenId4VciProof.php | 8 +++++--- .../Jwks/Factories/JwksDecoratorFactoryTest.php | 2 +- tests/src/Jwks/JwksFetcherTest.php | 16 ++++++++-------- 6 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/Jwks/Factories/JwksDecoratorFactory.php b/src/Jwks/Factories/JwksDecoratorFactory.php index 69826c7..f5f5458 100644 --- a/src/Jwks/Factories/JwksDecoratorFactory.php +++ b/src/Jwks/Factories/JwksDecoratorFactory.php @@ -12,7 +12,7 @@ class JwksDecoratorFactory /** * @phpstan-ignore missingType.iterableValue (JWKS array is validated later) */ - public function fromKeyData(array $jwks): JwksDecorator + public function fromKeySetData(array $jwks): JwksDecorator { return new JwksDecorator(JWKSet::createFromKeyData($jwks)); } diff --git a/src/Jwks/JwksFetcher.php b/src/Jwks/JwksFetcher.php index 5bbb120..37d5b33 100644 --- a/src/Jwks/JwksFetcher.php +++ b/src/Jwks/JwksFetcher.php @@ -92,7 +92,7 @@ public function fromCache(string $uri): ?JwksDecorator $this->logger?->debug('JWKS JSON decoded, proceeding to instance building.', ['uri' => $uri, 'jwks' => $jwks]); - return $this->jwksDecoratorFactory->fromKeyData($jwks); + return $this->jwksDecoratorFactory->fromKeySetData($jwks); } /** @@ -152,7 +152,7 @@ public function fromJwksUri(string $uri): ?JwksDecorator $this->logger?->debug('Proceeding to instance building.', ['uri' => $uri, 'jwks' => $jwks]); - return $this->jwksDecoratorFactory->fromKeyData($jwks); + return $this->jwksDecoratorFactory->fromKeySetData($jwks); } /** @@ -216,6 +216,6 @@ public function fromSignedJwksUri(string $uri, array $federationJwks): ?JwksDeco ); } - return $this->jwksDecoratorFactory->fromKeyData($signedJwks->jsonSerialize()); + return $this->jwksDecoratorFactory->fromKeySetData($signedJwks->jsonSerialize()); } } diff --git a/src/Jws/ParsedJws.php b/src/Jws/ParsedJws.php index a191890..bf8c90e 100644 --- a/src/Jws/ParsedJws.php +++ b/src/Jws/ParsedJws.php @@ -147,7 +147,7 @@ public function verifyWithKeySet(array $jwks, int $signatureIndex = 0): void if ( !$this->jwsVerifierDecorator->verifyWithKeySet( $this->jwsDecorator, - $this->jwksDecoratorFactory->fromKeyData($jwks), + $this->jwksDecoratorFactory->fromKeySetData($jwks), $signatureIndex, ) ) { @@ -155,6 +155,20 @@ public function verifyWithKeySet(array $jwks, int $signatureIndex = 0): void } } + /** + * @param mixed[] $key + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * + */ + public function verifyWithKey(array $key): void + { + $this->verifyWithKeySet([ + 'keys' => [ + $key, + ], + ]); + } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException diff --git a/src/VerifiableCredentials/OpenId4VciProof.php b/src/VerifiableCredentials/OpenId4VciProof.php index 88d1ca9..72b42a6 100644 --- a/src/VerifiableCredentials/OpenId4VciProof.php +++ b/src/VerifiableCredentials/OpenId4VciProof.php @@ -109,6 +109,10 @@ public function getNonce(): ?string return is_null($nonce) ? null : $this->helpers->type()->ensureNonEmptyString($nonce, $claimKey); } + // TODO + // key_attestation: OPTIONAL + // trust_chain: OPTIONAL + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException @@ -124,9 +128,7 @@ protected function validate(): void $this->getIssuer(...), $this->getAudience(...), $this->getIssuedAt(...), - // TODO - // key_attestation: OPTIONAL - // trust_chain: OPTIONAL + $this->getExpirationTime(...), ); } } diff --git a/tests/src/Jwks/Factories/JwksDecoratorFactoryTest.php b/tests/src/Jwks/Factories/JwksDecoratorFactoryTest.php index 8e94411..aec23ff 100644 --- a/tests/src/Jwks/Factories/JwksDecoratorFactoryTest.php +++ b/tests/src/Jwks/Factories/JwksDecoratorFactoryTest.php @@ -46,6 +46,6 @@ public function testCanCreateInstance(): void public function testCanBuildFromKeyData(): void { - $this->assertInstanceOf(Jwks\JwksDecorator::class, $this->sut()->fromKeyData($this->jwksArraySample)); + $this->assertInstanceOf(Jwks\JwksDecorator::class, $this->sut()->fromKeySetData($this->jwksArraySample)); } } diff --git a/tests/src/Jwks/JwksFetcherTest.php b/tests/src/Jwks/JwksFetcherTest.php index 666ff51..fa73b1a 100644 --- a/tests/src/Jwks/JwksFetcherTest.php +++ b/tests/src/Jwks/JwksFetcherTest.php @@ -149,7 +149,7 @@ public function testCanGetFromCache(): void ->method('getValue') ->willReturn($this->jwksArraySample); - $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeySetData') ->with($this->jwksArraySample); $this->assertInstanceOf(JwksDecorator::class, $this->sut()->fromCache('uri')); @@ -213,7 +213,7 @@ public function testCanGetFromJwksUri(): void ->method('getValue') ->willReturn($this->jwksArraySample); - $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeySetData') ->with($this->jwksArraySample); $this->cacheDecoratorMock->expects($this->once())->method('set') @@ -274,7 +274,7 @@ public function testJwksUriLogsErrorInCaseOfCacheSetError(): void ->method('getValue') ->willReturn($this->jwksArraySample); - $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeySetData') ->with($this->jwksArraySample); $this->cacheDecoratorMock->expects($this->once())->method('set') @@ -314,7 +314,7 @@ public function testCanGetFromCacheOrJwksUri(): void ->method('getValue') ->willReturn($this->jwksArraySample); - $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeySetData') ->with($this->jwksArraySample); $this->assertInstanceOf(JwksDecorator::class, $this->sut()->fromCacheOrJwksUri('uri')); @@ -340,7 +340,7 @@ public function testCanGetFromSignedJwksUri(): void ->with($this->jwksArraySample) ->willReturn('jwks-json'); - $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeySetData') ->with($this->jwksArraySample); $this->cacheDecoratorMock->expects($this->once())->method('set') @@ -372,7 +372,7 @@ public function testSignedJwksUriTakesExpClaimIntoAccountForCaching(): void ->with($this->jwksArraySample) ->willReturn('jwks-json'); - $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeySetData') ->with($this->jwksArraySample); $this->maxCacheDurationDecoratorMock->expects($this->once()) @@ -417,7 +417,7 @@ public function testSignedJwksUriLogsErrorOnCacheSetError(): void ->with($this->jwksArraySample) ->willReturn('jwks-json'); - $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeySetData') ->with($this->jwksArraySample); $this->cacheDecoratorMock->expects($this->once())->method('set') @@ -453,7 +453,7 @@ public function testCanGetFromCacheOrSignedJwksUri(): void ->with($this->jwksArraySample) ->willReturn('jwks-json'); - $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeyData') + $this->jwksDecoratorFactoryMock->expects($this->once())->method('fromKeySetData') ->with($this->jwksArraySample); $this->cacheDecoratorMock->expects($this->once())->method('set') From e90296c689656540c409f741008b3f9dc7d05b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 30 Jul 2025 11:36:53 +0200 Subject: [PATCH 29/66] WIP SD-JWT --- src/Codebooks/ClaimsEnum.php | 9 + src/Codebooks/ContentTypesEnum.php | 1 + src/Codebooks/HashAlgorithmsEnum.php | 60 +++++ src/Codebooks/JwtTypesEnum.php | 2 + src/Codebooks/SdJwtDisclosureType.php | 11 + src/Exceptions/SdJwtException.php | 11 + src/Federation/MetadataPolicyApplicator.php | 9 +- src/Federation/MetadataPolicyResolver.php | 6 +- src/Helpers.php | 16 ++ src/Helpers/Arr.php | 101 ++++++++- src/Helpers/Hash.php | 20 ++ src/Helpers/Json.php | 2 +- src/Helpers/Random.php | 57 +++++ src/SdJwt/Disclosure.php | 142 ++++++++++++ src/SdJwt/DisclosureBag.php | 50 +++++ src/SdJwt/Factories/DisclosureFactory.php | 79 +++++++ src/SdJwt/Factories/SdJwtFactory.php | 163 ++++++++++++++ src/SdJwt/KbJwt.php | 11 + src/SdJwt/SdJwt.php | 136 ++++++++++++ .../MetadataPolicyApplicatorTest.php | 17 +- .../Federation/MetadataPolicyResolverTest.php | 9 +- tests/src/Helpers/ArrTest.php | 72 ++++++ tests/src/SdJwt/DisclosureTest.php | 127 +++++++++++ .../src/SdJwt/Factories/SdJwtFactoryTest.php | 207 ++++++++++++++++++ 24 files changed, 1294 insertions(+), 24 deletions(-) create mode 100644 src/Codebooks/HashAlgorithmsEnum.php create mode 100644 src/Codebooks/SdJwtDisclosureType.php create mode 100644 src/Exceptions/SdJwtException.php create mode 100644 src/Helpers/Hash.php create mode 100644 src/Helpers/Random.php create mode 100644 src/SdJwt/Disclosure.php create mode 100644 src/SdJwt/DisclosureBag.php create mode 100644 src/SdJwt/Factories/DisclosureFactory.php create mode 100644 src/SdJwt/Factories/SdJwtFactory.php create mode 100644 src/SdJwt/KbJwt.php create mode 100644 src/SdJwt/SdJwt.php create mode 100644 tests/src/SdJwt/DisclosureTest.php create mode 100644 tests/src/SdJwt/Factories/SdJwtFactoryTest.php diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index e6180b0..e0dc9bf 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -6,6 +6,10 @@ enum ClaimsEnum: string { + // _SelectiveDisclosure + case _Sd = '_sd'; + // _SelectiveDisclosureAlgorithm + case _SdAlg = '_sd_alg'; // @context case AtContext = '@context'; // @type @@ -38,6 +42,8 @@ enum ClaimsEnum: string case ClientName = 'client_name'; case ClientRegistrationTypes = 'client_registration_types'; case ClientRegistrationTypesSupported = 'client_registration_types_supported'; + // Confirmation + case Cnf = 'cnf'; case CodeChallengeMethodsSupported = 'code_challenge_methods_supported'; case Contacts = 'contacts'; case CredentialConfigurationId = 'credential_configuration_id'; @@ -59,6 +65,7 @@ enum ClaimsEnum: string case Description = 'description'; case Display = 'display'; case DisplayName = 'display_name'; + case DotDotDot = '...'; case EndSessionEndpoint = 'end_session_endpoint'; case EncryptionRequired = 'encryption_required'; // EncryptionValuesSupported @@ -185,6 +192,8 @@ enum ClaimsEnum: string case UserinfoEndpoint = 'userinfo_endpoint'; // VerifiableCredential case Vc = 'vc'; + // VerifiableCredentialType + case Vct = 'vct'; // X509certificateChain case X5c = 'x5c'; } diff --git a/src/Codebooks/ContentTypesEnum.php b/src/Codebooks/ContentTypesEnum.php index fc3bee6..e55681d 100644 --- a/src/Codebooks/ContentTypesEnum.php +++ b/src/Codebooks/ContentTypesEnum.php @@ -6,6 +6,7 @@ enum ContentTypesEnum: string { + case ApplicationDcSdJwt = 'application/dc+sd-jwt'; case ApplicationJwt = 'application/jwt'; case ApplicationEntityStatementJwt = 'application/entity-statement+jwt'; case ApplicationTrustMarkJwt = 'application/trust-mark+jwt'; diff --git a/src/Codebooks/HashAlgorithmsEnum.php b/src/Codebooks/HashAlgorithmsEnum.php new file mode 100644 index 0000000..e510f9f --- /dev/null +++ b/src/Codebooks/HashAlgorithmsEnum.php @@ -0,0 +1,60 @@ +value; + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + public function phpName(): string + { + return match ($this) { + self::SHA_256 => 'sha256', + self::SHA_384 => 'sha384', + self::SHA_512 => 'sha512', + self::SHA3_224 => 'sha3-224', + self::SHA3_256 => 'sha3-256', + self::SHA3_384 => 'sha3-384', + self::SHA3_512 => 'sha3-512', + self::SHA_256_128, + self::SHA_256_120, + self::SHA_256_96, + self::SHA_256_64, + self::SHA_256_32, + self::BLAKE2S_256, + self::BLAKE2B_256, + self::BLAKE2B_512, + self::K12_256, + self::K12_512 => throw new OpenIdException('Hash algorithm not supported (.' . $this->ianaName() . ').',), + }; + } +} diff --git a/src/Codebooks/JwtTypesEnum.php b/src/Codebooks/JwtTypesEnum.php index 6808cbf..d9b88af 100644 --- a/src/Codebooks/JwtTypesEnum.php +++ b/src/Codebooks/JwtTypesEnum.php @@ -6,7 +6,9 @@ enum JwtTypesEnum: string { + case DcSdJwt = 'dc+sd-jwt'; case EntityStatementJwt = 'entity-statement+jwt'; + case ExampleSdJwt = 'example+sd-jwt'; case JwkSetJwt = 'jwk-set+jwt'; case Jwt = 'JWT'; case OpenId4VciProofJwt = 'openid4vci-proof+jwt'; diff --git a/src/Codebooks/SdJwtDisclosureType.php b/src/Codebooks/SdJwtDisclosureType.php new file mode 100644 index 0000000..fb535fe --- /dev/null +++ b/src/Codebooks/SdJwtDisclosureType.php @@ -0,0 +1,11 @@ +helpers->arr()->ensureArrayDepth($metadata, $policyParameterName); - $metadata[$policyParameterName] = $this->resolveParameterValueAfterPolicy( - $operatorValue, + $this->helpers->arr()->setNestedValue( + $metadata, + $this->resolveParameterValueAfterPolicy( + $operatorValue, + $policyParameterName, + ), $policyParameterName, ); } elseif ($metadataPolicyOperatorEnum === MetadataPolicyOperatorsEnum::Add) { diff --git a/src/Federation/MetadataPolicyResolver.php b/src/Federation/MetadataPolicyResolver.php index 1da7265..2439960 100644 --- a/src/Federation/MetadataPolicyResolver.php +++ b/src/Federation/MetadataPolicyResolver.php @@ -120,14 +120,12 @@ public function for( (!is_array($currentPolicy[$nextPolicyParameter])) || (!array_key_exists($metadataPolicyOperatorEnum->value, $currentPolicy[$nextPolicyParameter])) ) { - $this->helpers->arr()->ensureArrayDepth( + $this->helpers->arr()->setNestedValue( $currentPolicy, + $operatorValue, $nextPolicyParameter, $metadataPolicyOperatorEnum->value, ); - // @phpstan-ignore offsetAccess.nonOffsetAccessible (We ensured this is array.) - $currentPolicy[$nextPolicyParameter][$metadataPolicyOperatorEnum->value] = - $operatorValue; // It exists, so we have to check special cases for merging. } elseif ( diff --git a/src/Helpers.php b/src/Helpers.php index 3f31dca..c3ee6fb 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -7,7 +7,9 @@ use SimpleSAML\OpenID\Helpers\Arr; use SimpleSAML\OpenID\Helpers\Base64Url; use SimpleSAML\OpenID\Helpers\DateTime; +use SimpleSAML\OpenID\Helpers\Hash; use SimpleSAML\OpenID\Helpers\Json; +use SimpleSAML\OpenID\Helpers\Random; use SimpleSAML\OpenID\Helpers\Type; use SimpleSAML\OpenID\Helpers\Url; @@ -25,6 +27,10 @@ class Helpers protected static ?Base64Url $base64Url = null; + protected static ?Hash $hash = null; + + protected static ?Random $random = null; + public function url(): Url { return self::$url ??= new Url(); @@ -54,4 +60,14 @@ public function base64Url(): Base64Url { return self::$base64Url ??= new Base64Url(); } + + public function hash(): Hash + { + return self::$hash ??= new Hash(); + } + + public function random(): Random + { + return self::$random ??= new Random(); + } } diff --git a/src/Helpers/Arr.php b/src/Helpers/Arr.php index 9b6ecf4..866e810 100644 --- a/src/Helpers/Arr.php +++ b/src/Helpers/Arr.php @@ -8,14 +8,34 @@ class Arr { + public const MAX_DEPTH = 99; + /** - * @phpstan-ignore missingType.iterableValue (We can handle mixed type) + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException */ - public function ensureArrayDepth(array &$array, int|string ...$keys): void + public function validateMaxDepth(int $depth): void { - if (count($keys) > 99) { - throw new OpenIdException('Refusing to recurse to given depth.'); + if ($depth > self::MAX_DEPTH) { + throw new OpenIdException( + sprintf( + 'Refusing to recurse to given depth %s. Max depth is %s.', + $depth, + self::MAX_DEPTH, + ), + ); } + } + + /** + * Ensure the existence of nested arrays for given keys. Note that this will create / overwrite any non-array + * nested values and make them an array. + * + * @param mixed[] $array + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + public function ensureArrayDepth(array &$array, int|string ...$keys): void + { + $this->validateMaxDepth(count($keys)); $key = array_shift($keys); @@ -30,6 +50,79 @@ public function ensureArrayDepth(array &$array, int|string ...$keys): void $this->ensureArrayDepth($array[$key], ...$keys); } + /** + * Get nested value reference at a given path. Creates nested arrays dynamically if the key is not present. + * + * @param mixed[] $array + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException If a non-array value exists on the path. + */ + public function &getNestedValueReference(array &$array, int|string ...$keys): mixed + { + $this->validateMaxDepth(count($keys)); + + $nested = &$array; + + foreach ($keys as $key) { + if (!is_array($nested)) { + throw new OpenIdException( + sprintf( + 'Refusing to operate on non-array value for key: %s, path: %s, array: %s.', + $key, + implode('.', $keys), + var_export($array, true), + ), + ); + } + + if (!isset($nested[$key])) { + $nested[$key] = []; + } + + $nested = &$nested[$key]; + } + + return $nested; + } + + /** + * Set a value at a path. + * + * @param mixed[] $array + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + public function setNestedValue(array &$array, mixed $value, int|string ...$keys): void + { + if (count($keys) < 1) { + return; + } + + $reference =& $this->getNestedValueReference($array, ...$keys); + + $reference = $value; + } + + /** + * @param mixed[] $array + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + public function addNestedValue(array &$array, mixed $value, int|string ...$keys): void + { + $reference =& $this->getNestedValueReference($array, ...$keys); + + if (!is_array($reference)) { + throw new OpenIdException( + sprintf( + 'Refusing to add value to non-array value. Array: %s, path: %s, value: %s.', + var_export($array, true), + implode('.', $keys), + var_export($value, true), + ), + ); + } + + $reference[] = $value; + } + /** * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException * @param mixed[] $array diff --git a/src/Helpers/Hash.php b/src/Helpers/Hash.php new file mode 100644 index 0000000..87e1bbe --- /dev/null +++ b/src/Helpers/Hash.php @@ -0,0 +1,20 @@ + 0) { + try { + $random = bin2hex(random_bytes($length)); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + $errors[] = $e->getMessage(); + continue; + } + + // @codeCoverageIgnoreEnd + + if ($blacklist !== null && in_array($random, $blacklist, true)) { + $errors[] = sprintf( + 'Random string %s is in the blacklist [%s], skipping.', + $random, + implode(', ', $blacklist), + ); + continue; + } + + return ($prefix ?? '') . $random . ($suffix ?? ''); + } + + throw new OpenIdException( + 'Could not generate a random string, errors were: ' . implode(', ', $errors) . '.', + ); + } +} diff --git a/src/SdJwt/Disclosure.php b/src/SdJwt/Disclosure.php new file mode 100644 index 0000000..604c621 --- /dev/null +++ b/src/SdJwt/Disclosure.php @@ -0,0 +1,142 @@ + + */ + public const FORBIDDEN_NAMES = [ + ClaimsEnum::_SdAlg->value, + ClaimsEnum::_Sd->value, + ClaimsEnum::DotDotDot->value, + ]; + + /** + * @var non-empty-string|null + */ + protected ?string $encoded = null; + + /** + * @var non-empty-string|null + */ + protected ?string $digest = null; + + /** + * @param array $path + * @throws \SimpleSAML\OpenID\Exceptions\SdJwtException + */ + public function __construct( + protected readonly Helpers $helpers, + protected readonly string $salt, + protected readonly mixed $value, + protected readonly ?string $name = null, + protected readonly array $path = [], + protected readonly HashAlgorithmsEnum $selectiveDisclosureAlgorithm = HashAlgorithmsEnum::SHA_256, + ) { + if ($this->name !== null && in_array($this->name, self::FORBIDDEN_NAMES, true)) { + throw new SdJwtException('Disclosure name cannot be one of the forbidden names.'); + } + + if ($this->name === null && $this->path === []) { + throw new SdJwtException('Disclosure name and path cannot be both empty.'); + } + } + + public function getSalt(): string + { + return $this->salt; + } + + public function getValue(): mixed + { + return $this->value; + } + + public function getName(): ?string + { + return $this->name; + } + + /** + * @return array + */ + public function getPath(): array + { + return $this->path; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + if ($this->name === null) { + return [$this->salt, $this->value]; + } + + return [$this->salt, $this->name, $this->value]; + } + + public function getType(): SdJwtDisclosureType + { + if ($this->name === null) { + return SdJwtDisclosureType::ArrayElement; + } + + return SdJwtDisclosureType::ObjectProperty; + } + + public function getEncoded(): string + { + return $this->encoded ??= $this->helpers->type()->ensureNonEmptyString( + $this->helpers->base64Url()->encode( + $this->helpers->json()->encode( + $this->jsonSerialize(), + ), + ), + ); + } + + /** + * @return non-empty-string + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + public function getDigest(): string + { + return $this->digest ??= $this->helpers->type()->ensureNonEmptyString( + $this->helpers->base64Url()->encode( + $this->helpers->hash()->for( + $this->selectiveDisclosureAlgorithm->phpName(), + $this->getEncoded(), + true, + ), + ), + ); + } + + /** + * @return non-empty-string|array{"...": non-empty-string} + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + public function getDigestRepresentation(): string|array + { + if ($this->getType() === SdJwtDisclosureType::ArrayElement) { + return [ + ClaimsEnum::DotDotDot->value => $this->getDigest(), + ]; + } + + return $this->getDigest(); + } +} diff --git a/src/SdJwt/DisclosureBag.php b/src/SdJwt/DisclosureBag.php new file mode 100644 index 0000000..f7c8ee0 --- /dev/null +++ b/src/SdJwt/DisclosureBag.php @@ -0,0 +1,50 @@ +disclosures = $disclosures; + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\SdJwtException + */ + public function add(Disclosure ...$disclosures): void + { + foreach ($disclosures as $disclosure) { + if (array_key_exists($disclosure->getSalt(), $this->disclosures)) { + throw new SdJwtException('Disclosure with the same salt already exists in the bag.'); + } + + $this->disclosures[$disclosure->getSalt()] = $disclosure; + } + } + + /** + * @return \SimpleSAML\OpenID\SdJwt\Disclosure[] + */ + public function all(): array + { + return $this->disclosures; + } + + /** + * @return string[] + */ + public function salts(): array + { + return array_keys($this->disclosures); + } +} diff --git a/src/SdJwt/Factories/DisclosureFactory.php b/src/SdJwt/Factories/DisclosureFactory.php new file mode 100644 index 0000000..0aa9b6b --- /dev/null +++ b/src/SdJwt/Factories/DisclosureFactory.php @@ -0,0 +1,79 @@ + $path + * @param string[] $saltBlacklist + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + public function build( + mixed $value, + ?string $name = null, + ?string $salt = null, + array $path = [], + HashAlgorithmsEnum $selectiveDisclosureAlgorithm = HashAlgorithmsEnum::SHA_256, + array $saltBlacklist = [], + ): Disclosure { + + $salt ??= $this->helpers->random()->string( + blacklist: $saltBlacklist, + ); + + return new Disclosure( + $this->helpers, + $salt, + $value, + $name, + $path, + $selectiveDisclosureAlgorithm, + ); + } + + /** + * @param array $path + * @param string[] $saltBlacklist + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + public function buildDecoy( + SdJwtDisclosureType $sdJwtDisclosureType, + array $path, + HashAlgorithmsEnum $selectiveDisclosureAlgorithm = HashAlgorithmsEnum::SHA_256, + array $saltBlacklist = [], + ): Disclosure { + $salt = $this->helpers->random()->string(blacklist: $saltBlacklist); + $value = $this->helpers->random()->string(); + $name = $this->helpers->random()->string(); + + if ($sdJwtDisclosureType === SdJwtDisclosureType::ArrayElement) { + $name = null; + if ($path === []) { + $path = [ + $this->helpers->random()->string(), + ]; + } + } + + return $this->build( + $value, + $name, + $salt, + $path, + $selectiveDisclosureAlgorithm, + ); + } +} diff --git a/src/SdJwt/Factories/SdJwtFactory.php b/src/SdJwt/Factories/SdJwtFactory.php new file mode 100644 index 0000000..91bb809 --- /dev/null +++ b/src/SdJwt/Factories/SdJwtFactory.php @@ -0,0 +1,163 @@ + $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ?DisclosureBag $disclosureBag = null, + ?KbJwt $kbJwt = null, + JwtTypesEnum $jwtTypesEnum = JwtTypesEnum::ExampleSdJwt, + HashAlgorithmsEnum $hashAlgorithmsEnum = HashAlgorithmsEnum::SHA_256, + ): SdJwt { + $header[ClaimsEnum::Typ->value] = $jwtTypesEnum->value; + + if ($disclosureBag instanceof DisclosureBag) { + $payload = $this->updatePayloadWithDisclosures($payload, $disclosureBag, $hashAlgorithmsEnum); + } + + /** @var array $payload */ + + return new SdJwt( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + $disclosureBag, + $kbJwt, + ); + } + + /** + * @param array $payload + * @return array + * @throws \SimpleSAML\OpenID\Exceptions\SdJwtException + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + public function updatePayloadWithDisclosures( + array $payload, + DisclosureBag $disclosureBag, + HashAlgorithmsEnum $hashAlgorithmsEnum, + int $minDecoys = 1, + int $maxDecoys = 5, + ): array { + $disclosures = $disclosureBag->all(); + + if ($disclosures === []) { + return $payload; + } + + $payload[ClaimsEnum::_SdAlg->value] = $hashAlgorithmsEnum->ianaName(); + + $disclosurePaths = []; + + foreach ($disclosures as $disclosure) { + $disclosurePath = $disclosure->getPath(); + + $disclosureType = $disclosure->getType(); + + if ($disclosureType === SdJwtDisclosureType::ObjectProperty) { + $disclosurePath = [...$disclosure->getPath(), ClaimsEnum::_Sd->value]; + } + + if (!in_array($disclosurePath, $disclosurePaths, true)) { + $disclosurePaths[] = $disclosurePath; + } + + $this->helpers->arr()->addNestedValue( + $payload, + $disclosure->getDigestRepresentation(), + ...$disclosurePath, + ); + + // Randomly add decoys. + if (random_int(0, 1) !== 0) { + $disclosurePathReference =& $this->helpers->arr()->getNestedValueReference( + $payload, + ...$disclosurePath, + ); + + if (is_array($disclosurePathReference)) { + $disclosureDecoy = $this->disclosureFactory->buildDecoy( + $disclosureType, + $disclosurePath, + $hashAlgorithmsEnum, + $disclosureBag->salts(), + ); + + // Make sure that we never add duplicates. + if (!in_array($disclosureDecoy->getDigestRepresentation(), $disclosurePathReference, true)) { + $disclosurePathReference[] = $disclosureDecoy->getDigestRepresentation(); + } + + if ($disclosureType === SdJwtDisclosureType::ObjectProperty) { + shuffle($disclosurePathReference); + } + } + } + } + + /** @var array $payload */ + + return $payload; + } +} diff --git a/src/SdJwt/KbJwt.php b/src/SdJwt/KbJwt.php new file mode 100644 index 0000000..1de7bb2 --- /dev/null +++ b/src/SdJwt/KbJwt.php @@ -0,0 +1,11 @@ +value; + + $_sdAlg = $this->getPayloadClaim($claimKey); + + return is_null($_sdAlg) ? + null : + HashAlgorithmsEnum::from($this->helpers->type()->ensureNonEmptyString($_sdAlg, $claimKey)); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @return ?mixed[] + */ + public function getConfirmation(): ?array + { + $claimKey = ClaimsEnum::Cnf->value; + + $cnf = $this->getPayloadClaim($claimKey); + + return is_null($cnf) ? + null : + $this->helpers->type()->ensureArray($cnf, $claimKey); + } + + public function getDisclosureBag(): ?DisclosureBag + { + return $this->disclosureBag; + } + + public function getKbJwt(): ?KbJwt + { + return $this->kbJwt; + } + + /** + * @throws \JsonException + */ + public function getDisclosedToken( + JwsSerializerEnum $jwsSerializerEnum = JwsSerializerEnum::Compact, + ?int $signatureIndex = null, + ?DisclosureBag $disclosureBag = null, + ): string { + $token = $this->getToken($jwsSerializerEnum, $signatureIndex) . self::TILDE; + + $disclosures = $this->disclosureBag?->all() ?? []; + + if ($disclosureBag instanceof DisclosureBag) { + $disclosures = array_filter( + $disclosureBag->all(), + fn (Disclosure $disclosure): bool => array_key_exists($disclosure->getSalt(), $disclosures), + ); + } + + foreach ($disclosures as $disclosure) { + $token .= $this->helpers->base64Url()->encode( + $this->helpers->json()->encode($disclosure->jsonSerialize()), + ) . self::TILDE; + } + + if ($this->kbJwt instanceof KbJwt) { + $token .= $this->kbJwt->getToken($jwsSerializerEnum, $signatureIndex); + } + + return $token; + } + + + + + /** + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + protected function validate(): void + { + $this->validateByCallbacks( + $this->getSelectiveDisclosureAlgorithm(...), + ); + } +} diff --git a/tests/src/Federation/MetadataPolicyApplicatorTest.php b/tests/src/Federation/MetadataPolicyApplicatorTest.php index 645a89e..e60a179 100644 --- a/tests/src/Federation/MetadataPolicyApplicatorTest.php +++ b/tests/src/Federation/MetadataPolicyApplicatorTest.php @@ -6,7 +6,6 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use SimpleSAML\OpenID\Codebooks\MetadataPolicyOperatorsEnum; use SimpleSAML\OpenID\Exceptions\MetadataPolicyException; @@ -15,9 +14,11 @@ #[CoversClass(MetadataPolicyApplicator::class)] #[UsesClass(MetadataPolicyOperatorsEnum::class)] +#[UsesClass(Helpers::class)] +#[UsesClass(Helpers\Arr::class)] final class MetadataPolicyApplicatorTest extends TestCase { - protected MockObject $helpersMock; + protected Helpers $helpers; protected array $metadataPolicySample = [ 'grant_types' => [ @@ -69,19 +70,19 @@ final class MetadataPolicyApplicatorTest extends TestCase ], ]; + protected function setUp(): void + { + $this->helpers = new Helpers(); + } + protected function sut( ?Helpers $helpers = null, ): MetadataPolicyApplicator { - $helpers ??= $this->helpersMock; + $helpers ??= $this->helpers; return new MetadataPolicyApplicator($helpers); } - protected function setUp(): void - { - $this->helpersMock = $this->createMock(Helpers::class); - } - public function testCanCreateInstance(): void { $this->assertInstanceOf(MetadataPolicyApplicator::class, $this->sut()); diff --git a/tests/src/Federation/MetadataPolicyResolverTest.php b/tests/src/Federation/MetadataPolicyResolverTest.php index 509399b..701cd2f 100644 --- a/tests/src/Federation/MetadataPolicyResolverTest.php +++ b/tests/src/Federation/MetadataPolicyResolverTest.php @@ -6,7 +6,6 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use SimpleSAML\OpenID\Codebooks\EntityTypesEnum; use SimpleSAML\OpenID\Codebooks\MetadataPolicyOperatorsEnum; @@ -16,9 +15,11 @@ #[CoversClass(MetadataPolicyResolver::class)] #[UsesClass(MetadataPolicyOperatorsEnum::class)] +#[UsesClass(Helpers::class)] +#[UsesClass(Helpers\Arr::class)] final class MetadataPolicyResolverTest extends TestCase { - protected MockObject $helpersMock; + protected Helpers $helpers; protected array $trustAnchorMetadataPolicySample = [ 'openid_relying_party' => [ @@ -62,13 +63,13 @@ final class MetadataPolicyResolverTest extends TestCase protected function setUp(): void { - $this->helpersMock = $this->createMock(Helpers::class); + $this->helpers = new Helpers(); } protected function sut( ?Helpers $helpers = null, ): MetadataPolicyResolver { - $helpers ??= $this->helpersMock; + $helpers ??= $this->helpers; return new MetadataPolicyResolver($helpers); } diff --git a/tests/src/Helpers/ArrTest.php b/tests/src/Helpers/ArrTest.php index badae3f..53d3b7d 100644 --- a/tests/src/Helpers/ArrTest.php +++ b/tests/src/Helpers/ArrTest.php @@ -80,4 +80,76 @@ public function testGetNestedValueThrowsIfTooDeep(): void $arr = []; $this->sut()->getNestedValue($arr, ...range(0, 100)); } + + public function testCanGetNestedValueReference(): void + { + $arr = []; + $this->sut()->getNestedValueReference($arr, 'a', 'b', 'c'); + $this->assertIsArray($arr['a']['b']); + $this->assertIsArray($arr['a']['b']['c']); + + $arr = ['a' => ['b' => 'c']]; + $reference = $this->sut()->getNestedValueReference($arr, 'a', 'b'); + $this->assertSame('c', $reference); + } + + public function testGetNestedValueThrowsForNonArrayPathElements(): void + { + $this->expectException(OpenIDException::class); + $this->expectExceptionMessage('non-array'); + + $arr = ['a' => ['b' => 'c']]; + $this->sut()->getNestedValueReference($arr, 'a', 'b', 'c'); + } + + public function testCanSetNestedValue(): void + { + $arr = []; + $this->sut()->setNestedValue($arr, 'b'); + $this->assertSame([], $arr); + + $arr = []; + $this->sut()->setNestedValue($arr, 'b', 'a'); + $this->assertSame(['a' => 'b'], $arr); + + $arr = []; + $this->sut()->setNestedValue($arr, 'c', 'a', 'b'); + $this->assertSame(['a' => ['b' => 'c']], $arr); + } + + public function testCanAddNestedValue(): void + { + $arr = []; + $this->sut()->addNestedValue($arr, 'b'); + $this->assertSame(['b'], $arr); + + $arr = []; + $this->sut()->addNestedValue($arr, 'b', 'a'); + $this->assertSame(['a' => ['b']], $arr); + + $arr = ['a' => []]; + $this->sut()->addNestedValue($arr, 'b', 'a'); + $this->assertSame(['a' => ['b']], $arr); + + $arr = ['a' => ['b']]; + $this->sut()->addNestedValue($arr, 'c', 'a'); + $this->assertSame(['a' => ['b', 'c']], $arr); + + $arr = ['a' => ['b']]; + $this->sut()->addNestedValue($arr, 'c', 'a', 'b'); + $this->assertSame(['a' => ['b', 'b' => ['c']]], $arr); + + $arr = ['a' => ['b']]; + $this->sut()->addNestedValue($arr, ['c'], 'a', 'b'); + $this->assertSame(['a' => ['b', 'b' => [['c']]]], $arr); + } + + public function testAddNestedValueThrowsForNonArrayPathElements(): void + { + $this->expectException(OpenIDException::class); + $this->expectExceptionMessage('non-array'); + + $arr = ['a' => 'b']; + $this->sut()->addNestedValue($arr, 'c', 'a'); + } } diff --git a/tests/src/SdJwt/DisclosureTest.php b/tests/src/SdJwt/DisclosureTest.php new file mode 100644 index 0000000..271a46d --- /dev/null +++ b/tests/src/SdJwt/DisclosureTest.php @@ -0,0 +1,127 @@ +helpers = new Helpers(); + $this->salt = 'salt'; + $this->value = 'value'; + $this->name = 'name'; + $this->path = ['path']; + $this->selectiveDisclosureAlgorithm = HashAlgorithmsEnum::SHA_256; + } + + protected function sut( + ?Helpers $helpers = null, + ?string $salt = null, + mixed $value = null, + false|null|string $name = null, + ?array $path = null, + ?HashAlgorithmsEnum $selectiveDisclosureAlgorithm = null, + ): Disclosure { + $helpers ??= $this->helpers; + $salt ??= $this->salt; + $value ??= $this->value; + $name = $name === false ? null : $name ?? $this->name; + $path ??= $this->path; + $selectiveDisclosureAlgorithm ??= $this->selectiveDisclosureAlgorithm; + + return new Disclosure( + $helpers, + $salt, + $value, + $name, + $path, + $selectiveDisclosureAlgorithm, + ); + } + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(Disclosure::class, $this->sut()); + } + + public function testThrowsForInvalidName(): void + { + $this->expectException(SdJwtException::class); + $this->expectExceptionMessage('forbidden name'); + + $this->sut( + name: ClaimsEnum::_Sd->value, + ); + } + + public function testThrowsForEmptyNameAndPath(): void + { + $this->expectException(SdJwtException::class); + $this->expectExceptionMessage('name and path'); + + $this->sut( + name: false, + path: [], + ); + } + + public function testCanGetCommonProperties(): void + { + $sut = $this->sut(); + + $this->assertSame($this->salt, $sut->getSalt()); + $this->assertSame($this->value, $sut->getValue()); + $this->assertSame($this->name, $sut->getName()); + $this->assertSame($this->path, $sut->getPath()); + } + + public function testHasProperSerialization(): void + { + $this->assertSame( + [$this->salt, $this->name, $this->value], + $this->sut()->jsonSerialize(), + ); + + $this->assertSame( + [$this->salt, $this->value], + $this->sut(name: false)->jsonSerialize(), + ); + } + + public function testHasProperType(): void + { + $this->assertSame( + SdJwtDisclosureType::ObjectProperty, + $this->sut()->getType(), + ); + + $this->assertSame( + SdJwtDisclosureType::ArrayElement, + $this->sut(name: false)->getType(), + ); + } +} diff --git a/tests/src/SdJwt/Factories/SdJwtFactoryTest.php b/tests/src/SdJwt/Factories/SdJwtFactoryTest.php new file mode 100644 index 0000000..5c93f38 --- /dev/null +++ b/tests/src/SdJwt/Factories/SdJwtFactoryTest.php @@ -0,0 +1,207 @@ + 'ES256', + 'typ' => 'example+sd-jwt', + 'kid' => 'F4VFObNusj3PHmrHxpqh4GNiuFHlfh-2s6xMJ95fLYA', + ]; + + protected array $expiredPayload = [ + 'iat' => 1734009487, + 'nbf' => 1734009487, + 'exp' => 1734009487, + 'iss' => 'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ALeaf/', + + ]; + + protected array $validPayload; + + protected function setUp(): void + { + $signatureMock = $this->createMock(Signature::class); + + $jwsMock = $this->createMock(JWS::class); + $jwsMock->method('getPayload') + ->willReturn('json-payload-string'); // Just so we have non-empty value. + $jwsMock->method('getSignature')->willReturn($signatureMock); + + $jwsDecoratorMock = $this->createMock(JwsDecorator::class); + $jwsDecoratorMock->method('jws')->willReturn($jwsMock); + + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + + $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); + $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); + $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); + + $this->helpersMock = $this->createMock(Helpers::class); + $jsonHelperMock = $this->createMock(Helpers\Json::class); + $this->helpersMock->method('json')->willReturn($jsonHelperMock); + $typeHelperMock = $this->createMock(Helpers\Type::class); + $this->helpersMock->method('type')->willReturn($typeHelperMock); + + $typeHelperMock->method('ensureNonEmptyString')->willReturnArgument(0); + $typeHelperMock->method('ensureInt')->willReturnArgument(0); + + $this->claimFactoryMock = $this->createMock(ClaimFactory::class); + + $this->validPayload = $this->expiredPayload; + $this->validPayload['exp'] = time() + 3600; + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); + $this->disclosureBagMock = $this->createMock(DisclosureBag::class); + + $this->disclosureFactoryMock = $this->createMock(DisclosureFactory::class); + } + + protected function sut( + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, + ?JwsVerifierDecorator $jwsVerifierDecorator = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, + ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, + ?DateIntervalDecorator $dateIntervalDecorator = null, + ?Helpers $helpers = null, + ?ClaimFactory $claimFactory = null, + ?DisclosureFactory $disclosureFactory = null, + ): SdJwtFactory { + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; + $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; + $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; + $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; + $helpers ??= $this->helpersMock; + $claimFactory ??= $this->claimFactoryMock; + $disclosureFactory ??= $this->disclosureFactoryMock; + + return new SdJwtFactory( + $jwsDecoratorBuilder, + $jwsVerifierDecorator, + $jwksDecoratorFactory, + $jwsSerializerManagerDecorator, + $dateIntervalDecorator, + $helpers, + $claimFactory, + $disclosureFactory, + ); + } + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(SdJwtFactory::class, $this->sut()); + } + + public function testCanBuildFromData(): void + { + $this->assertInstanceOf( + SdJwt::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::ES256, + $this->validPayload, + $this->sampleHeader, + $this->disclosureBagMock, + ), + ); + } + + public function testCanUpdatePayloadWithDisclosures(): void + { + // ["_26bc4LT-ac6q2KI6cBW5es","family_name","Möbius"] + $helpers = new Helpers(); + $disclosureFactory = new DisclosureFactory($helpers); + + $disclosure = $disclosureFactory->build( + value: 'Möbius', + name: 'family_name', + salt: '_26bc4LT-ac6q2KI6cBW5es', + ); + + $disclosureBag = new DisclosureBag($disclosure); + + $sut = $this->sut(helpers: $helpers, disclosureFactory: $disclosureFactory); + + $payload = [ + 'given_name' => 'John', + ]; + + $payload = $sut->updatePayloadWithDisclosures( + $payload, + $disclosureBag, + HashAlgorithmsEnum::SHA_256, + ); + + $this->assertArrayHasKey(ClaimsEnum::_SdAlg->value, $payload); + $this->assertArrayHasKey(ClaimsEnum::_Sd->value, $payload); + $this->assertNotEmpty($payload[ClaimsEnum::_Sd->value]); + $this->assertContains('TZjouOTrBKEwUNjNDs9yeMzBoQn8FFLPaJjRRmAtwrM', $payload[ClaimsEnum::_Sd->value]); + } +} From 87a93304032c53eb998deaecd88d1021ae2e39c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 4 Aug 2025 11:51:40 +0200 Subject: [PATCH 30/66] WIP --- src/Codebooks/ParamsEnum.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Codebooks/ParamsEnum.php b/src/Codebooks/ParamsEnum.php index 41710c8..3821b9a 100644 --- a/src/Codebooks/ParamsEnum.php +++ b/src/Codebooks/ParamsEnum.php @@ -8,6 +8,7 @@ enum ParamsEnum: string { case AcrValues = 'acr_values'; case Assertion = 'assertion'; + case AuthorizationDetails = 'authorization_details'; case Claims = 'claims'; case ClientAssertion = 'client_assertion'; case ClientAssertionType = 'client_assertion_type'; @@ -27,6 +28,7 @@ enum ParamsEnum: string case MaxAge = 'max_age'; case Nonce = 'nonce'; case PostLogoutRedirectUri = 'post_logout_redirect_uri'; + case PreAuthorizedCode = 'pre-authorized_code'; case Prompt = 'prompt'; case RedirectUri = 'redirect_uri'; case Request = 'request'; From 737eac1b2594aea45950beb0837ca2d115f38fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 4 Aug 2025 15:26:05 +0200 Subject: [PATCH 31/66] WIP --- src/Codebooks/ClaimsEnum.php | 1 + src/Codebooks/JwtTypesEnum.php | 1 + src/Exceptions/SdJwtVcException.php | 9 ++ src/Helpers/Arr.php | 20 +++ src/VerifiableCredentials.php | 27 ++++ .../SdJwtVc/Factories/SdJwtVcFactory.php | 60 +++++++++ src/VerifiableCredentials/SdJwtVc/SdJwtVc.php | 117 ++++++++++++++++++ 7 files changed, 235 insertions(+) create mode 100644 src/Exceptions/SdJwtVcException.php create mode 100644 src/VerifiableCredentials/SdJwtVc/Factories/SdJwtVcFactory.php create mode 100644 src/VerifiableCredentials/SdJwtVc/SdJwtVc.php diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index e0dc9bf..880fd51 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -165,6 +165,7 @@ enum ClaimsEnum: string case ServiceDocumentation = 'service_documentation'; case SignedJwksUri = 'signed_jwks_uri'; case SignedMetadata = 'signed_metadata'; + case Status = 'status'; // Subject case Sub = 'sub'; case SubjectTypesSupported = 'subject_types_supported'; diff --git a/src/Codebooks/JwtTypesEnum.php b/src/Codebooks/JwtTypesEnum.php index d9b88af..0f89e67 100644 --- a/src/Codebooks/JwtTypesEnum.php +++ b/src/Codebooks/JwtTypesEnum.php @@ -14,4 +14,5 @@ enum JwtTypesEnum: string case OpenId4VciProofJwt = 'openid4vci-proof+jwt'; case TrustMarkJwt = 'trust-mark+jwt'; case TrustMarkDelegationJwt = 'trust-mark-delegation+jwt'; + case VcSdJwt = 'vc+sd-jwt'; } diff --git a/src/Exceptions/SdJwtVcException.php b/src/Exceptions/SdJwtVcException.php new file mode 100644 index 0000000..a8668ee --- /dev/null +++ b/src/Exceptions/SdJwtVcException.php @@ -0,0 +1,9 @@ + $value) { + if ($currentKey === $key) { + return true; + } + + if (is_array($value) && $this->containsKey($value, $key)) { + return true; + } + } + + return false; + } } diff --git a/src/VerifiableCredentials.php b/src/VerifiableCredentials.php index 5a96526..66e18e7 100644 --- a/src/VerifiableCredentials.php +++ b/src/VerifiableCredentials.php @@ -17,10 +17,12 @@ use SimpleSAML\OpenID\Jws\Factories\JwsVerifierDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; +use SimpleSAML\OpenID\SdJwt\Factories\DisclosureFactory; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; use SimpleSAML\OpenID\VerifiableCredentials\ClaimsPathPointerResolver; use SimpleSAML\OpenID\VerifiableCredentials\Factories\CredentialOfferFactory; use SimpleSAML\OpenID\VerifiableCredentials\Factories\OpenId4VciProofFactory; +use SimpleSAML\OpenID\VerifiableCredentials\SdJwtVc\Factories\SdJwtVcFactory; use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Factories\JwtVcJsonFactory; class VerifiableCredentials @@ -59,6 +61,10 @@ class VerifiableCredentials protected ?OpenId4VciProofFactory $openId4VciProofFactory = null; + protected ?DisclosureFactory $disclosureFactory = null; + + protected ?SdJwtVcFactory $sdJwtVcFactory = null; + public function __construct( protected readonly SupportedSerializers $supportedSerializers = new SupportedSerializers(), protected readonly SupportedAlgorithms $supportedAlgorithms = new SupportedAlgorithms(), @@ -178,4 +184,25 @@ public function openId4VciProofFactory(): OpenId4VciProofFactory $this->claimFactory(), ); } + + public function disclosureFactory(): DisclosureFactory + { + return $this->disclosureFactory ??= new DisclosureFactory( + $this->helpers(), + ); + } + + public function sdJwtVcFactory(): SdJwtVcFactory + { + return $this->sdJwtVcFactory ??= new SdJwtVcFactory( + $this->jwsDecoratorBuilder(), + $this->jwsVerifierDecorator(), + $this->jwksDecoratorFactory(), + $this->jwsSerializerManagerDecorator(), + $this->timestampValidationLeewayDecorator, + $this->helpers(), + $this->claimFactory(), + $this->disclosureFactory(), + ); + } } diff --git a/src/VerifiableCredentials/SdJwtVc/Factories/SdJwtVcFactory.php b/src/VerifiableCredentials/SdJwtVc/Factories/SdJwtVcFactory.php new file mode 100644 index 0000000..4c57bf7 --- /dev/null +++ b/src/VerifiableCredentials/SdJwtVc/Factories/SdJwtVcFactory.php @@ -0,0 +1,60 @@ + $payload + * @param array $header + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException + */ + public function fromData( + JwkDecorator $signingKey, + SignatureAlgorithmEnum $signatureAlgorithm, + array $payload, + array $header, + ?DisclosureBag $disclosureBag = null, + ?KbJwt $kbJwt = null, + JwtTypesEnum $jwtTypesEnum = JwtTypesEnum::DcSdJwt, + HashAlgorithmsEnum $hashAlgorithmsEnum = HashAlgorithmsEnum::SHA_256, + ): SdJwtVc { + $header[ClaimsEnum::Typ->value] = $jwtTypesEnum->value; + + if ($disclosureBag instanceof DisclosureBag) { + $payload = $this->updatePayloadWithDisclosures($payload, $disclosureBag, $hashAlgorithmsEnum); + } + + /** @var array $payload */ + + return new SdJwtVc( + $this->jwsDecoratorBuilder->fromData( + $signingKey, + $signatureAlgorithm, + $payload, + $header, + ), + $this->jwsVerifierDecorator, + $this->jwksDecoratorFactory, + $this->jwsSerializerManagerDecorator, + $this->timestampValidationLeeway, + $this->helpers, + $this->claimFactory, + $disclosureBag, + $kbJwt, + ); + } +} diff --git a/src/VerifiableCredentials/SdJwtVc/SdJwtVc.php b/src/VerifiableCredentials/SdJwtVc/SdJwtVc.php new file mode 100644 index 0000000..a665710 --- /dev/null +++ b/src/VerifiableCredentials/SdJwtVc/SdJwtVc.php @@ -0,0 +1,117 @@ +value, + ClaimsEnum::Nbf->value, + ClaimsEnum::Exp->value, + ClaimsEnum::Cnf->value, + ClaimsEnum::Vct->value, + ClaimsEnum::Status->value, + ]; + + public function getType(): string + { + $typ = parent::getType() ?? throw new SdJwtVcException('No Type header claim found.'); + + if (!in_array($typ, [JwtTypesEnum::DcSdJwt->value, JwtTypesEnum::VcSdJwt->value], true)) { + throw new EntityStatementException('Invalid Type header claim.'); + } + + return $typ; + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException + * @throws \SimpleSAML\OpenID\Exceptions\SdJwtVcException + */ + public function getVerifiableCredentialType(): string + { + $claimKey = ClaimsEnum::Vct->value; + + ($vct = $this->getPayloadClaim($claimKey)) ?? throw new SdJwtVcException( + 'No Verifiable Credential Type claim found.', + ); + + return $this->helpers->type()->ensureNonEmptyString($vct, $claimKey); + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\SdJwtVcException + */ + protected function ensureNonSelectivelyDisclosableClaims(): void + { + if (! $this->disclosureBag instanceof DisclosureBag) { + return; + } + + $disclosureNames = array_filter(array_map( + fn (Disclosure $disclosure): ?string => $disclosure->getName(), + $this->disclosureBag->all(), + )); + + $nonDisclosableClaims = array_intersect($disclosureNames, self::NON_SELECTIVELY_DISCLOSABLE_CLAIMS); + + if ($nonDisclosableClaims !== []) { + throw new SdJwtVcException( + sprintf( + 'The following claims are not selectively disclosable: %s', + implode(', ', $nonDisclosableClaims), + ), + ); + } + } + + /** + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + * @throws \SimpleSAML\OpenID\Exceptions\SdJwtVcException + */ + protected function ensureNoSdClaimWhenNoDisclosures(): void + { + if ( + $this->disclosureBag instanceof DisclosureBag && + ($this->disclosureBag->all() !== []) + ) { + return; + } + + if ( + $this->helpers->arr()->containsKey( + $this->getPayload(), + ClaimsEnum::_Sd->value, + ) + ) { + throw new SdJwtVcException( + 'The _Sd claim is not allowed when no disclosures are specified.', + ); + } + } + + protected function validate(): void + { + $this->validateByCallbacks( + $this->getType(...), + $this->getVerifiableCredentialType(...), + $this->ensureNonSelectivelyDisclosableClaims(...), + $this->ensureNoSdClaimWhenNoDisclosures(...), + ); + } +} From 4e56d8b3429b89381a8ed2e2d945c64b27f0a12e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 4 Aug 2025 16:19:24 +0200 Subject: [PATCH 32/66] WIP --- src/Codebooks/CredentialFormatIdentifiersEnum.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Codebooks/CredentialFormatIdentifiersEnum.php b/src/Codebooks/CredentialFormatIdentifiersEnum.php index fbb1809..d4ba0bd 100644 --- a/src/Codebooks/CredentialFormatIdentifiersEnum.php +++ b/src/Codebooks/CredentialFormatIdentifiersEnum.php @@ -20,4 +20,6 @@ enum CredentialFormatIdentifiersEnum: string // IETF SD-JWT VC case DcSdJwt = 'dc+sd-jwt'; + // Deprecated identifier for IETF SD-JWT VC. Use 'dc+sd-jwt' instead. + case VcSdJwt = 'vc+sd-jwt'; } From 4bcdc199dd6cba405d16199f67dea7d98fa6b565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 4 Aug 2025 17:03:24 +0200 Subject: [PATCH 33/66] WIP --- src/SdJwt/Factories/DisclosureBagFactory.php | 16 ++++++++++++++++ src/VerifiableCredentials.php | 8 ++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/SdJwt/Factories/DisclosureBagFactory.php diff --git a/src/SdJwt/Factories/DisclosureBagFactory.php b/src/SdJwt/Factories/DisclosureBagFactory.php new file mode 100644 index 0000000..0a428b7 --- /dev/null +++ b/src/SdJwt/Factories/DisclosureBagFactory.php @@ -0,0 +1,16 @@ +disclosureBagFactory ??= new DisclosureBagFactory(); + } + public function sdJwtVcFactory(): SdJwtVcFactory { return $this->sdJwtVcFactory ??= new SdJwtVcFactory( From a4b991eee21d3c889f2e043dc08a48f2cde46bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 6 Aug 2025 09:29:45 +0200 Subject: [PATCH 34/66] WIP --- src/SdJwt/SdJwt.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/SdJwt/SdJwt.php b/src/SdJwt/SdJwt.php index 314360b..b39d27a 100644 --- a/src/SdJwt/SdJwt.php +++ b/src/SdJwt/SdJwt.php @@ -87,16 +87,12 @@ public function getKbJwt(): ?KbJwt return $this->kbJwt; } - /** - * @throws \JsonException - */ - public function getDisclosedToken( + public function getToken( JwsSerializerEnum $jwsSerializerEnum = JwsSerializerEnum::Compact, ?int $signatureIndex = null, ?DisclosureBag $disclosureBag = null, ): string { - $token = $this->getToken($jwsSerializerEnum, $signatureIndex) . self::TILDE; - + $token = parent::getToken($jwsSerializerEnum, $signatureIndex) . self::TILDE; $disclosures = $this->disclosureBag?->all() ?? []; if ($disclosureBag instanceof DisclosureBag) { @@ -119,8 +115,14 @@ public function getDisclosedToken( return $token; } - - + /** + */ + public function getUndisclosedToken( + JwsSerializerEnum $jwsSerializerEnum = JwsSerializerEnum::Compact, + ?int $signatureIndex = null, + ): string { + return parent::getToken($jwsSerializerEnum, $signatureIndex); + } /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException From eadc2169605284c2759ee1c608944995adcbd475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 11 Aug 2025 16:35:24 +0200 Subject: [PATCH 35/66] WIP --- src/Helpers/Arr.php | 27 ++++++++++++++++++++++++++- src/SdJwt/SdJwt.php | 2 -- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Helpers/Arr.php b/src/Helpers/Arr.php index 3b5c64b..326aaf9 100644 --- a/src/Helpers/Arr.php +++ b/src/Helpers/Arr.php @@ -160,7 +160,7 @@ public function isAssociative(array $array): bool } /** - * Is array of arrays. + * Is an array of arrays. * @param mixed[] $array */ public function isOfArrays(array $array): bool @@ -193,4 +193,29 @@ public function containsKey(array $array, string|int $key): bool return false; } + + /** + * Recursively sort an array by keys if keys are strings and values if keys are numeric. + * + * @param mixed[] $array + */ + public function hybridSort(array &$array): void + { + // Determine if keys are numeric or string + $allNumeric = array_keys($array) === array_keys(array_values($array)); + + // Sort appropriately + if ($allNumeric) { + sort($array); // numeric indexes: sort by value + } else { + ksort($array); // string keys: sort by key + } + + // Recurse into nested arrays + foreach ($array as &$value) { + if (is_array($value)) { + $this->hybridSort($value); + } + } + } } diff --git a/src/SdJwt/SdJwt.php b/src/SdJwt/SdJwt.php index b39d27a..f216d98 100644 --- a/src/SdJwt/SdJwt.php +++ b/src/SdJwt/SdJwt.php @@ -115,8 +115,6 @@ public function getToken( return $token; } - /** - */ public function getUndisclosedToken( JwsSerializerEnum $jwsSerializerEnum = JwsSerializerEnum::Compact, ?int $signatureIndex = null, From 0144143421800285a13422b7ca69fd8ae13b444f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 18 Aug 2025 10:11:28 +0200 Subject: [PATCH 36/66] Add TxCode --- src/Codebooks/TxCodeInputModeEnum.php | 11 ++++ src/VerifiableCredentials.php | 8 +++ .../Factories/TxCodeFactory.php | 17 ++++++ src/VerifiableCredentials/TxCode.php | 59 +++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 src/Codebooks/TxCodeInputModeEnum.php create mode 100644 src/VerifiableCredentials/Factories/TxCodeFactory.php create mode 100644 src/VerifiableCredentials/TxCode.php diff --git a/src/Codebooks/TxCodeInputModeEnum.php b/src/Codebooks/TxCodeInputModeEnum.php new file mode 100644 index 0000000..0eb2088 --- /dev/null +++ b/src/Codebooks/TxCodeInputModeEnum.php @@ -0,0 +1,11 @@ +disclosureFactory(), ); } + + public function txCodeFactory(): TxCodeFactory + { + return $this->txCodeFactory ??= new TxCodeFactory(); + } } diff --git a/src/VerifiableCredentials/Factories/TxCodeFactory.php b/src/VerifiableCredentials/Factories/TxCodeFactory.php new file mode 100644 index 0000000..4839f26 --- /dev/null +++ b/src/VerifiableCredentials/Factories/TxCodeFactory.php @@ -0,0 +1,17 @@ +txCode) && $this->txCode < 0) { + throw new \InvalidArgumentException('TxCode must be a positive integer or a string.'); + } + + $this->inputMode = is_numeric($this->txCode) ? TxCodeInputModeEnum::Numeric : TxCodeInputModeEnum::Text; + $this->length = mb_strlen((string)$this->txCode); + } + + public function getTxCode(): int|string + { + return $this->txCode; + } + + public function getDescription(): string + { + return $this->description; + } + + public function getInputMode(): TxCodeInputModeEnum + { + return $this->inputMode; + } + + public function getLength(): int + { + return $this->length; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + ClaimsEnum::InputMode->value => $this->inputMode->value, + ClaimsEnum::Length->value => $this->length, + ClaimsEnum::Description->value => $this->description, + ]; + } +} From c1a8fc740c1c373046f638da8d30b8fe6b1d8e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 18 Aug 2025 10:52:47 +0200 Subject: [PATCH 37/66] Add ext-mbstring requirement --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index af7ecfa..560136d 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "php": "^8.2", "ext-gmp": "*", "ext-filter": "*", + "ext-mbstring": "*", "guzzlehttp/guzzle": "^7.8", "psr/http-client": "^1", From 5d19dfe61087d9f03563128165749f4634f03084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 18 Aug 2025 14:30:03 +0200 Subject: [PATCH 38/66] Get TxCode as string --- src/VerifiableCredentials/TxCode.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/VerifiableCredentials/TxCode.php b/src/VerifiableCredentials/TxCode.php index f2fc374..8e8a5bc 100644 --- a/src/VerifiableCredentials/TxCode.php +++ b/src/VerifiableCredentials/TxCode.php @@ -14,20 +14,20 @@ class TxCode implements \JsonSerializable protected readonly int $length; public function __construct( - protected readonly int|string $txCode, + protected readonly int|string $code, protected readonly string $description, ) { - if (is_numeric($this->txCode) && $this->txCode < 0) { + if (is_numeric($this->code) && $this->code < 0) { throw new \InvalidArgumentException('TxCode must be a positive integer or a string.'); } - $this->inputMode = is_numeric($this->txCode) ? TxCodeInputModeEnum::Numeric : TxCodeInputModeEnum::Text; - $this->length = mb_strlen((string)$this->txCode); + $this->inputMode = is_numeric($this->code) ? TxCodeInputModeEnum::Numeric : TxCodeInputModeEnum::Text; + $this->length = mb_strlen((string)$this->code); } - public function getTxCode(): int|string + public function getCode(): int|string { - return $this->txCode; + return $this->code; } public function getDescription(): string @@ -56,4 +56,9 @@ public function jsonSerialize(): array ClaimsEnum::Description->value => $this->description, ]; } + + public function getTxCodeAsString(): string + { + return (string)$this->code; + } } From 6a8222ee65a70284b41d2863b94794a10e751b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 18 Aug 2025 14:31:08 +0200 Subject: [PATCH 39/66] Get TxCode as string --- src/VerifiableCredentials/TxCode.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/VerifiableCredentials/TxCode.php b/src/VerifiableCredentials/TxCode.php index 8e8a5bc..cf0377f 100644 --- a/src/VerifiableCredentials/TxCode.php +++ b/src/VerifiableCredentials/TxCode.php @@ -30,6 +30,11 @@ public function getCode(): int|string return $this->code; } + public function getCodeAsString(): string + { + return (string)$this->code; + } + public function getDescription(): string { return $this->description; @@ -56,9 +61,4 @@ public function jsonSerialize(): array ClaimsEnum::Description->value => $this->description, ]; } - - public function getTxCodeAsString(): string - { - return (string)$this->code; - } } From c0a72b57f64fab07c3fc70c28fd66701ef3e6f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 10 Sep 2025 14:27:12 +0200 Subject: [PATCH 40/66] Update ClaimsEnum --- src/Codebooks/ClaimsEnum.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index 880fd51..a05c2bb 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -60,6 +60,7 @@ enum ClaimsEnum: string case Credential_Status = 'credentialStatus'; case Credential_Subject = 'credentialSubject'; case CryptographicBindingMethodsSupported = 'cryptographic_binding_methods_supported'; + case CryptographicSuitesSupported= 'cryptographic_suites_supported'; case DeferredCredentialEndpoint = 'deferred_credential_endpoint'; case Delegation = 'delegation'; case Description = 'description'; From aa31a9f1b54492dfaa0dbf3733e81ba6d2433510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 10 Sep 2025 15:14:31 +0200 Subject: [PATCH 41/66] Update ClaimsEnum --- src/Codebooks/ClaimsEnum.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index a05c2bb..673c70b 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -53,6 +53,7 @@ enum ClaimsEnum: string case CredentialEndpoint = 'credential_endpoint'; case CredentialIdentifier = 'credential_identifier'; case CredentialIssuer = 'credential_issuer'; + case CredentialMetadata = 'credential_metadata'; case CredentialResponseEncryption = 'credential_response_encryption'; case Credential_Schema = 'credentialSchema'; // CredentialSigningAlgorithmValuesSupported From 07d879d787461d80ebd05170b9c5d4673a8f62cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 10 Sep 2025 15:38:06 +0200 Subject: [PATCH 42/66] Lint --- src/Codebooks/ClaimsEnum.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index 673c70b..476146c 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -61,7 +61,7 @@ enum ClaimsEnum: string case Credential_Status = 'credentialStatus'; case Credential_Subject = 'credentialSubject'; case CryptographicBindingMethodsSupported = 'cryptographic_binding_methods_supported'; - case CryptographicSuitesSupported= 'cryptographic_suites_supported'; + case CryptographicSuitesSupported = 'cryptographic_suites_supported'; case DeferredCredentialEndpoint = 'deferred_credential_endpoint'; case Delegation = 'delegation'; case Description = 'description'; From 4a1681caeefbbaf65ad5e9719698c0a1ae5897d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 10 Sep 2025 15:48:47 +0200 Subject: [PATCH 43/66] Add gmp to windows CI run --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 5713810..b81f06d 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -220,7 +220,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - extensions: ctype, date, dom, filter, hash, mbstring, openssl, pcre, soap, spl, xml, sodium + extensions: ctype, date, dom, filter, hash, mbstring, openssl, pcre, soap, spl, xml, sodium, gmp tools: composer ini-values: error_reporting=E_ALL coverage: none From d5892ea94c37d4ae1f477f515f47c6baee3b6f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 17 Sep 2025 10:06:24 +0200 Subject: [PATCH 44/66] Update ClaimsEnum --- src/Codebooks/ClaimsEnum.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index 476146c..27a937b 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -52,6 +52,7 @@ enum ClaimsEnum: string case CredentialDefinition = 'credential_definition'; case CredentialEndpoint = 'credential_endpoint'; case CredentialIdentifier = 'credential_identifier'; + case CredentialIdentifiers = 'credential_identifiers'; case CredentialIssuer = 'credential_issuer'; case CredentialMetadata = 'credential_metadata'; case CredentialResponseEncryption = 'credential_response_encryption'; From 8c794da1ffe508b69020cd03d17214689970a8c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Thu, 18 Sep 2025 11:36:08 +0200 Subject: [PATCH 45/66] Lint --- src/Algorithms/AlgorithmManagerDecorator.php | 1 + src/Algorithms/SignatureAlgorithmBag.php | 4 +++ src/Algorithms/SignatureAlgorithmEnum.php | 2 ++ src/Claims/ClaimInterface.php | 1 + src/Claims/GenericClaim.php | 3 ++ src/Claims/JwksClaim.php | 3 ++ src/Codebooks/GrantTypesEnum.php | 9 +++++ src/Codebooks/HashAlgorithmsEnum.php | 3 ++ src/Codebooks/MetadataPolicyOperatorsEnum.php | 14 ++++++++ src/Codebooks/WellKnownEnum.php | 2 ++ src/Core.php | 15 ++++++++ src/Core/ClientAssertion.php | 6 ++++ src/Core/Factories/ClientAssertionFactory.php | 1 + src/Core/Factories/RequestObjectFactory.php | 1 + src/Decorators/CacheDecorator.php | 5 +++ src/Decorators/DateIntervalDecorator.php | 4 +++ src/Decorators/HttpClientDecorator.php | 2 ++ src/Did.php | 2 ++ src/Did/DidKeyJwkResolver.php | 8 +++++ src/Factories/ClaimFactory.php | 5 +++ src/Federation.php | 33 +++++++++++++++++ .../Claims/TrustMarkOwnersClaimBag.php | 6 ++++ .../Claims/TrustMarkOwnersClaimValue.php | 5 +++ src/Federation/Claims/TrustMarksClaimBag.php | 6 ++++ .../Claims/TrustMarksClaimValue.php | 4 +++ src/Federation/EntityStatement.php | 16 +++++++++ src/Federation/EntityStatementFetcher.php | 7 ++++ .../Factories/EntityStatementFactory.php | 1 + .../Factories/FederationClaimFactory.php | 6 ++++ .../Factories/RequestObjectFactory.php | 1 + .../Factories/TrustChainFactory.php | 4 +++ .../Factories/TrustMarkDelegationFactory.php | 1 + src/Federation/Factories/TrustMarkFactory.php | 1 + src/Federation/MetadataPolicyApplicator.php | 3 ++ src/Federation/MetadataPolicyResolver.php | 2 ++ src/Federation/RequestObject.php | 5 +++ src/Federation/TrustChain.php | 26 ++++++++++++++ src/Federation/TrustChainBag.php | 6 ++++ src/Federation/TrustChainResolver.php | 6 ++++ src/Federation/TrustMark.php | 10 ++++++ src/Federation/TrustMarkDelegation.php | 7 ++++ src/Federation/TrustMarkFetcher.php | 6 ++++ src/Federation/TrustMarkValidator.php | 12 +++++++ src/Helpers.php | 8 +++++ src/Helpers/Arr.php | 10 ++++++ src/Helpers/Base64Url.php | 1 + src/Helpers/DateTime.php | 2 ++ src/Helpers/Json.php | 1 + src/Helpers/Type.php | 16 +++++++++ src/Helpers/Url.php | 1 + src/Jwk.php | 1 + src/Jwk/Factories/JwkDecoratorFactory.php | 5 +++ src/Jwk/JwkDecorator.php | 1 + src/Jwks.php | 17 +++++++++ src/Jwks/Factories/SignedJwksFactory.php | 1 + src/Jwks/JwksDecorator.php | 2 ++ src/Jwks/JwksFetcher.php | 6 ++++ src/Jwks/SignedJwks.php | 5 +++ src/Jws/AbstractJwsFetcher.php | 2 ++ src/Jws/Factories/ParsedJwsFactory.php | 2 ++ src/Jws/JwsDecorator.php | 1 + src/Jws/JwsDecoratorBuilder.php | 2 ++ src/Jws/JwsFetcher.php | 5 +++ src/Jws/JwsVerifierDecorator.php | 2 ++ src/Jws/ParsedJws.php | 22 ++++++++++++ src/SdJwt/Disclosure.php | 11 ++++++ src/SdJwt/DisclosureBag.php | 4 +++ src/SdJwt/Factories/DisclosureFactory.php | 2 ++ src/SdJwt/Factories/SdJwtFactory.php | 2 ++ src/SdJwt/SdJwt.php | 7 ++++ src/Serializers/JwsSerializerBag.php | 4 +++ src/Serializers/JwsSerializerEnum.php | 1 + .../JwsSerializerManagerDecorator.php | 3 ++ src/SupportedAlgorithms.php | 1 + src/SupportedSerializers.php | 1 + src/Utils/ArtifactFetcher.php | 4 +++ src/VerifiableCredentials.php | 21 +++++++++++ .../ClaimsPathPointerResolver.php | 1 + src/VerifiableCredentials/CredentialOffer.php | 1 + .../CredentialOfferGrantsBag.php | 2 ++ .../CredentialOfferGrantsValue.php | 1 + .../CredentialOfferParameters.php | 1 + .../Factories/CredentialOfferFactory.php | 1 + .../Factories/OpenId4VciProofFactory.php | 1 + src/VerifiableCredentials/OpenId4VciProof.php | 6 ++++ src/VerifiableCredentials/SdJwtVc/SdJwtVc.php | 5 +++ src/VerifiableCredentials/TxCode.php | 7 ++++ .../AbstractIdentifiedTypedClaimValue.php | 7 ++++ .../Claims/AbstractTypedClaimValue.php | 6 ++++ .../VcDataModel/Claims/TypeClaimValue.php | 4 +++ .../Claims/VcAtContextClaimValue.php | 5 +++ .../VcDataModel/Claims/VcClaimValue.php | 13 +++++++ .../Claims/VcCredentialSchemaClaimBag.php | 4 +++ .../Claims/VcCredentialSubjectClaimBag.php | 4 +++ .../Claims/VcCredentialSubjectClaimValue.php | 4 +++ .../VcDataModel/Claims/VcEvidenceClaimBag.php | 4 +++ .../VcDataModel/Claims/VcIssuerClaimValue.php | 6 ++++ .../Claims/VcRefreshServiceClaimBag.php | 4 +++ .../Claims/VcTermsOfUseClaimBag.php | 4 +++ .../Factories/JwtVcJsonFactory.php | 1 + .../Factories/VcDataModelClaimFactory.php | 16 +++++++++ .../VcDataModel/JwtVcJson.php | 16 +++++++++ .../AlgorithmManagerDecoratorTest.php | 4 +++ .../Algorithms/SignatureAlgorithmBagTest.php | 5 +++ tests/src/Claims/GenericClaimTest.php | 4 +++ tests/src/Claims/JwksClaimTest.php | 4 +++ .../MetadataPolicyOperatorsEnumTest.php | 23 ++++++++++++ tests/src/Codebooks/WellKnownEnumTest.php | 2 ++ tests/src/Core/ClientAssertionTest.php | 7 +++- .../Factories/ClientAssertionFactoryTest.php | 4 ++- .../Factories/RequestObjectFactoryTest.php | 6 ++-- tests/src/Core/RequestObjectTest.php | 9 +++-- tests/src/CoreTest.php | 4 +++ tests/src/Decorators/CacheDecoratorTest.php | 7 ++++ .../Decorators/DateIntervalDecoratorTest.php | 5 +++ .../Decorators/HttpClientDecoratorTest.php | 6 ++++ tests/src/Did/DidKeyJwkResolverTest.php | 36 +++++++++++++++++++ .../AlgorithmManagerDecoratorFactoryTest.php | 4 ++- .../Factories/CacheDecoratorFactoryTest.php | 4 +++ tests/src/Factories/ClaimFactoryTest.php | 8 +++++ .../DateIntervalDecoratorFactoryTest.php | 4 +++ .../HttpClientDecoratorFactoryTest.php | 4 +++ ...sSerializerManagerDecoratorFactoryTest.php | 4 ++- .../Claims/TrustMarkOwnersClaimBagTest.php | 5 +++ .../Claims/TrustMarkOwnersClaimValueTest.php | 5 +++ .../Claims/TrustMarksClaimBagTest.php | 8 +++++ .../Claims/TrustMarksClaimValueTest.php | 5 +++ .../Federation/EntityStatementFetcherTest.php | 8 +++++ tests/src/Federation/EntityStatementTest.php | 25 +++++++++++-- .../Factories/EntityStatementFactoryTest.php | 5 ++- .../Factories/FederationClaimFactoryTest.php | 8 +++++ .../Factories/RequestObjectFactoryTest.php | 7 ++-- .../Factories/TrustChainBagFactoryTest.php | 4 +++ .../Factories/TrustChainFactoryTest.php | 8 +++++ .../TrustMarkDelegationFactoryTest.php | 7 ++-- .../Factories/TrustMarkFactoryTest.php | 7 ++-- .../MetadataPolicyApplicatorTest.php | 21 +++++++++++ .../Federation/MetadataPolicyResolverTest.php | 16 +++++++++ .../MetadataPolicyTestVectorsTest.php | 3 ++ tests/src/Federation/RequestObjectTest.php | 7 +++- tests/src/Federation/TrustChainBagTest.php | 7 ++++ .../src/Federation/TrustChainResolverTest.php | 17 +++++++++ tests/src/Federation/TrustChainTest.php | 18 ++++++++++ .../Federation/TrustMarkDelegationTest.php | 6 ++-- tests/src/Federation/TrustMarkFetcherTest.php | 8 +++++ tests/src/Federation/TrustMarkTest.php | 6 ++-- .../src/Federation/TrustMarkValidatorTest.php | 28 +++++++++++++++ tests/src/FederationTest.php | 4 +++ tests/src/Help.php | 2 ++ tests/src/Helpers/ArrTest.php | 9 +++++ tests/src/Helpers/JsonTest.php | 1 + tests/src/Helpers/TypeTest.php | 17 +++++++++ tests/src/Helpers/UrlTest.php | 2 ++ tests/src/HelpersTest.php | 2 ++ .../Factories/JwksDecoratorFactoryTest.php | 4 +++ .../Jwks/Factories/SignedJwksFactoryTest.php | 7 ++-- tests/src/Jwks/JwksDecoratorTest.php | 6 +++- tests/src/Jwks/JwksFetcherTest.php | 18 +++++++++- tests/src/Jwks/SignedJwksTest.php | 6 ++-- tests/src/JwksTest.php | 4 +++ .../JwsDecoratorBuilderFactoryTest.php | 4 +++ .../JwsVerifierDecoratorFactoryTest.php | 4 +++ .../Jws/Factories/ParsedJwsFactoryTest.php | 4 +++ tests/src/Jws/JwsDecoratorBuilderTest.php | 5 +++ tests/src/Jws/JwsDecoratorTest.php | 4 +++ tests/src/Jws/JwsFetcherTest.php | 9 ++++- tests/src/Jws/JwsVerifierDecoratorTest.php | 5 +++ tests/src/Jws/ParsedJwsTest.php | 24 ++++++++++++- tests/src/SdJwt/DisclosureTest.php | 8 +++++ .../src/SdJwt/Factories/SdJwtFactoryTest.php | 5 +++ .../src/Serializers/JwsSerializerBagTest.php | 8 +++-- .../JwsSerializerManagerDecoratorTest.php | 5 +++ tests/src/SupportedAlgorithmsTest.php | 4 +++ tests/src/SupportedSerializersTest.php | 4 +++ tests/src/Utils/ArtifactFetcherTest.php | 14 ++++++++ .../ClaimsPathPointerResolverTest.php | 9 +++++ .../Factories/JwtVcJsonFactoryTest.php | 5 ++- 177 files changed, 1119 insertions(+), 40 deletions(-) diff --git a/src/Algorithms/AlgorithmManagerDecorator.php b/src/Algorithms/AlgorithmManagerDecorator.php index a0db886..7116158 100644 --- a/src/Algorithms/AlgorithmManagerDecorator.php +++ b/src/Algorithms/AlgorithmManagerDecorator.php @@ -13,6 +13,7 @@ public function __construct( ) { } + public function algorithmManager(): AlgorithmManager { return $this->algorithmManager; diff --git a/src/Algorithms/SignatureAlgorithmBag.php b/src/Algorithms/SignatureAlgorithmBag.php index 6483937..fe66a42 100644 --- a/src/Algorithms/SignatureAlgorithmBag.php +++ b/src/Algorithms/SignatureAlgorithmBag.php @@ -11,16 +11,19 @@ class SignatureAlgorithmBag /** @var \SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum[] */ protected array $algorithms; + public function __construct(SignatureAlgorithmEnum $algorithm, SignatureAlgorithmEnum ...$algorithms) { $this->algorithms = [$algorithm, ...$algorithms]; } + public function add(SignatureAlgorithmEnum $algorithm): void { $this->algorithms[] = $algorithm; } + /** * @return \SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum[] */ @@ -29,6 +32,7 @@ public function getAll(): array return $this->algorithms; } + /** * @return \Jose\Component\Signature\Algorithm\SignatureAlgorithm[] */ diff --git a/src/Algorithms/SignatureAlgorithmEnum.php b/src/Algorithms/SignatureAlgorithmEnum.php index 35a87b7..df84f0f 100644 --- a/src/Algorithms/SignatureAlgorithmEnum.php +++ b/src/Algorithms/SignatureAlgorithmEnum.php @@ -31,6 +31,7 @@ enum SignatureAlgorithmEnum: string case RS384 = 'RS384'; case RS512 = 'RS512'; + public function instance(): SignatureAlgorithm { return match ($this) { @@ -48,6 +49,7 @@ public function instance(): SignatureAlgorithm }; } + public function isNone(): bool { return $this === SignatureAlgorithmEnum::none; diff --git a/src/Claims/ClaimInterface.php b/src/Claims/ClaimInterface.php index 0026315..9206443 100644 --- a/src/Claims/ClaimInterface.php +++ b/src/Claims/ClaimInterface.php @@ -8,5 +8,6 @@ interface ClaimInterface extends \JsonSerializable { public function getName(): string; + public function getValue(): mixed; } diff --git a/src/Claims/GenericClaim.php b/src/Claims/GenericClaim.php index 01f2d6a..0c073ea 100644 --- a/src/Claims/GenericClaim.php +++ b/src/Claims/GenericClaim.php @@ -15,16 +15,19 @@ public function __construct( ) { } + public function getValue(): mixed { return $this->value; } + public function getName(): string { return $this->name; } + /** * @return array */ diff --git a/src/Claims/JwksClaim.php b/src/Claims/JwksClaim.php index e07d072..749cea0 100644 --- a/src/Claims/JwksClaim.php +++ b/src/Claims/JwksClaim.php @@ -18,6 +18,7 @@ public function __construct( ) { } + /** * @return array{keys:non-empty-array>} */ @@ -26,6 +27,7 @@ public function getValue(): array return $this->value; } + /** * @return non-empty-string */ @@ -34,6 +36,7 @@ public function getName(): string return $this->name; } + /** * @return array>}> */ diff --git a/src/Codebooks/GrantTypesEnum.php b/src/Codebooks/GrantTypesEnum.php index 0d20d5a..7b8f5b2 100644 --- a/src/Codebooks/GrantTypesEnum.php +++ b/src/Codebooks/GrantTypesEnum.php @@ -10,4 +10,13 @@ enum GrantTypesEnum: string case Implicit = 'implicit'; case PreAuthorizedCode = 'urn:ietf:params:oauth:grant-type:pre-authorized_code'; case RefreshToken = 'refresh_token'; + + + public function canBeUsedForVerifiableCredentialIssuance(): bool + { + return match ($this) { + self::AuthorizationCode, self::PreAuthorizedCode => true, + default => false, + }; + } } diff --git a/src/Codebooks/HashAlgorithmsEnum.php b/src/Codebooks/HashAlgorithmsEnum.php index e510f9f..95dd8f9 100644 --- a/src/Codebooks/HashAlgorithmsEnum.php +++ b/src/Codebooks/HashAlgorithmsEnum.php @@ -27,11 +27,14 @@ enum HashAlgorithmsEnum: string case BLAKE2B_512 = 'blake2b-512'; case K12_256 = 'k12-256'; case K12_512 = 'k12-512'; + + public function ianaName(): string { return $this->value; } + /** * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException */ diff --git a/src/Codebooks/MetadataPolicyOperatorsEnum.php b/src/Codebooks/MetadataPolicyOperatorsEnum.php index 5f711a3..f5cafa5 100644 --- a/src/Codebooks/MetadataPolicyOperatorsEnum.php +++ b/src/Codebooks/MetadataPolicyOperatorsEnum.php @@ -20,6 +20,7 @@ enum MetadataPolicyOperatorsEnum: string case SupersetOf = 'superset_of'; case Essential = 'essential'; + /** * @return string[] */ @@ -28,6 +29,7 @@ public static function values(): array return array_column(self::cases(), 'value'); } + /** * @return string[] */ @@ -60,6 +62,7 @@ public function getSupportedOperatorValueTypes(): array }; } + /** * @return string[] */ @@ -87,6 +90,7 @@ public function getSupportedParameterValueTypes(): array }; } + /** * @return string[] * @throws \SimpleSAML\OpenID\Exceptions\MetadataPolicyException @@ -105,6 +109,7 @@ public function getSupportedOperatorContainedValueTypes(): array }; } + /** * @return string[] * @throws \SimpleSAML\OpenID\Exceptions\MetadataPolicyException @@ -124,6 +129,7 @@ public function getSupportedParameterContainedValueTypes(): array }; } + /** * @phpstan-ignore missingType.iterableValue (We can handle mixed type using array_diff) */ @@ -134,6 +140,7 @@ public function isValueSubsetOf(mixed $value, array $superset): bool return array_diff($value, $superset) === []; } + /** * @phpstan-ignore missingType.iterableValue (We can handle mixed type using array_diff) */ @@ -145,6 +152,7 @@ public function isValueSupersetOf(mixed $value, array $subset): bool return array_diff($subset, $value) === []; } + /** * @throws \SimpleSAML\OpenID\Exceptions\MetadataPolicyException */ @@ -169,6 +177,7 @@ public function isOperatorValueTypeSupported(mixed $operatorValue): bool return true; } + /** * @throws \SimpleSAML\OpenID\Exceptions\MetadataPolicyException */ @@ -193,6 +202,7 @@ public function isParameterValueTypeSupported(mixed $parameterValue): bool return true; } + /** * @return string[] */ @@ -255,6 +265,7 @@ public function getSupportedOperatorCombinations(): array ]; } + /** * @param string[] $operatorKeys */ @@ -263,6 +274,7 @@ public function isOperatorCombinationSupported(array $operatorKeys): bool return array_diff($operatorKeys, $this->getSupportedOperatorCombinations()) === []; } + /** * Validate general parameter operation rules like operator combinations and operator value type. * @@ -307,6 +319,7 @@ public static function validateGeneralParameterOperationRules(array $parameterOp } } + /** * @param array $parameterOperations * @throws \SimpleSAML\OpenID\Exceptions\MetadataPolicyException @@ -505,6 +518,7 @@ public static function validateSpecificParameterOperationRules(array $parameterO } } + /** * @throws \SimpleSAML\OpenID\Exceptions\MetadataPolicyException */ diff --git a/src/Codebooks/WellKnownEnum.php b/src/Codebooks/WellKnownEnum.php index 13f6373..b715b2d 100644 --- a/src/Codebooks/WellKnownEnum.php +++ b/src/Codebooks/WellKnownEnum.php @@ -11,6 +11,7 @@ enum WellKnownEnum: string case OpenIdFederation = 'openid-federation'; case OpenIdCredentialIssuer = 'openid-credential-issuer'; + public function path(): string { if ($this === WellKnownEnum::Prefix) { @@ -20,6 +21,7 @@ public function path(): string return self::Prefix->value . '/' . $this->value; } + public function uriFor(string $entityId): string { return rtrim($entityId, '/') . '/' . $this->path(); diff --git a/src/Core.php b/src/Core.php index 9e091f7..3ea476d 100644 --- a/src/Core.php +++ b/src/Core.php @@ -55,6 +55,7 @@ class Core protected ?AlgorithmManagerDecorator $algorithmManagerDecorator = null; + public function __construct( protected readonly SupportedAlgorithms $supportedAlgorithms = new SupportedAlgorithms( new SignatureAlgorithmBag( @@ -70,6 +71,7 @@ public function __construct( ->build($timestampValidationLeeway); } + public function requestObjectFactory(): RequestObjectFactory { return $this->requestObjectFactory ??= new RequestObjectFactory( @@ -83,6 +85,7 @@ public function requestObjectFactory(): RequestObjectFactory ); } + public function clientAssertionFactory(): ClientAssertionFactory { return $this->clientAssertionFactory ??= new ClientAssertionFactory( @@ -96,11 +99,13 @@ public function clientAssertionFactory(): ClientAssertionFactory ); } + public function helpers(): Helpers { return $this->helpers ??= new Helpers(); } + public function algorithmManagerDecoratorFactory(): AlgorithmManagerDecoratorFactory { if (is_null($this->algorithmManagerDecoratorFactory)) { @@ -110,12 +115,14 @@ public function algorithmManagerDecoratorFactory(): AlgorithmManagerDecoratorFac return $this->algorithmManagerDecoratorFactory; } + public function algorithmManagerDecorator(): AlgorithmManagerDecorator { return $this->algorithmManagerDecorator ??= $this->algorithmManagerDecoratorFactory() ->build($this->supportedAlgorithms); } + public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDecoratorFactory { if (is_null($this->jwsSerializerManagerDecoratorFactory)) { @@ -125,6 +132,7 @@ public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDeco return $this->jwsSerializerManagerDecoratorFactory; } + public function jwsDecoratorBuilderFactory(): JwsDecoratorBuilderFactory { if (is_null($this->jwsDecoratorBuilderFactory)) { @@ -134,6 +142,7 @@ public function jwsDecoratorBuilderFactory(): JwsDecoratorBuilderFactory return $this->jwsDecoratorBuilderFactory; } + public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory { if (is_null($this->jwsVerifierDecoratorFactory)) { @@ -143,11 +152,13 @@ public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory return $this->jwsVerifierDecoratorFactory; } + public function jwksDecoratorFactory(): JwksDecoratorFactory { return $this->jwksDecoratorFactory ??= new JwksDecoratorFactory(); } + public function dateIntervalDecoratorFactory(): DateIntervalDecoratorFactory { if (is_null($this->dateIntervalDecoratorFactory)) { @@ -157,6 +168,7 @@ public function dateIntervalDecoratorFactory(): DateIntervalDecoratorFactory return $this->dateIntervalDecoratorFactory; } + public function jwsSerializerManagerDecorator(): JwsSerializerManagerDecorator { if (is_null($this->jwsSerializerManagerDecorator)) { @@ -167,6 +179,7 @@ public function jwsSerializerManagerDecorator(): JwsSerializerManagerDecorator return $this->jwsSerializerManagerDecorator; } + public function jwsDecoratorBuilder(): JwsDecoratorBuilder { if (is_null($this->jwsDecoratorBuilder)) { @@ -180,6 +193,7 @@ public function jwsDecoratorBuilder(): JwsDecoratorBuilder return $this->jwsDecoratorBuilder; } + public function jwsVerifierDecorator(): JwsVerifierDecorator { if (is_null($this->jwsVerifierDecorator)) { @@ -191,6 +205,7 @@ public function jwsVerifierDecorator(): JwsVerifierDecorator return $this->jwsVerifierDecorator; } + public function claimFactory(): ClaimFactory { return $this->claimFactory ??= new ClaimFactory( diff --git a/src/Core/ClientAssertion.php b/src/Core/ClientAssertion.php index 9a9db84..26087e7 100644 --- a/src/Core/ClientAssertion.php +++ b/src/Core/ClientAssertion.php @@ -26,6 +26,7 @@ protected function validate(): void ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\ClientAssertionException @@ -39,6 +40,7 @@ protected function validateIssuerAndSubject(): void } } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\ClientAssertionException @@ -49,6 +51,7 @@ public function getIssuer(): string return parent::getIssuer() ?? throw new ClientAssertionException('No Issuer claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\ClientAssertionException @@ -59,6 +62,7 @@ public function getSubject(): string return parent::getSubject() ?? throw new ClientAssertionException('No Subject claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\ClientAssertionException @@ -69,6 +73,7 @@ public function getAudience(): array return parent::getAudience() ?? throw new ClientAssertionException('No Audience claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\ClientAssertionException @@ -78,6 +83,7 @@ public function getJwtId(): string return parent::getJwtId() ?? throw new ClientAssertionException('No JWT ID claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\ClientAssertionException diff --git a/src/Core/Factories/ClientAssertionFactory.php b/src/Core/Factories/ClientAssertionFactory.php index a3cab0d..af6a1a3 100644 --- a/src/Core/Factories/ClientAssertionFactory.php +++ b/src/Core/Factories/ClientAssertionFactory.php @@ -24,6 +24,7 @@ public function fromToken(string $token): ClientAssertion ); } + /** * @param array $payload * @param array $header diff --git a/src/Core/Factories/RequestObjectFactory.php b/src/Core/Factories/RequestObjectFactory.php index 961506f..757a6dd 100644 --- a/src/Core/Factories/RequestObjectFactory.php +++ b/src/Core/Factories/RequestObjectFactory.php @@ -24,6 +24,7 @@ public function fromToken(string $token): RequestObject ); } + /** * @param array $payload * @param array $header diff --git a/src/Decorators/CacheDecorator.php b/src/Decorators/CacheDecorator.php index 84efb37..7237870 100644 --- a/src/Decorators/CacheDecorator.php +++ b/src/Decorators/CacheDecorator.php @@ -13,6 +13,7 @@ public function __construct(public readonly CacheInterface $cache) { } + /** * @throws \Psr\SimpleCache\InvalidArgumentException */ @@ -21,6 +22,7 @@ public function get(mixed $default, string $keyElement, string ...$keyElements): return $this->cache->get(self::keyFor($keyElement, ...$keyElements), $default); } + /** * @throws \Psr\SimpleCache\InvalidArgumentException */ @@ -29,6 +31,7 @@ public function set(mixed $value, int|DateInterval $ttl, string $keyElement, str $this->cache->set(self::keyFor($keyElement, ...$keyElements), $value, $ttl); } + /** * @throws \Psr\SimpleCache\InvalidArgumentException */ @@ -37,6 +40,7 @@ public function has(string $keyElement, string ...$keyElements): bool return $this->cache->has(self::keyFor($keyElement, ...$keyElements)); } + /** * @throws \Psr\SimpleCache\InvalidArgumentException */ @@ -45,6 +49,7 @@ public function delete(string $keyElement, string ...$keyElements): bool return $this->cache->delete(self::keyFor($keyElement, ...$keyElements)); } + public static function keyFor(string $element, string ...$elements): string { return hash('sha256', implode('-', [$element, ...$elements])); diff --git a/src/Decorators/DateIntervalDecorator.php b/src/Decorators/DateIntervalDecorator.php index dec66ed..73797df 100644 --- a/src/Decorators/DateIntervalDecorator.php +++ b/src/Decorators/DateIntervalDecorator.php @@ -11,11 +11,13 @@ class DateIntervalDecorator { public int $inSeconds; + public function __construct(public readonly DateInterval $dateInterval) { $this->inSeconds = self::toSeconds($this->dateInterval); } + public function lowestInSecondsComparedToExpirationTime(int $expirationTime): int { $timestamp = time(); @@ -24,6 +26,7 @@ public function lowestInSecondsComparedToExpirationTime(int $expirationTime): in return min($this->inSeconds, $expirationInSeconds); } + public static function toSeconds(DateInterval $dateInterval): int { $reference = new DateTimeImmutable(); @@ -32,6 +35,7 @@ public static function toSeconds(DateInterval $dateInterval): int return $endTime->getTimestamp() - $reference->getTimestamp(); } + public function getInSeconds(): int { return $this->inSeconds; diff --git a/src/Decorators/HttpClientDecorator.php b/src/Decorators/HttpClientDecorator.php index 9eefa44..5e0a5b9 100644 --- a/src/Decorators/HttpClientDecorator.php +++ b/src/Decorators/HttpClientDecorator.php @@ -15,10 +15,12 @@ class HttpClientDecorator { public const DEFAULT_HTTP_CLIENT_CONFIG = [RequestOptions::ALLOW_REDIRECTS => true,]; + public function __construct(public readonly Client $client = new Client(self::DEFAULT_HTTP_CLIENT_CONFIG)) { } + /** * @throws \SimpleSAML\OpenID\Exceptions\HttpException */ diff --git a/src/Did.php b/src/Did.php index 07cf44f..7f73736 100644 --- a/src/Did.php +++ b/src/Did.php @@ -12,6 +12,7 @@ class Did protected ?Helpers $helpers = null; + public function didKeyResolver(): DidKeyJwkResolver { return $this->didKeyResolver ??= new DidKeyJwkResolver( @@ -19,6 +20,7 @@ public function didKeyResolver(): DidKeyJwkResolver ); } + public function helpers(): Helpers { return $this->helpers ??= new Helpers(); diff --git a/src/Did/DidKeyJwkResolver.php b/src/Did/DidKeyJwkResolver.php index 377ef69..7df4ba8 100644 --- a/src/Did/DidKeyJwkResolver.php +++ b/src/Did/DidKeyJwkResolver.php @@ -18,6 +18,7 @@ public function __construct( ) { } + /** * Extract JWK from a did:key value. * @@ -89,6 +90,7 @@ public function extractJwkFromDidKey(string $didKey): array } } + /** * Decode a variable integer (varint) from bytes. * @@ -223,6 +225,7 @@ public function createEd25519Jwk(string $rawKeyBytes): array ]; } + /** * Create a JWK for an X25519 public key. * @@ -239,6 +242,7 @@ public function createX25519Jwk(string $rawKeyBytes): array ]; } + /** * Create a JWK for a Secp256k1 public key. * @@ -272,6 +276,7 @@ public function createSecp256k1Jwk(string $rawKeyBytes): array ]; } + /** * Create a JWK for a P-256 (NIST) public key. * @@ -310,6 +315,7 @@ public function createP256Jwk(string $rawKeyBytes): array ]; } + /** * Create a JWK for a P-384 (NIST) public key. * @@ -338,6 +344,7 @@ public function createP384Jwk(string $rawKeyBytes): array ]; } + /** * Create a JWK for a P-521 (NIST) public key. * @@ -365,6 +372,7 @@ public function createP521Jwk(string $rawKeyBytes): array ]; } + /** * Create a JWK from raw JSON data. * diff --git a/src/Factories/ClaimFactory.php b/src/Factories/ClaimFactory.php index 82501d2..93f2921 100644 --- a/src/Factories/ClaimFactory.php +++ b/src/Factories/ClaimFactory.php @@ -18,11 +18,13 @@ class ClaimFactory protected VcDataModelClaimFactory $vcDataModelClaimFactory; + public function __construct( protected readonly Helpers $helpers, ) { } + public function forFederation(): FederationClaimFactory { return $this->federationClaimFactory ??= new FederationClaimFactory( @@ -31,6 +33,7 @@ public function forFederation(): FederationClaimFactory ); } + public function forVcDataModel(): VcDataModelClaimFactory { return $this->vcDataModelClaimFactory ??= new VcDataModelClaimFactory( @@ -39,6 +42,7 @@ public function forVcDataModel(): VcDataModelClaimFactory ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException */ @@ -50,6 +54,7 @@ public function buildGeneric(mixed $value, string $name): GenericClaim ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwksException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException diff --git a/src/Federation.php b/src/Federation.php index 314fe91..e60cc65 100644 --- a/src/Federation.php +++ b/src/Federation.php @@ -104,6 +104,7 @@ class Federation protected ?AlgorithmManagerDecorator $algorithmManagerDecorator = null; + public function __construct( protected readonly SupportedAlgorithms $supportedAlgorithms = new SupportedAlgorithms(), protected readonly SupportedSerializers $supportedSerializers = new SupportedSerializers(), @@ -122,6 +123,7 @@ public function __construct( $this->httpClientDecorator = $this->httpClientDecoratorFactory()->build($client); } + public function jwsVerifierDecorator(): JwsVerifierDecorator { return $this->jwsVerifierDecorator ??= $this->jwsVerifierDecoratorFactory()->build( @@ -129,6 +131,7 @@ public function jwsVerifierDecorator(): JwsVerifierDecorator ); } + public function jwsDecoratorBuilder(): JwsDecoratorBuilder { return $this->jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderFactory()->build( @@ -138,12 +141,14 @@ public function jwsDecoratorBuilder(): JwsDecoratorBuilder ); } + public function jwsSerializerManagerDecorator(): JwsSerializerManagerDecorator { return $this->jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorFactory() ->build($this->supportedSerializers); } + public function entityStatementFetcher(): EntityStatementFetcher { return $this->entityStatementFetcher ??= new EntityStatementFetcher( @@ -155,16 +160,19 @@ public function entityStatementFetcher(): EntityStatementFetcher ); } + public function metadataPolicyResolver(): MetadataPolicyResolver { return $this->metadataPolicyResolver ??= new MetadataPolicyResolver($this->helpers()); } + public function metadataPolicyApplicator(): MetadataPolicyApplicator { return $this->metadataPolicyApplicator ??= new MetadataPolicyApplicator($this->helpers()); } + public function trustChainFactory(): TrustChainFactory { return $this->trustChainFactory ??= new TrustChainFactory( @@ -176,6 +184,7 @@ public function trustChainFactory(): TrustChainFactory ); } + public function trustChainResolver(): TrustChainResolver { return $this->trustChainResolver ??= new TrustChainResolver( @@ -189,6 +198,7 @@ public function trustChainResolver(): TrustChainResolver ); } + public function entityStatementFactory(): EntityStatementFactory { return $this->entityStatementFactory ??= new EntityStatementFactory( @@ -202,6 +212,7 @@ public function entityStatementFactory(): EntityStatementFactory ); } + public function requestObjectFactory(): RequestObjectFactory { return $this->requestObjectFactory ??= new RequestObjectFactory( @@ -215,6 +226,7 @@ public function requestObjectFactory(): RequestObjectFactory ); } + public function trustMarkFactory(): TrustMarkFactory { return $this->trustMarkFactory ??= new TrustMarkFactory( @@ -228,6 +240,7 @@ public function trustMarkFactory(): TrustMarkFactory ); } + public function trustMarkDelegationFactory(): TrustMarkDelegationFactory { return $this->trustMarkDelegationFactory ?? new TrustMarkDelegationFactory( @@ -241,6 +254,7 @@ public function trustMarkDelegationFactory(): TrustMarkDelegationFactory ); } + public function trustMarkValidator(): TrustMarkValidator { return $this->trustMarkValidator ??= new TrustMarkValidator( @@ -253,6 +267,7 @@ public function trustMarkValidator(): TrustMarkValidator ); } + public function trustMarkFetcher(): TrustMarkFetcher { return $this->trustMarkFetcher ??= new TrustMarkFetcher( @@ -264,42 +279,50 @@ public function trustMarkFetcher(): TrustMarkFetcher ); } + public function helpers(): Helpers { return $this->helpers ??= new Helpers(); } + public function algorithmManagerDecoratorFactory(): AlgorithmManagerDecoratorFactory { return $this->algorithmManagerDecoratorFactory ??= new AlgorithmManagerDecoratorFactory(); } + public function algorithmManagerDecorator(): AlgorithmManagerDecorator { return $this->algorithmManagerDecorator ??= $this->algorithmManagerDecoratorFactory() ->build($this->supportedAlgorithms); } + public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDecoratorFactory { return $this->jwsSerializerManagerDecoratorFactory ??= new JwsSerializerManagerDecoratorFactory(); } + public function jwsDecoratorBuilderFactory(): JwsDecoratorBuilderFactory { return $this->jwsDecoratorBuilderFactory ??= new JwsDecoratorBuilderFactory(); } + public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory { return $this->jwsVerifierDecoratorFactory ??= new JwsVerifierDecoratorFactory(); } + public function jwksDecoratorFactory(): JwksDecoratorFactory { return $this->jwksDecoratorFactory ??= new JwksDecoratorFactory(); } + public function dateIntervalDecoratorFactory(): DateIntervalDecoratorFactory { if (is_null($this->dateIntervalDecoratorFactory)) { @@ -309,11 +332,13 @@ public function dateIntervalDecoratorFactory(): DateIntervalDecoratorFactory return $this->dateIntervalDecoratorFactory; } + public function trustChainBagFactory(): TrustChainBagFactory { return $this->trustChainBagFactory ??= new TrustChainBagFactory(); } + public function httpClientDecoratorFactory(): HttpClientDecoratorFactory { if (is_null($this->httpClientDecoratorFactory)) { @@ -323,6 +348,7 @@ public function httpClientDecoratorFactory(): HttpClientDecoratorFactory return $this->httpClientDecoratorFactory; } + public function cacheDecoratorFactory(): CacheDecoratorFactory { if (is_null($this->cacheDecoratorFactory)) { @@ -332,31 +358,37 @@ public function cacheDecoratorFactory(): CacheDecoratorFactory return $this->cacheDecoratorFactory; } + public function maxCacheDurationDecorator(): DateIntervalDecorator { return $this->maxCacheDurationDecorator; } + public function supportedAlgorithms(): SupportedAlgorithms { return $this->supportedAlgorithms; } + public function supportedSerializers(): SupportedSerializers { return $this->supportedSerializers; } + public function maxTrustChainDepth(): int { return $this->maxTrustChainDepth; } + public function cacheDecorator(): ?CacheDecorator { return $this->cacheDecorator; } + public function artifactFetcher(): ArtifactFetcher { return $this->artifactFetcher ??= new ArtifactFetcher( @@ -366,6 +398,7 @@ public function artifactFetcher(): ArtifactFetcher ); } + public function claimFactory(): ClaimFactory { return $this->claimFactory ??= new ClaimFactory( diff --git a/src/Federation/Claims/TrustMarkOwnersClaimBag.php b/src/Federation/Claims/TrustMarkOwnersClaimBag.php index 4a88db7..b28227a 100644 --- a/src/Federation/Claims/TrustMarkOwnersClaimBag.php +++ b/src/Federation/Claims/TrustMarkOwnersClaimBag.php @@ -11,11 +11,13 @@ class TrustMarkOwnersClaimBag implements JsonSerializable /** @var array */ protected array $trustMarkOwnersClaimValues = []; + public function __construct(TrustMarkOwnersClaimValue ...$trustMarkOwnersClaimValues) { $this->add(...$trustMarkOwnersClaimValues); } + public function add(TrustMarkOwnersClaimValue ...$trustMarkOwnersClaimValues): void { foreach ($trustMarkOwnersClaimValues as $trustMarkOwnersClaimValue) { @@ -24,16 +26,19 @@ public function add(TrustMarkOwnersClaimValue ...$trustMarkOwnersClaimValues): v } } + public function has(string $trustMarkType): bool { return isset($this->trustMarkOwnersClaimValues[$trustMarkType]); } + public function get(string $trustMarkType): ?TrustMarkOwnersClaimValue { return $this->trustMarkOwnersClaimValues[$trustMarkType] ?? null; } + /** * @return array */ @@ -42,6 +47,7 @@ public function getAll(): array return $this->trustMarkOwnersClaimValues; } + /** * @return array> */ diff --git a/src/Federation/Claims/TrustMarkOwnersClaimValue.php b/src/Federation/Claims/TrustMarkOwnersClaimValue.php index 749c95a..85e930c 100644 --- a/src/Federation/Claims/TrustMarkOwnersClaimValue.php +++ b/src/Federation/Claims/TrustMarkOwnersClaimValue.php @@ -23,6 +23,7 @@ public function __construct( ) { } + /** * @return non-empty-string */ @@ -31,6 +32,7 @@ public function getTrustMarkType(): string return $this->trustMarkType; } + /** * @return non-empty-string */ @@ -39,11 +41,13 @@ public function getSubject(): string return $this->subject; } + public function getJwks(): JwksClaim { return $this->jwks; } + /** * @return array */ @@ -52,6 +56,7 @@ public function getOtherClaims(): array return $this->otherClaims; } + /** * @return array */ diff --git a/src/Federation/Claims/TrustMarksClaimBag.php b/src/Federation/Claims/TrustMarksClaimBag.php index 654a582..a1cecbd 100644 --- a/src/Federation/Claims/TrustMarksClaimBag.php +++ b/src/Federation/Claims/TrustMarksClaimBag.php @@ -13,16 +13,19 @@ class TrustMarksClaimBag implements JsonSerializable */ protected array $trustMarksClaimValues = []; + public function __construct(TrustMarksClaimValue ...$trustMarksClaimValue) { $this->add(...$trustMarksClaimValue); } + public function add(TrustMarksClaimValue ...$trustMarksClaimValue): void { $this->trustMarksClaimValues = array_merge($this->trustMarksClaimValues, $trustMarksClaimValue); } + /** * @return \SimpleSAML\OpenID\Federation\Claims\TrustMarksClaimValue[] */ @@ -31,6 +34,7 @@ public function getAll(): array return $this->trustMarksClaimValues; } + /** * @param non-empty-string $trustMarkType * @return \SimpleSAML\OpenID\Federation\Claims\TrustMarksClaimValue[] @@ -43,6 +47,7 @@ public function getAllFor(string $trustMarkType): array )); } + public function getFirstFor(string $trustMarkType): ?TrustMarksClaimValue { foreach ($this->trustMarksClaimValues as $trustMarkClaim) { @@ -54,6 +59,7 @@ public function getFirstFor(string $trustMarkType): ?TrustMarksClaimValue return null; } + /** * @return array */ diff --git a/src/Federation/Claims/TrustMarksClaimValue.php b/src/Federation/Claims/TrustMarksClaimValue.php index 89604c6..fc217ac 100644 --- a/src/Federation/Claims/TrustMarksClaimValue.php +++ b/src/Federation/Claims/TrustMarksClaimValue.php @@ -25,6 +25,7 @@ public function __construct( ) { } + /** * @return non-empty-string */ @@ -33,6 +34,7 @@ public function getTrustMarkType(): string return $this->trustMarkType; } + /** * @return non-empty-string */ @@ -41,6 +43,7 @@ public function getTrustMark(): string return $this->trustMark; } + /** * @return array */ @@ -49,6 +52,7 @@ public function getOtherClaims(): array return $this->otherClaims; } + /** * @return array */ diff --git a/src/Federation/EntityStatement.php b/src/Federation/EntityStatement.php index 619c4d7..018d7c6 100644 --- a/src/Federation/EntityStatement.php +++ b/src/Federation/EntityStatement.php @@ -25,6 +25,7 @@ public function getIssuer(): string return parent::getIssuer() ?? throw new EntityStatementException('No Issuer claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -35,6 +36,7 @@ public function getSubject(): string return parent::getSubject() ?? throw new EntityStatementException('No Subject claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -44,6 +46,7 @@ public function getIssuedAt(): int return parent::getIssuedAt() ?? throw new EntityStatementException('No Issued At claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -54,6 +57,7 @@ public function getExpirationTime(): int return parent::getExpirationTime() ?? throw new EntityStatementException('No Expiration Time claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -69,6 +73,7 @@ public function getJwks(): JwksClaim return $this->claimFactory->buildJwks($jwks); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -85,6 +90,7 @@ public function getType(): string return $typ; } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -119,6 +125,7 @@ public function getAuthorityHints(): ?array return $this->helpers->type()->ensureArrayWithValuesAsNonEmptyStrings($authorityHints, $claimKey); } + /** * @return ?array * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -142,6 +149,7 @@ public function getMetadata(): ?array return $this->helpers->type()->ensureArrayWithKeysAsStrings($metadata); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException @@ -170,6 +178,7 @@ public function getMetadataPolicy(): ?array return $metadataPolicy; } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -202,6 +211,7 @@ public function getTrustMarks(): ?TrustMarksClaimBag return $trustMarkClaimBag; } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -223,6 +233,7 @@ public function getTrustMarkOwners(): ?TrustMarkOwnersClaimBag return $this->claimFactory->forFederation()->buildTrustMarkOwnersClaimBagFrom($trustMarkOwnersClaimData); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -233,6 +244,7 @@ public function getKeyId(): string return parent::getKeyId() ?? throw new EntityStatementException('No KeyId header claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -256,6 +268,7 @@ public function getFederationFetchEndpoint(): ?string return $this->helpers->type()->ensureNonEmptyString($federationFetchEndpoint); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -279,6 +292,7 @@ public function getFederationTrustMarkEndpoint(): ?string return $this->helpers->type()->ensureNonEmptyString($federationTrustMarkEndpoint); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -288,6 +302,7 @@ public function isConfiguration(): bool return $this->getIssuer() === $this->getSubject(); } + /** * @param array|null $jwks * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException @@ -303,6 +318,7 @@ public function verifyWithKeySet(?array $jwks = null, int $signatureIndex = 0): parent::verifyWithKeySet($jwks, $signatureIndex); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException diff --git a/src/Federation/EntityStatementFetcher.php b/src/Federation/EntityStatementFetcher.php index 74460c2..e7cc433 100644 --- a/src/Federation/EntityStatementFetcher.php +++ b/src/Federation/EntityStatementFetcher.php @@ -28,16 +28,19 @@ public function __construct( parent::__construct($parsedJwsFactory, $artifactFetcher, $maxCacheDuration, $helpers, $logger); } + protected function buildJwsInstance(string $token): EntityStatement { return $this->parsedJwsFactory->fromToken($token); } + public function getExpectedContentTypeHttpHeader(): string { return ContentTypesEnum::ApplicationEntityStatementJwt->value; } + /** * Fetch entity statement from a well-known URI. By default, this will be openid-federation (entity configuration). * Fetch will first check if the entity statement is available in cache. If not, it will do a network fetch. @@ -59,6 +62,7 @@ public function fromCacheOrWellKnownEndpoint( return $this->fromCacheOrNetwork($wellKnownUri); } + /** * @param \SimpleSAML\OpenID\Federation\EntityStatement $entityConfiguration Entity from which to use the fetch * endpoint (issuer). @@ -87,6 +91,7 @@ public function fromCacheOrFetchEndpoint( ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\FetchException @@ -96,6 +101,7 @@ public function fromCacheOrNetwork(string $uri): EntityStatement return $this->fromCache($uri) ?? $this->fromNetwork($uri); } + /** * Fetch entity statement from cache, if available. URI is used as cache key. * @@ -125,6 +131,7 @@ public function fromCache(string $uri): ?EntityStatement // @codeCoverageIgnoreEnd } + /** * Fetch entity statement from network. Each successful fetch will be cached, with URI being used as a cache key. * diff --git a/src/Federation/Factories/EntityStatementFactory.php b/src/Federation/Factories/EntityStatementFactory.php index 4f43349..d3dd577 100644 --- a/src/Federation/Factories/EntityStatementFactory.php +++ b/src/Federation/Factories/EntityStatementFactory.php @@ -30,6 +30,7 @@ public function fromToken(string $token): EntityStatement ); } + /** * @param array $payload * @param array $header diff --git a/src/Federation/Factories/FederationClaimFactory.php b/src/Federation/Factories/FederationClaimFactory.php index d2c0073..2a9d1cc 100644 --- a/src/Federation/Factories/FederationClaimFactory.php +++ b/src/Federation/Factories/FederationClaimFactory.php @@ -21,6 +21,7 @@ public function __construct( ) { } + /** * @param array $otherClaims * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -37,6 +38,7 @@ public function buildTrustMarksClaimValue( return new TrustMarksClaimValue($trustMarkType, $trustMark, $otherClaims); } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -71,11 +73,13 @@ public function buildTrustMarksClaimValueFrom(mixed $trustMarksClaimData): Trust ); } + public function buildTrustMarksClaimBag(TrustMarksClaimValue ...$trustMarksClaimValues): TrustMarksClaimBag { return new TrustMarksClaimBag(...$trustMarksClaimValues); } + public function buildTrustMarkOwnersClaimValue( mixed $trustMarkType, mixed $subject, @@ -95,6 +99,7 @@ public function buildTrustMarkOwnersClaimValue( ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkException @@ -141,6 +146,7 @@ public function buildTrustMarkOwnersClaimBagFrom(mixed $trustMarkOwnersClaimData return $this->buildTrustMarkOwnersClaimBag(...$trustMarkOwnersClaimValues); } + public function buildTrustMarkOwnersClaimBag( TrustMarkOwnersClaimValue ...$trustMarkOwnersClaimValues, ): TrustMarkOwnersClaimBag { diff --git a/src/Federation/Factories/RequestObjectFactory.php b/src/Federation/Factories/RequestObjectFactory.php index c79691f..0ed9126 100644 --- a/src/Federation/Factories/RequestObjectFactory.php +++ b/src/Federation/Factories/RequestObjectFactory.php @@ -28,6 +28,7 @@ public function fromToken(string $token): RequestObject ); } + /** * @param array $payload * @param array $header diff --git a/src/Federation/Factories/TrustChainFactory.php b/src/Federation/Factories/TrustChainFactory.php index 15e3897..c1238a4 100644 --- a/src/Federation/Factories/TrustChainFactory.php +++ b/src/Federation/Factories/TrustChainFactory.php @@ -23,6 +23,7 @@ public function __construct( ) { } + public function empty(): TrustChain { return new TrustChain( @@ -33,6 +34,7 @@ public function empty(): TrustChain ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -64,6 +66,7 @@ public function fromStatements(EntityStatement ...$statements): TrustChain return $trustChain; } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException @@ -78,6 +81,7 @@ public function fromTokens(string ...$tokens): TrustChain return $this->fromStatements(...$statements); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException diff --git a/src/Federation/Factories/TrustMarkDelegationFactory.php b/src/Federation/Factories/TrustMarkDelegationFactory.php index 0696a6e..16978ef 100644 --- a/src/Federation/Factories/TrustMarkDelegationFactory.php +++ b/src/Federation/Factories/TrustMarkDelegationFactory.php @@ -26,6 +26,7 @@ public function fromToken(string $token): TrustMarkDelegation ); } + /** * @param array $payload * @param array $header diff --git a/src/Federation/Factories/TrustMarkFactory.php b/src/Federation/Factories/TrustMarkFactory.php index f3f8837..73e6d2b 100644 --- a/src/Federation/Factories/TrustMarkFactory.php +++ b/src/Federation/Factories/TrustMarkFactory.php @@ -29,6 +29,7 @@ public function fromToken( ); } + /** * @param array $payload * @param array $header diff --git a/src/Federation/MetadataPolicyApplicator.php b/src/Federation/MetadataPolicyApplicator.php index 72da685..1da20af 100644 --- a/src/Federation/MetadataPolicyApplicator.php +++ b/src/Federation/MetadataPolicyApplicator.php @@ -16,6 +16,7 @@ public function __construct( ) { } + /** * @param array> $resolvedMetadataPolicy Resolved (validated) metadata policy. * @param array $metadata @@ -189,6 +190,7 @@ public function for( return $metadata; } + /** * @param array $metadata */ @@ -204,6 +206,7 @@ protected function resolveParameterValueBeforePolicy(array $metadata, string $pa return $value; } + protected function resolveParameterValueAfterPolicy(mixed $value, string $parameter): mixed { // Special case for 'scope' parameter, which needs to be converted to string after policy application. diff --git a/src/Federation/MetadataPolicyResolver.php b/src/Federation/MetadataPolicyResolver.php index 2439960..a6cc216 100644 --- a/src/Federation/MetadataPolicyResolver.php +++ b/src/Federation/MetadataPolicyResolver.php @@ -16,6 +16,7 @@ public function __construct( ) { } + /** * @return array>> * @throws \SimpleSAML\OpenID\Exceptions\MetadataPolicyException @@ -54,6 +55,7 @@ public function ensureFormat(array $metadataPolicies): array return $metadataPolicies; } + /** * @param array>>> $metadataPolicies * @param string[] $criticalMetadataPolicyOperators diff --git a/src/Federation/RequestObject.php b/src/Federation/RequestObject.php index 9d950eb..6caf1e2 100644 --- a/src/Federation/RequestObject.php +++ b/src/Federation/RequestObject.php @@ -20,6 +20,7 @@ public function getAudience(): array return parent::getAudience() ?? throw new RequestObjectException('No Audience claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\RequestObjectException @@ -30,6 +31,7 @@ public function getIssuer(): string return parent::getIssuer() ?? throw new RequestObjectException('No Issuer claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\RequestObjectException @@ -46,6 +48,7 @@ protected function validateSubject(): void } } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\RequestObjectException @@ -56,6 +59,7 @@ public function getJwtId(): string return parent::getJwtId() ?? throw new RequestObjectException('No JWT ID claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -65,6 +69,7 @@ public function getExpirationTime(): int return parent::getExpirationTime() ?? throw new RequestObjectException('No Expiration Time claim found.'); } + /** * @return ?string[] * @throws \SimpleSAML\OpenID\Exceptions\JwsException diff --git a/src/Federation/TrustChain.php b/src/Federation/TrustChain.php index b2f0d95..1f539b6 100644 --- a/src/Federation/TrustChain.php +++ b/src/Federation/TrustChain.php @@ -55,6 +55,7 @@ class TrustChain implements JsonSerializable */ protected array $resolvedMetadata = []; + public function __construct( protected readonly DateIntervalDecorator $timestampValidationLeewayDecorator, protected readonly MetadataPolicyResolver $metadataPolicyResolver, @@ -63,6 +64,7 @@ public function __construct( ) { } + /** * Check if the trust chain is (currently) empty, meaning there are no entity statements present in the chain. */ @@ -71,6 +73,7 @@ public function isEmpty(): bool return $this->entities === []; } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException */ @@ -82,6 +85,7 @@ public function getResolvedExpirationTime(): int return $this->expirationTime ?? throw new TrustChainException('Empty expiration time encountered.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException */ @@ -95,6 +99,7 @@ public function getResolvedLeaf(): EntityStatement return $leaf; } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException */ @@ -105,6 +110,7 @@ public function getResolvedImmediateSuperior(): ?EntityStatement return $this->entities[1] ?? null; } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException */ @@ -120,6 +126,7 @@ public function getResolvedTrustAnchor(): EntityStatement return $trustAnchor; } + /** * @return ?array * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException @@ -140,6 +147,7 @@ public function getResolvedMetadata(EntityTypesEnum $entityTypeEnum): ?array return $this->resolvedMetadata[$entityTypeEnum->value] ?? null; } + /** * Get resolved chain length. * @@ -152,6 +160,7 @@ public function getResolvedLength(): int return count($this->entities); } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -168,6 +177,7 @@ public function addLeaf(EntityStatement $entityStatement): void $this->updateExpirationTime($entityStatement); } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -186,6 +196,7 @@ public function addSubordinate(EntityStatement $entityStatement): void $this->gatherMetadataPolicies($entityStatement); } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -205,6 +216,7 @@ public function addTrustAnchor(EntityStatement $entityStatement): void $this->isResolved = true; } + /** * Add a Trust Anchor Entity Configuration to create a single entity statement chain. This accommodates a special * case for the Trust Chain of the Trust Anchor itself. @@ -226,6 +238,7 @@ public function addForTrustAnchorOnly(EntityStatement $entityStatement): void $this->isResolved = true; } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -244,6 +257,7 @@ protected function updateExpirationTime(EntityStatement $entityStatement): void $this->validateExpirationTime(); } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException */ @@ -260,6 +274,7 @@ public function validateExpirationTime(): void } } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException */ @@ -270,6 +285,7 @@ protected function validateIsResolved(): void } } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException */ @@ -280,6 +296,7 @@ protected function validateIsNotResolved(): void } } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException */ @@ -290,6 +307,7 @@ protected function validateIsEmpty(): void } } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException */ @@ -300,6 +318,7 @@ protected function validateIsNotEmpty(): void } } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException */ @@ -313,6 +332,7 @@ protected function validateAtLeastNumberOfEntities(int $count): void } } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ @@ -327,6 +347,7 @@ protected function validateConfigurationStatement(EntityStatement $entityStateme $entityStatement->verifyWithKeySet(); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -356,6 +377,7 @@ protected function validateSubordinateStatement(EntityStatement $entityStatement $previousStatement->verifyWithKeySet($entityStatement->getJwks()->getValue()); } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException * @@ -371,6 +393,7 @@ public function jsonSerialize(): array ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ @@ -383,6 +406,7 @@ protected function gatherCriticalMetadataPolicyOperators(EntityStatement $entity $this->criticalMetadataPolicyOperators = array_merge($this->criticalMetadataPolicyOperators, $operators); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\MetadataPolicyException @@ -398,6 +422,7 @@ protected function gatherMetadataPolicies(EntityStatement $entityStatement): voi } } + /** * @throws \SimpleSAML\OpenID\Exceptions\MetadataPolicyException * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException @@ -470,6 +495,7 @@ protected function resolveMetadataFor(EntityTypesEnum $entityTypeEnum): void ); } + /** * @return \SimpleSAML\OpenID\Federation\EntityStatement[] */ diff --git a/src/Federation/TrustChainBag.php b/src/Federation/TrustChainBag.php index 4d95903..93416c6 100644 --- a/src/Federation/TrustChainBag.php +++ b/src/Federation/TrustChainBag.php @@ -11,11 +11,13 @@ class TrustChainBag /** @var \SimpleSAML\OpenID\Federation\TrustChain[] */ protected array $trustChains = []; + public function __construct(TrustChain $trustChain, TrustChain ...$trustChains) { $this->add($trustChain, ...$trustChains); } + public function add(TrustChain $trustChain, TrustChain ...$trustChains): void { $this->trustChains[] = $trustChain; @@ -28,6 +30,7 @@ public function add(TrustChain $trustChain, TrustChain ...$trustChains): void ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException */ @@ -37,6 +40,7 @@ public function getShortest(): TrustChain return $shortestChain; } + /** * Get the shortest Trust Chain prioritized by Trust Anchor ID. Returns null if none of the given Trust Anchor IDs * is found. @@ -65,6 +69,7 @@ public function getShortestByTrustAnchorPriority(string $trustAnchorId, string . $prioritizedChain : null; } + /** * @return \SimpleSAML\OpenID\Federation\TrustChain[] */ @@ -73,6 +78,7 @@ public function getAll(): array return $this->trustChains; } + public function getCount(): int { return count($this->trustChains); diff --git a/src/Federation/TrustChainResolver.php b/src/Federation/TrustChainResolver.php index 4b3ada7..72a105b 100644 --- a/src/Federation/TrustChainResolver.php +++ b/src/Federation/TrustChainResolver.php @@ -18,6 +18,7 @@ class TrustChainResolver protected int $maxAuthorityHints; + public function __construct( protected readonly EntityStatementFetcher $entityStatementFetcher, protected readonly TrustChainFactory $trustChainFactory, @@ -32,6 +33,7 @@ public function __construct( $this->maxAuthorityHints = min(12, max(1, $maxAuthorityHints)); } + /** * Get entity configuration statements chains up to given Trust Anchors. * @@ -162,6 +164,7 @@ public function getConfigurationChains( return $configurationChains; } + /** * Resolve trust chains for given entity and trust anchor IDs. * @@ -301,6 +304,7 @@ public function for(string $entityId, array $validTrustAnchorIds): TrustChainBag return $trustChainBag; } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustChainException * @phpstan-ignore missingType.iterableValue (We validate it here) @@ -324,11 +328,13 @@ protected function validateStart(string $entityId, array $validTrustAnchorIds): } } + public function getMaxTrustChainDepth(): int { return $this->maxTrustChainDepth; } + /** * @throws \Psr\SimpleCache\InvalidArgumentException * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException diff --git a/src/Federation/TrustMark.php b/src/Federation/TrustMark.php index b9d4969..9ab7e8b 100644 --- a/src/Federation/TrustMark.php +++ b/src/Federation/TrustMark.php @@ -39,6 +39,7 @@ public function __construct( ); } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -49,6 +50,7 @@ public function getIssuer(): string return parent::getIssuer() ?? throw new TrustMarkException('No Issuer claim found.'); } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -59,6 +61,7 @@ public function getSubject(): string return parent::getSubject() ?? throw new TrustMarkException('No Subject claim found.'); } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -74,6 +77,7 @@ public function getTrustMarkType(): string return $this->helpers->type()->ensureNonEmptyString($trustMarkType); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkException @@ -83,6 +87,7 @@ public function getIssuedAt(): int return parent::getIssuedAt() ?? throw new TrustMarkException('No Issued At claim found.'); } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -97,6 +102,7 @@ public function getLogoUri(): ?string $this->helpers->type()->ensureNonEmptyString($logoUri, ClaimsEnum::LogoUri->value); } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -111,6 +117,7 @@ public function getReference(): ?string $this->helpers->type()->ensureNonEmptyString($ref, ClaimsEnum::Ref->value); } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -125,6 +132,7 @@ public function getDelegation(): ?string $this->helpers->type()->ensureNonEmptyString($delegation, ClaimsEnum::Delegation->value); } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -135,6 +143,7 @@ public function getKeyId(): string return parent::getKeyId() ?? throw new TrustMarkException('No KeyId header claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -155,6 +164,7 @@ public function getType(): string return $typ; } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkException diff --git a/src/Federation/TrustMarkDelegation.php b/src/Federation/TrustMarkDelegation.php index 3a5af04..bc3f300 100644 --- a/src/Federation/TrustMarkDelegation.php +++ b/src/Federation/TrustMarkDelegation.php @@ -21,6 +21,7 @@ public function getIssuer(): string return parent::getIssuer() ?? throw new TrustMarkDelegationException('No Issuer claim found.'); } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -32,6 +33,7 @@ public function getSubject(): string return parent::getSubject() ?? throw new TrustMarkDelegationException('No Subject claim found.'); } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -48,6 +50,7 @@ public function getTrustMarkType(): string return $this->helpers->type()->ensureNonEmptyString($trustMarkType); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException @@ -57,6 +60,7 @@ public function getIssuedAt(): int return parent::getIssuedAt() ?? throw new TrustMarkDelegationException('No Issued At claim found.'); } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -71,6 +75,7 @@ public function getReference(): ?string $this->helpers->type()->ensureNonEmptyString($ref, ClaimsEnum::Ref->value); } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -81,6 +86,7 @@ public function getKeyId(): string return parent::getKeyId() ?? throw new TrustMarkDelegationException('No KeyId header claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -97,6 +103,7 @@ public function getType(): string return $typ; } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException diff --git a/src/Federation/TrustMarkFetcher.php b/src/Federation/TrustMarkFetcher.php index 5cdfa69..6bc34d6 100644 --- a/src/Federation/TrustMarkFetcher.php +++ b/src/Federation/TrustMarkFetcher.php @@ -27,16 +27,19 @@ public function __construct( parent::__construct($parsedJwsFactory, $artifactFetcher, $maxCacheDuration, $helpers, $logger); } + protected function buildJwsInstance(string $token): TrustMark { return $this->parsedJwsFactory->fromToken($token); } + public function getExpectedContentTypeHttpHeader(): string { return ContentTypesEnum::ApplicationTrustMarkJwt->value; } + /** * @param \SimpleSAML\OpenID\Federation\EntityStatement $entityConfiguration Entity from which to use the * federation_trust_mark_endpoint (issuer). @@ -68,6 +71,7 @@ public function fromCacheOrFederationTrustMarkEndpoint( ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\FetchException @@ -77,6 +81,7 @@ public function fromCacheOrNetwork(string $uri): TrustMark return $this->fromCache($uri) ?? $this->fromNetwork($uri); } + /** * Fetch Trust Mark from cache, if available. URI is used as cache key. * @@ -106,6 +111,7 @@ public function fromCache(string $uri): ?TrustMark // @codeCoverageIgnoreEnd } + /** * Fetch Trust Mark from network. Each successful fetch will be cached, with URI being used as a cache key. * diff --git a/src/Federation/TrustMarkValidator.php b/src/Federation/TrustMarkValidator.php index e3115c4..d8fc9fe 100644 --- a/src/Federation/TrustMarkValidator.php +++ b/src/Federation/TrustMarkValidator.php @@ -26,6 +26,7 @@ public function __construct( ) { } + /** * If cached, validation has already been performed. * @@ -84,6 +85,7 @@ public function isValidationCachedFor( return false; } + /** * @param non-empty-string $trustMarkType * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException @@ -116,6 +118,7 @@ public function fromCacheOrDoForTrustMarkType( ); } + /** * @param non-empty-string $trustMarkType * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException @@ -236,6 +239,7 @@ public function doForTrustMarkType( ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -269,6 +273,7 @@ public function fromCacheOrDoForTrustMarksClaimValue( ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -293,6 +298,7 @@ public function doForTrustMarksClaimValue( ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkException @@ -358,6 +364,7 @@ public function validateTrustMarksClaimValue( return $trustMark; } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -389,6 +396,7 @@ public function fromCacheOrDoForTrustMark( ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException @@ -475,6 +483,7 @@ public function doForTrustMark( } } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -506,6 +515,7 @@ public function validateSubjectClaim( ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -553,6 +563,7 @@ public function validateTrustChainForTrustMarkIssuer( return $trustMarkIssuerChain; } + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -582,6 +593,7 @@ public function validateTrustMarkSignature( $this->logger?->debug('Trust Mark signature validated.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException diff --git a/src/Helpers.php b/src/Helpers.php index c3ee6fb..bcfda27 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -31,41 +31,49 @@ class Helpers protected static ?Random $random = null; + public function url(): Url { return self::$url ??= new Url(); } + public function json(): Json { return self::$json ??= new Json(); } + public function arr(): Arr { return self::$arr ??= new Arr(); } + public function type(): Type { return self::$type ??= new Type(); } + public function dateTime(): DateTime { return self::$dateTime ??= new DateTime(); } + public function base64Url(): Base64Url { return self::$base64Url ??= new Base64Url(); } + public function hash(): Hash { return self::$hash ??= new Hash(); } + public function random(): Random { return self::$random ??= new Random(); diff --git a/src/Helpers/Arr.php b/src/Helpers/Arr.php index 326aaf9..cd14b32 100644 --- a/src/Helpers/Arr.php +++ b/src/Helpers/Arr.php @@ -10,6 +10,7 @@ class Arr { public const MAX_DEPTH = 99; + /** * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException */ @@ -26,6 +27,7 @@ public function validateMaxDepth(int $depth): void } } + /** * Ensure the existence of nested arrays for given keys. Note that this will create / overwrite any non-array * nested values and make them an array. @@ -50,6 +52,7 @@ public function ensureArrayDepth(array &$array, int|string ...$keys): void $this->ensureArrayDepth($array[$key], ...$keys); } + /** * Get nested value reference at a given path. Creates nested arrays dynamically if the key is not present. * @@ -84,6 +87,7 @@ public function &getNestedValueReference(array &$array, int|string ...$keys): mi return $nested; } + /** * Set a value at a path. * @@ -101,6 +105,7 @@ public function setNestedValue(array &$array, mixed $value, int|string ...$keys) $reference = $value; } + /** * @param mixed[] $array * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException @@ -123,6 +128,7 @@ public function addNestedValue(array &$array, mixed $value, int|string ...$keys) $reference[] = $value; } + /** * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException * @param mixed[] $array @@ -150,6 +156,7 @@ public function getNestedValue(array $array, int|string ...$keys): mixed return $this->getNestedValue($nestedArray, ...$keys); } + /** * @param mixed[] $array */ @@ -159,6 +166,7 @@ public function isAssociative(array $array): bool return array_keys($array) !== range(0, count($array) - 1); } + /** * Is an array of arrays. * @param mixed[] $array @@ -174,6 +182,7 @@ public function isOfArrays(array $array): bool return true; } + /** * Recursively check if an array contains the key, at any level. * @@ -194,6 +203,7 @@ public function containsKey(array $array, string|int $key): bool return false; } + /** * Recursively sort an array by keys if keys are strings and values if keys are numeric. * diff --git a/src/Helpers/Base64Url.php b/src/Helpers/Base64Url.php index 5566116..d6b7344 100644 --- a/src/Helpers/Base64Url.php +++ b/src/Helpers/Base64Url.php @@ -26,6 +26,7 @@ public function encode(string $data): string return rtrim(strtr($base64, '+/', '-_'), '='); } + /** * Decode data from Base64URL format. * diff --git a/src/Helpers/DateTime.php b/src/Helpers/DateTime.php index d003972..eb803ce 100644 --- a/src/Helpers/DateTime.php +++ b/src/Helpers/DateTime.php @@ -39,6 +39,7 @@ public function fromXsDateTime(string $input, ?DateTimeZone $tz = null): DateTim return new DateTimeImmutable($input, $tz); } + /** * @throws \Exception */ @@ -47,6 +48,7 @@ public function getUtc(string $datetime = 'now'): DateTimeImmutable return new DateTimeImmutable($datetime, new DateTimeZone('UTC')); } + /** * @throws \Exception */ diff --git a/src/Helpers/Json.php b/src/Helpers/Json.php index 82bd0d5..5c7f9ad 100644 --- a/src/Helpers/Json.php +++ b/src/Helpers/Json.php @@ -14,6 +14,7 @@ public function decode(string $json): mixed return json_decode($json, true, 512, JSON_THROW_ON_ERROR); } + /** * @throws \JsonException */ diff --git a/src/Helpers/Type.php b/src/Helpers/Type.php index 028e362..c4ec90c 100644 --- a/src/Helpers/Type.php +++ b/src/Helpers/Type.php @@ -33,6 +33,7 @@ public function ensureString(mixed $value, ?string $context = null): string throw new InvalidValueException($error); } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -54,6 +55,7 @@ public function ensureNonEmptyString(mixed $value, ?string $context = null): str throw new InvalidValueException($error); } + /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @return mixed[] @@ -86,6 +88,7 @@ public function ensureArray(mixed $value, ?string $context = null): array throw new InvalidValueException($error); } + /** * @return array * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -104,6 +107,7 @@ public function ensureArrayWithKeysAsStrings(mixed $value, ?string $context = nu ); } + /** * @return array * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -122,6 +126,7 @@ public function ensureArrayWithKeysAsNonEmptyStrings(mixed $value, ?string $cont ); } + /** * @return string[] * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -137,6 +142,7 @@ public function ensureArrayWithValuesAsStrings(mixed $value, ?string $context = ); } + /** * @return non-empty-string[] * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -152,6 +158,7 @@ public function ensureArrayWithValuesAsNonEmptyStrings(mixed $value, ?string $co ); } + /** * @return array * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -174,6 +181,7 @@ public function ensureArrayWithKeysAndValuesAsStrings(mixed $value, ?string $con ); } + /** * @return array * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -196,6 +204,7 @@ public function ensureArrayWithKeysAndValuesAsNonEmptyStrings(mixed $value, ?str ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException */ @@ -218,6 +227,7 @@ public function ensureInt(mixed $value, ?string $context = null): int throw new InvalidValueException($error); } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -240,6 +250,7 @@ public function enforceRegex( return $value; } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -264,6 +275,7 @@ public function enforceUri( return $value; } + /** * @param mixed[] $array * @return array @@ -287,6 +299,7 @@ public function enforceArrayOfArrays(array $array, ?string $context = null): arr return $array; } + /** * @param mixed[] $array * @return non-empty-array @@ -306,6 +319,7 @@ public function enforceNonEmptyArray(array $array, ?string $context = null): arr return $array; } + /** * @param mixed[] $array * @return non-empty-array @@ -320,6 +334,7 @@ public function enforceNonEmptyArrayWithValuesAsNonEmptyStrings(array $array, ?s return $array; } + /** * @param mixed[] $array * @return non-empty-array @@ -347,6 +362,7 @@ public function enforceNonEmptyArrayOfNonEmptyArrays(array $array, ?string $cont return $array; } + protected function prepareErrorMessage(string $message, mixed $value, ?string $context = null): string { return $message . diff --git a/src/Helpers/Url.php b/src/Helpers/Url.php index 370341f..0f6e8e5 100644 --- a/src/Helpers/Url.php +++ b/src/Helpers/Url.php @@ -12,6 +12,7 @@ public function isValid(string $url): bool return (bool)filter_var($url, FILTER_VALIDATE_URL); } + /** * Add (new) params to URL while preserving existing ones (if any). * @param array $params diff --git a/src/Jwk.php b/src/Jwk.php index 2a6d956..6660c12 100644 --- a/src/Jwk.php +++ b/src/Jwk.php @@ -10,6 +10,7 @@ class Jwk { protected ?JwkDecoratorFactory $jwkDecoratorFactory = null; + public function jwkDecoratorFactory(): JwkDecoratorFactory { return $this->jwkDecoratorFactory ??= new JwkDecoratorFactory(); diff --git a/src/Jwk/Factories/JwkDecoratorFactory.php b/src/Jwk/Factories/JwkDecoratorFactory.php index 8ac479c..261d995 100644 --- a/src/Jwk/Factories/JwkDecoratorFactory.php +++ b/src/Jwk/Factories/JwkDecoratorFactory.php @@ -20,6 +20,7 @@ public function fromData(array $data): JwkDecorator ); } + /** * @param non-empty-string $path * @param mixed[] $additionalData @@ -34,6 +35,7 @@ public function fromPkcs1Or8KeyFile( ); } + /** * @param non-empty-string $key * @param mixed[] $additionalData @@ -48,6 +50,7 @@ public function fromPkcs1Or8Key( ); } + /** * @param non-empty-string $path * @param mixed[] $additionalData @@ -62,6 +65,7 @@ public function fromPkcs12CertificateFile( ); } + /** * @param non-empty-string $path * @param mixed[] $additionalData @@ -75,6 +79,7 @@ public function fromX509CertificateFile( ); } + /** * @param non-empty-string $certificate * @param mixed[] $additionalData diff --git a/src/Jwk/JwkDecorator.php b/src/Jwk/JwkDecorator.php index 5266005..618546b 100644 --- a/src/Jwk/JwkDecorator.php +++ b/src/Jwk/JwkDecorator.php @@ -13,6 +13,7 @@ public function __construct( ) { } + public function jwk(): JWK { return $this->jwk; diff --git a/src/Jwks.php b/src/Jwks.php index e75597a..64f55e8 100644 --- a/src/Jwks.php +++ b/src/Jwks.php @@ -69,6 +69,7 @@ class Jwks protected ?AlgorithmManagerDecorator $algorithmManagerDecorator = null; + public function __construct( protected readonly SupportedAlgorithms $supportedAlgorithms = new SupportedAlgorithms(), protected readonly SupportedSerializers $supportedSerializers = new SupportedSerializers(), @@ -85,11 +86,13 @@ public function __construct( $this->httpClientDecorator = $this->httpClientDecoratorFactory()->build($httpClient); } + public function jwksDecoratorFactory(): JwksDecoratorFactory { return $this->jwksDecoratorFactory ??= new JwksDecoratorFactory(); } + public function signedJwksFactory(): SignedJwksFactory { return $this->signedJwksFactory ??= new SignedJwksFactory( @@ -103,6 +106,7 @@ public function signedJwksFactory(): SignedJwksFactory ); } + public function jwksFetcher(): JwksFetcher { return $this->jwksFetcher ??= new JwksFetcher( @@ -117,11 +121,13 @@ public function jwksFetcher(): JwksFetcher ); } + public function helpers(): Helpers { return $this->helpers ??= new Helpers(); } + public function algorithmManagerDecoratorFactory(): AlgorithmManagerDecoratorFactory { if (is_null($this->algorithmManagerDecoratorFactory)) { @@ -131,6 +137,7 @@ public function algorithmManagerDecoratorFactory(): AlgorithmManagerDecoratorFac return $this->algorithmManagerDecoratorFactory; } + public function algorithmManagerDecorator(): AlgorithmManagerDecorator { return $this->algorithmManagerDecorator ??= $this->algorithmManagerDecoratorFactory()->build( @@ -138,6 +145,7 @@ public function algorithmManagerDecorator(): AlgorithmManagerDecorator ); } + public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDecoratorFactory { if (is_null($this->jwsSerializerManagerDecoratorFactory)) { @@ -147,6 +155,7 @@ public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDeco return $this->jwsSerializerManagerDecoratorFactory; } + public function jwsDecoratorBuilderFactory(): JwsDecoratorBuilderFactory { if (is_null($this->jwsDecoratorBuilderFactory)) { @@ -156,6 +165,7 @@ public function jwsDecoratorBuilderFactory(): JwsDecoratorBuilderFactory return $this->jwsDecoratorBuilderFactory; } + public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory { if (is_null($this->jwsVerifierDecoratorFactory)) { @@ -165,6 +175,7 @@ public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory return $this->jwsVerifierDecoratorFactory; } + public function dateIntervalDecoratorFactory(): DateIntervalDecoratorFactory { if (is_null($this->dateIntervalDecoratorFactory)) { @@ -174,6 +185,7 @@ public function dateIntervalDecoratorFactory(): DateIntervalDecoratorFactory return $this->dateIntervalDecoratorFactory; } + public function cacheDecoratorFactory(): CacheDecoratorFactory { if (is_null($this->cacheDecoratorFactory)) { @@ -183,6 +195,7 @@ public function cacheDecoratorFactory(): CacheDecoratorFactory return $this->cacheDecoratorFactory; } + public function httpClientDecoratorFactory(): HttpClientDecoratorFactory { if (is_null($this->httpClientDecoratorFactory)) { @@ -192,6 +205,7 @@ public function httpClientDecoratorFactory(): HttpClientDecoratorFactory return $this->httpClientDecoratorFactory; } + public function jwsVerifierDecorator(): JwsVerifierDecorator { return $this->jwsVerifierDecorator ??= $this->jwsVerifierDecoratorFactory()->build( @@ -199,6 +213,7 @@ public function jwsVerifierDecorator(): JwsVerifierDecorator ); } + public function jwsDecoratorBuilder(): JwsDecoratorBuilder { return $this->jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderFactory()->build( @@ -208,12 +223,14 @@ public function jwsDecoratorBuilder(): JwsDecoratorBuilder ); } + public function jwsSerializerManagerDecorator(): JwsSerializerManagerDecorator { return $this->jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorFactory() ->build($this->supportedSerializers); } + public function claimFactory(): ClaimFactory { return $this->claimFactory ??= new ClaimFactory( diff --git a/src/Jwks/Factories/SignedJwksFactory.php b/src/Jwks/Factories/SignedJwksFactory.php index 0ca6fca..3ce6d08 100644 --- a/src/Jwks/Factories/SignedJwksFactory.php +++ b/src/Jwks/Factories/SignedJwksFactory.php @@ -29,6 +29,7 @@ public function fromToken(string $token): SignedJwks ); } + /** * @param array $payload * @param array $header diff --git a/src/Jwks/JwksDecorator.php b/src/Jwks/JwksDecorator.php index 652f3fc..0eae167 100644 --- a/src/Jwks/JwksDecorator.php +++ b/src/Jwks/JwksDecorator.php @@ -15,11 +15,13 @@ public function __construct(protected readonly JWKSet $jwks) { } + public function jwks(): JWKSet { return $this->jwks; } + /** * @return array{keys:array>} */ diff --git a/src/Jwks/JwksFetcher.php b/src/Jwks/JwksFetcher.php index 37d5b33..43b99ef 100644 --- a/src/Jwks/JwksFetcher.php +++ b/src/Jwks/JwksFetcher.php @@ -31,6 +31,7 @@ public function __construct( ) { } + /** * @return array{keys:non-empty-array>} * @throws \SimpleSAML\OpenID\Exceptions\JwksException @@ -52,6 +53,7 @@ protected function decodeJwksJson(string $jwksJson): array return $this->claimFactory->buildJwks($jwks)->getValue(); } + public function fromCache(string $uri): ?JwksDecorator { $this->logger?->debug( @@ -95,6 +97,7 @@ public function fromCache(string $uri): ?JwksDecorator return $this->jwksDecoratorFactory->fromKeySetData($jwks); } + /** */ public function fromCacheOrJwksUri(string $uri): ?JwksDecorator @@ -102,6 +105,7 @@ public function fromCacheOrJwksUri(string $uri): ?JwksDecorator return $this->fromCache($uri) ?? $this->fromJwksUri($uri); } + /** */ public function fromJwksUri(string $uri): ?JwksDecorator @@ -155,6 +159,7 @@ public function fromJwksUri(string $uri): ?JwksDecorator return $this->jwksDecoratorFactory->fromKeySetData($jwks); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @phpstan-ignore missingType.iterableValue (JWKS array is validated later) @@ -164,6 +169,7 @@ public function fromCacheOrSignedJwksUri(string $uri, array $federationJwks): ?J return $this->fromCache($uri) ?? $this->fromSignedJwksUri($uri, $federationJwks); } + /** * @param string $uri URI from which to fetch SignedJwks statement. * @param array $federationJwks Federation JWKS which will be used to check signature on SignedJwks statement. diff --git a/src/Jwks/SignedJwks.php b/src/Jwks/SignedJwks.php index 2da88da..ef83fa2 100644 --- a/src/Jwks/SignedJwks.php +++ b/src/Jwks/SignedJwks.php @@ -44,6 +44,7 @@ public function getKeys(): array return $ensuredKeys; } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\SignedJwksException @@ -54,6 +55,7 @@ public function getIssuer(): string return parent::getIssuer() ?? throw new SignedJwksException('No Issuer claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\SignedJwksException @@ -64,6 +66,7 @@ public function getSubject(): string return parent::getSubject() ?? throw new SignedJwksException('No Subject claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\SignedJwksException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -80,6 +83,7 @@ public function getType(): string return $typ; } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ @@ -95,6 +99,7 @@ protected function validate(): void ); } + /** * @return array{keys: array>} * @throws \SimpleSAML\OpenID\Exceptions\JwsException diff --git a/src/Jws/AbstractJwsFetcher.php b/src/Jws/AbstractJwsFetcher.php index d31383b..6226fc6 100644 --- a/src/Jws/AbstractJwsFetcher.php +++ b/src/Jws/AbstractJwsFetcher.php @@ -19,10 +19,12 @@ public function __construct( ) { } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ abstract protected function buildJwsInstance(string $token): ParsedJws; + abstract public function getExpectedContentTypeHttpHeader(): ?string; } diff --git a/src/Jws/Factories/ParsedJwsFactory.php b/src/Jws/Factories/ParsedJwsFactory.php index 304c7d9..81d0930 100644 --- a/src/Jws/Factories/ParsedJwsFactory.php +++ b/src/Jws/Factories/ParsedJwsFactory.php @@ -28,6 +28,7 @@ public function __construct( ) { } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ @@ -44,6 +45,7 @@ public function fromToken(string $token): ParsedJws ); } + /** * @param array $payload * @param array $header diff --git a/src/Jws/JwsDecorator.php b/src/Jws/JwsDecorator.php index 9908944..6f06d21 100644 --- a/src/Jws/JwsDecorator.php +++ b/src/Jws/JwsDecorator.php @@ -12,6 +12,7 @@ public function __construct(protected readonly JWS $jws) { } + public function jws(): JWS { return $this->jws; diff --git a/src/Jws/JwsDecoratorBuilder.php b/src/Jws/JwsDecoratorBuilder.php index 4104f65..27e71d5 100644 --- a/src/Jws/JwsDecoratorBuilder.php +++ b/src/Jws/JwsDecoratorBuilder.php @@ -22,6 +22,7 @@ public function __construct( ) { } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ @@ -34,6 +35,7 @@ public function fromToken(string $token): JwsDecorator } } + /** * @param array $payload * @param array $header diff --git a/src/Jws/JwsFetcher.php b/src/Jws/JwsFetcher.php index 930e884..a3c2893 100644 --- a/src/Jws/JwsFetcher.php +++ b/src/Jws/JwsFetcher.php @@ -24,6 +24,7 @@ public function __construct( parent::__construct($artifactFetcher, $maxCacheDuration, $helpers, $logger); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ @@ -32,11 +33,13 @@ protected function buildJwsInstance(string $token): ParsedJws return $this->parsedJwsFactory->fromToken($token); } + public function getExpectedContentTypeHttpHeader(): ?string { return null; } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\FetchException @@ -46,6 +49,7 @@ public function fromCacheOrNetwork(string $uri): ParsedJws return $this->fromCache($uri) ?? $this->fromNetwork($uri); } + /** * Fetch JWS from cache, if available. URI is used as cache key. * @@ -73,6 +77,7 @@ public function fromCache(string $uri): ?ParsedJws return $this->buildJwsInstance($jws); } + /** * Fetch JWS from network. Each successful fetch will be cached, with URI being used as a cache key. * diff --git a/src/Jws/JwsVerifierDecorator.php b/src/Jws/JwsVerifierDecorator.php index 7449434..3642e53 100644 --- a/src/Jws/JwsVerifierDecorator.php +++ b/src/Jws/JwsVerifierDecorator.php @@ -14,11 +14,13 @@ public function __construct( ) { } + public function jwsVerifier(): JWSVerifier { return $this->jwsVerifier; } + public function verifyWithKeySet( JwsDecorator $jwsDecorator, JwksDecorator $jwksDecorator, diff --git a/src/Jws/ParsedJws.php b/src/Jws/ParsedJws.php index bf8c90e..2474afd 100644 --- a/src/Jws/ParsedJws.php +++ b/src/Jws/ParsedJws.php @@ -29,6 +29,7 @@ class ParsedJws protected ?string $token = null; + public function __construct( protected readonly JwsDecorator $jwsDecorator, protected readonly JwsVerifierDecorator $jwsVerifierDecorator, @@ -41,10 +42,12 @@ public function __construct( $this->validate(); } + protected function validate(): void { } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ @@ -65,6 +68,7 @@ protected function validateByCallbacks(callable ...$calls): void } } + /** * @return array * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -78,6 +82,7 @@ public function getHeader(int $signatureId = 0): array } } + /** * @param non-empty-string $key * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -87,6 +92,7 @@ public function getHeaderClaim(string $key): mixed return $this->getHeader()[$key] ?? null; } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ @@ -95,6 +101,7 @@ public function getPayloadClaim(string $key): mixed return $this->getPayload()[$key] ?? null; } + public function getNestedPayloadClaim(int|string ...$keys): mixed { return $this->helpers->arr()->getNestedValue( @@ -103,6 +110,7 @@ public function getNestedPayloadClaim(int|string ...$keys): mixed ); } + public function getToken( JwsSerializerEnum $jwsSerializerEnum = JwsSerializerEnum::Compact, ?int $signatureIndex = null, @@ -114,6 +122,7 @@ public function getToken( ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @return array @@ -138,6 +147,7 @@ public function getPayload(): array } } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @phpstan-ignore missingType.iterableValue (JWKS array is validated later) @@ -155,6 +165,7 @@ public function verifyWithKeySet(array $jwks, int $signatureIndex = 0): void } } + /** * @param mixed[] $key * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -169,6 +180,7 @@ public function verifyWithKey(array $key): void ]); } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -185,6 +197,7 @@ public function getIssuer(): ?string $this->helpers->type()->ensureNonEmptyString($iss, ClaimsEnum::Iss->value); } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -199,6 +212,7 @@ public function getSubject(): ?string return is_null($sub) ? null : $this->helpers->type()->ensureNonEmptyString($sub, $claimKey); } + /** * @return ?string[] * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -224,6 +238,7 @@ public function getAudience(): ?array throw new JwsException(sprintf('Invalid audience claim format: %s', var_export($aud, true))); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\ClientAssertionException @@ -239,6 +254,7 @@ public function getJwtId(): ?string return is_null($jti) ? null : $this->helpers->type()->ensureNonEmptyString($jti, $claimKey); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -260,6 +276,7 @@ public function getExpirationTime(): ?int return $exp; } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -281,6 +298,7 @@ public function getNotBefore(): ?int return $nbf; } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -302,6 +320,7 @@ public function getIssuedAt(): ?int return $iat; } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -318,6 +337,7 @@ public function getIdentifier(): ?string $this->helpers->type()->ensureNonEmptyString($id, $claimKey); } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -332,6 +352,7 @@ public function getKeyId(): ?string return is_null($kid) ? null : $this->helpers->type()->ensureNonEmptyString($kid, $claimKey); } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -346,6 +367,7 @@ public function getType(): ?string return is_null($typ) ? null : $this->helpers->type()->ensureNonEmptyString($typ, $claimKey); } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException diff --git a/src/SdJwt/Disclosure.php b/src/SdJwt/Disclosure.php index 604c621..aaba379 100644 --- a/src/SdJwt/Disclosure.php +++ b/src/SdJwt/Disclosure.php @@ -22,6 +22,7 @@ class Disclosure implements JsonSerializable ClaimsEnum::DotDotDot->value, ]; + /** * @var non-empty-string|null */ @@ -32,6 +33,7 @@ class Disclosure implements JsonSerializable */ protected ?string $digest = null; + /** * @param array $path * @throws \SimpleSAML\OpenID\Exceptions\SdJwtException @@ -53,21 +55,25 @@ public function __construct( } } + public function getSalt(): string { return $this->salt; } + public function getValue(): mixed { return $this->value; } + public function getName(): ?string { return $this->name; } + /** * @return array */ @@ -76,6 +82,7 @@ public function getPath(): array return $this->path; } + /** * @return array */ @@ -88,6 +95,7 @@ public function jsonSerialize(): array return [$this->salt, $this->name, $this->value]; } + public function getType(): SdJwtDisclosureType { if ($this->name === null) { @@ -97,6 +105,7 @@ public function getType(): SdJwtDisclosureType return SdJwtDisclosureType::ObjectProperty; } + public function getEncoded(): string { return $this->encoded ??= $this->helpers->type()->ensureNonEmptyString( @@ -108,6 +117,7 @@ public function getEncoded(): string ); } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException @@ -125,6 +135,7 @@ public function getDigest(): string ); } + /** * @return non-empty-string|array{"...": non-empty-string} * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException diff --git a/src/SdJwt/DisclosureBag.php b/src/SdJwt/DisclosureBag.php index f7c8ee0..d0f90b3 100644 --- a/src/SdJwt/DisclosureBag.php +++ b/src/SdJwt/DisclosureBag.php @@ -13,11 +13,13 @@ class DisclosureBag */ protected array $disclosures; + public function __construct(Disclosure ...$disclosures) { $this->disclosures = $disclosures; } + /** * @throws \SimpleSAML\OpenID\Exceptions\SdJwtException */ @@ -32,6 +34,7 @@ public function add(Disclosure ...$disclosures): void } } + /** * @return \SimpleSAML\OpenID\SdJwt\Disclosure[] */ @@ -40,6 +43,7 @@ public function all(): array return $this->disclosures; } + /** * @return string[] */ diff --git a/src/SdJwt/Factories/DisclosureFactory.php b/src/SdJwt/Factories/DisclosureFactory.php index 0aa9b6b..a1b505a 100644 --- a/src/SdJwt/Factories/DisclosureFactory.php +++ b/src/SdJwt/Factories/DisclosureFactory.php @@ -16,6 +16,7 @@ public function __construct( ) { } + /** * @param array $path * @param string[] $saltBlacklist @@ -44,6 +45,7 @@ public function build( ); } + /** * @param array $path * @param string[] $saltBlacklist diff --git a/src/SdJwt/Factories/SdJwtFactory.php b/src/SdJwt/Factories/SdJwtFactory.php index 91bb809..1050b74 100644 --- a/src/SdJwt/Factories/SdJwtFactory.php +++ b/src/SdJwt/Factories/SdJwtFactory.php @@ -45,6 +45,7 @@ public function __construct( ); } + /** * @param array $payload * @param array $header @@ -87,6 +88,7 @@ public function fromData( ); } + /** * @param array $payload * @return array diff --git a/src/SdJwt/SdJwt.php b/src/SdJwt/SdJwt.php index f216d98..c4d0d26 100644 --- a/src/SdJwt/SdJwt.php +++ b/src/SdJwt/SdJwt.php @@ -23,6 +23,7 @@ class SdJwt extends ParsedJws { public const TILDE = '~'; + public function __construct( JwsDecorator $jwsDecorator, JwsVerifierDecorator $jwsVerifierDecorator, @@ -61,6 +62,7 @@ public function getSelectiveDisclosureAlgorithm(): ?HashAlgorithmsEnum HashAlgorithmsEnum::from($this->helpers->type()->ensureNonEmptyString($_sdAlg, $claimKey)); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -77,16 +79,19 @@ public function getConfirmation(): ?array $this->helpers->type()->ensureArray($cnf, $claimKey); } + public function getDisclosureBag(): ?DisclosureBag { return $this->disclosureBag; } + public function getKbJwt(): ?KbJwt { return $this->kbJwt; } + public function getToken( JwsSerializerEnum $jwsSerializerEnum = JwsSerializerEnum::Compact, ?int $signatureIndex = null, @@ -115,6 +120,7 @@ public function getToken( return $token; } + public function getUndisclosedToken( JwsSerializerEnum $jwsSerializerEnum = JwsSerializerEnum::Compact, ?int $signatureIndex = null, @@ -122,6 +128,7 @@ public function getUndisclosedToken( return parent::getToken($jwsSerializerEnum, $signatureIndex); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException diff --git a/src/Serializers/JwsSerializerBag.php b/src/Serializers/JwsSerializerBag.php index 92703ed..a4b5e9e 100644 --- a/src/Serializers/JwsSerializerBag.php +++ b/src/Serializers/JwsSerializerBag.php @@ -13,16 +13,19 @@ class JwsSerializerBag */ protected array $jwsSerializers; + public function __construct(JwsSerializerEnum ...$jwsSerializers) { $this->jwsSerializers = $jwsSerializers; } + public function add(JwsSerializerEnum $jwsSerializer): void { $this->jwsSerializers[] = $jwsSerializer; } + /** * @return \SimpleSAML\OpenID\Serializers\JwsSerializerEnum[] */ @@ -31,6 +34,7 @@ public function getAll(): array return $this->jwsSerializers; } + /** * @return \Jose\Component\Signature\Serializer\JWSSerializer[] */ diff --git a/src/Serializers/JwsSerializerEnum.php b/src/Serializers/JwsSerializerEnum.php index 2ed5414..c878fc2 100644 --- a/src/Serializers/JwsSerializerEnum.php +++ b/src/Serializers/JwsSerializerEnum.php @@ -15,6 +15,7 @@ enum JwsSerializerEnum: string case JsonGeneral = 'jws_json_general'; case JsonFlattened = 'jws_json_flattened'; + public function instance(): JWSSerializer { return match ($this) { diff --git a/src/Serializers/JwsSerializerManagerDecorator.php b/src/Serializers/JwsSerializerManagerDecorator.php index a69898f..d3f3975 100644 --- a/src/Serializers/JwsSerializerManagerDecorator.php +++ b/src/Serializers/JwsSerializerManagerDecorator.php @@ -14,16 +14,19 @@ public function __construct( ) { } + public function jwsSerializerManager(): JWSSerializerManager { return $this->jwsSerializerManager; } + public function serialize(string $name, JwsDecorator $jwsDecorator, ?int $signatureIndex = null): string { return $this->jwsSerializerManager()->serialize($name, $jwsDecorator->jws(), $signatureIndex); } + public function unserialize(string $input, ?string &$name = null): JwsDecorator { return new JwsDecorator($this->jwsSerializerManager()->unserialize($input, $name)); diff --git a/src/SupportedAlgorithms.php b/src/SupportedAlgorithms.php index f126c27..caa3879 100644 --- a/src/SupportedAlgorithms.php +++ b/src/SupportedAlgorithms.php @@ -19,6 +19,7 @@ public function __construct( ) { } + public function getSignatureAlgorithmBag(): SignatureAlgorithmBag { return $this->signatureAlgorithmBag; diff --git a/src/SupportedSerializers.php b/src/SupportedSerializers.php index 8bc5b92..a9bdf24 100644 --- a/src/SupportedSerializers.php +++ b/src/SupportedSerializers.php @@ -16,6 +16,7 @@ public function __construct( ) { } + public function getJwsSerializerBag(): JwsSerializerBag { return $this->jwsSerializerBag; diff --git a/src/Utils/ArtifactFetcher.php b/src/Utils/ArtifactFetcher.php index 4d6b4c8..8fd49a7 100644 --- a/src/Utils/ArtifactFetcher.php +++ b/src/Utils/ArtifactFetcher.php @@ -22,6 +22,7 @@ public function __construct( ) { } + public function fromCacheAsString(string $keyElement, string ...$keyElements): ?string { if (is_null($this->cacheDecorator)) { @@ -66,6 +67,7 @@ public function fromCacheAsString(string $keyElement, string ...$keyElements): ? return null; } + /** * @throws \SimpleSAML\OpenID\Exceptions\FetchException */ @@ -89,6 +91,7 @@ public function fromNetwork(string $uri): ResponseInterface return $response; } + /** * @throws \SimpleSAML\OpenID\Exceptions\FetchException */ @@ -106,6 +109,7 @@ public function fromNetworkAsString(string $uri): string return $artifact; } + public function cacheIt(string $artifact, int|DateInterval $ttl, string $keyElement, string ...$keyElements): void { if (is_null($this->cacheDecorator)) { diff --git a/src/VerifiableCredentials.php b/src/VerifiableCredentials.php index 4e51071..ddf4642 100644 --- a/src/VerifiableCredentials.php +++ b/src/VerifiableCredentials.php @@ -71,6 +71,7 @@ class VerifiableCredentials protected ?TxCodeFactory $txCodeFactory = null; + public function __construct( protected readonly SupportedSerializers $supportedSerializers = new SupportedSerializers(), protected readonly SupportedAlgorithms $supportedAlgorithms = new SupportedAlgorithms(), @@ -81,16 +82,19 @@ public function __construct( ->build($timestampValidationLeeway); } + public function dateIntervalDecoratorFactory(): DateIntervalDecoratorFactory { return $this->dateIntervalDecoratorFactory ??= new DateIntervalDecoratorFactory(); } + public function helpers(): Helpers { return $this->helpers ??= new Helpers(); } + public function claimsPathPointerResolver(): ClaimsPathPointerResolver { return $this->claimsPathPointerResolver ??= new ClaimsPathPointerResolver( @@ -98,33 +102,39 @@ public function claimsPathPointerResolver(): ClaimsPathPointerResolver ); } + public function jwsDecoratorBuilderFactory(): JwsDecoratorBuilderFactory { return $this->jwsDecoratorBuilderFactory ??= new JwsDecoratorBuilderFactory(); } + public function jwsSerializerManagerDecoratorFactory(): JwsSerializerManagerDecoratorFactory { return $this->jwsSerializerManagerDecoratorFactory ??= new JwsSerializerManagerDecoratorFactory(); } + public function jwsSerializerManagerDecorator(): JwsSerializerManagerDecorator { return $this->jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorFactory() ->build($this->supportedSerializers); } + public function algorithmManagerDecoratorFactory(): AlgorithmManagerDecoratorFactory { return $this->algorithmManagerDecoratorFactory ??= new AlgorithmManagerDecoratorFactory(); } + public function algorithmManagerDecorator(): AlgorithmManagerDecorator { return $this->algorithmManagerDecorator ??= $this->algorithmManagerDecoratorFactory() ->build($this->supportedAlgorithms); } + public function jwsDecoratorBuilder(): JwsDecoratorBuilder { return $this->jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderFactory()->build( @@ -134,11 +144,13 @@ public function jwsDecoratorBuilder(): JwsDecoratorBuilder ); } + public function jwsVerifierDecoratorFactory(): JwsVerifierDecoratorFactory { return $this->jwsVerifierDecoratorFactory ??= new JwsVerifierDecoratorFactory(); } + public function jwsVerifierDecorator(): JwsVerifierDecorator { return $this->jwsVerifierDecorator ??= $this->jwsVerifierDecoratorFactory()->build( @@ -146,11 +158,13 @@ public function jwsVerifierDecorator(): JwsVerifierDecorator ); } + public function jwksDecoratorFactory(): JwksDecoratorFactory { return $this->jwksDecoratorFactory ??= new JwksDecoratorFactory(); } + public function claimFactory(): ClaimFactory { return $this->claimFactory ??= new ClaimFactory( @@ -158,6 +172,7 @@ public function claimFactory(): ClaimFactory ); } + public function jwtVcJsonFactory(): JwtVcJsonFactory { return $this->jwtVcJsonFactory ??= new JwtVcJsonFactory( @@ -171,6 +186,7 @@ public function jwtVcJsonFactory(): JwtVcJsonFactory ); } + public function credentialOfferFactory(): CredentialOfferFactory { return $this->credentialOfferFactory ??= new CredentialOfferFactory( @@ -178,6 +194,7 @@ public function credentialOfferFactory(): CredentialOfferFactory ); } + public function openId4VciProofFactory(): OpenId4VciProofFactory { return $this->openId4VciProofFactory ??= new OpenId4VciProofFactory( @@ -191,6 +208,7 @@ public function openId4VciProofFactory(): OpenId4VciProofFactory ); } + public function disclosureFactory(): DisclosureFactory { return $this->disclosureFactory ??= new DisclosureFactory( @@ -198,11 +216,13 @@ public function disclosureFactory(): DisclosureFactory ); } + public function disclosureBagFactory(): DisclosureBagFactory { return $this->disclosureBagFactory ??= new DisclosureBagFactory(); } + public function sdJwtVcFactory(): SdJwtVcFactory { return $this->sdJwtVcFactory ??= new SdJwtVcFactory( @@ -217,6 +237,7 @@ public function sdJwtVcFactory(): SdJwtVcFactory ); } + public function txCodeFactory(): TxCodeFactory { return $this->txCodeFactory ??= new TxCodeFactory(); diff --git a/src/VerifiableCredentials/ClaimsPathPointerResolver.php b/src/VerifiableCredentials/ClaimsPathPointerResolver.php index ab08599..e1341e7 100644 --- a/src/VerifiableCredentials/ClaimsPathPointerResolver.php +++ b/src/VerifiableCredentials/ClaimsPathPointerResolver.php @@ -17,6 +17,7 @@ public function __construct( ) { } + /** * @see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-processing * diff --git a/src/VerifiableCredentials/CredentialOffer.php b/src/VerifiableCredentials/CredentialOffer.php index cfc62fe..ba262d2 100644 --- a/src/VerifiableCredentials/CredentialOffer.php +++ b/src/VerifiableCredentials/CredentialOffer.php @@ -15,6 +15,7 @@ public function __construct( ) { } + /** * @throws \SimpleSAML\OpenID\Exceptions\CredentialOfferException * @return string|mixed[] diff --git a/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsBag.php b/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsBag.php index ad5f90f..b2e2a7a 100644 --- a/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsBag.php +++ b/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsBag.php @@ -11,11 +11,13 @@ class CredentialOfferGrantsBag implements \JsonSerializable */ public readonly array $credentialOfferGrantsValues; + public function __construct(CredentialOfferGrantsValue ...$credentialOfferGrantsValues) { $this->credentialOfferGrantsValues = $credentialOfferGrantsValues; } + /** * @return array */ diff --git a/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValue.php b/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValue.php index 5aff2e1..c76e047 100644 --- a/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValue.php +++ b/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValue.php @@ -83,6 +83,7 @@ public function __construct( } } + /** * @return array */ diff --git a/src/VerifiableCredentials/CredentialOffer/CredentialOfferParameters.php b/src/VerifiableCredentials/CredentialOffer/CredentialOfferParameters.php index 3855daa..6fd385f 100644 --- a/src/VerifiableCredentials/CredentialOffer/CredentialOfferParameters.php +++ b/src/VerifiableCredentials/CredentialOffer/CredentialOfferParameters.php @@ -19,6 +19,7 @@ public function __construct( ) { } + /** * @return array */ diff --git a/src/VerifiableCredentials/Factories/CredentialOfferFactory.php b/src/VerifiableCredentials/Factories/CredentialOfferFactory.php index 02fc673..252f44b 100644 --- a/src/VerifiableCredentials/Factories/CredentialOfferFactory.php +++ b/src/VerifiableCredentials/Factories/CredentialOfferFactory.php @@ -19,6 +19,7 @@ public function __construct( ) { } + /** * @param mixed[]|null $parameters * @throws \SimpleSAML\OpenID\Exceptions\CredentialOfferException diff --git a/src/VerifiableCredentials/Factories/OpenId4VciProofFactory.php b/src/VerifiableCredentials/Factories/OpenId4VciProofFactory.php index cca265e..42d6f3d 100644 --- a/src/VerifiableCredentials/Factories/OpenId4VciProofFactory.php +++ b/src/VerifiableCredentials/Factories/OpenId4VciProofFactory.php @@ -26,6 +26,7 @@ public function fromToken(string $token): OpenId4VciProof ); } + /** * @param array $payload * @param array $header diff --git a/src/VerifiableCredentials/OpenId4VciProof.php b/src/VerifiableCredentials/OpenId4VciProof.php index 72b42a6..2d9ff40 100644 --- a/src/VerifiableCredentials/OpenId4VciProof.php +++ b/src/VerifiableCredentials/OpenId4VciProof.php @@ -34,6 +34,7 @@ public function getAlgorithm(): string return $alg; } + /** * @return non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -52,6 +53,7 @@ public function getType(): string return $typ; } + /** * @return ?mixed[] * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -66,6 +68,7 @@ public function getJsonWebKey(): ?array return is_null($jwk) ? null : $this->helpers->type()->ensureArray($jwk, $claimKey); } + /** * @return ?non-empty-string[] * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -80,6 +83,7 @@ public function getX509CertificateChain(): ?array return is_null($x5c) ? null : $this->helpers->type()->ensureArrayWithValuesAsNonEmptyStrings($x5c, $claimKey); } + /** * @return string[] * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -91,11 +95,13 @@ public function getAudience(): array return parent::getAudience() ?? throw new OpenId4VciProofException('No Audience claim found.'); } + public function getIssuedAt(): int { return parent::getIssuedAt() ?? throw new OpenId4VciProofException('No IssuedAt claim found.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException diff --git a/src/VerifiableCredentials/SdJwtVc/SdJwtVc.php b/src/VerifiableCredentials/SdJwtVc/SdJwtVc.php index a665710..1427d3a 100644 --- a/src/VerifiableCredentials/SdJwtVc/SdJwtVc.php +++ b/src/VerifiableCredentials/SdJwtVc/SdJwtVc.php @@ -27,6 +27,7 @@ class SdJwtVc extends SdJwt ClaimsEnum::Status->value, ]; + public function getType(): string { $typ = parent::getType() ?? throw new SdJwtVcException('No Type header claim found.'); @@ -38,6 +39,7 @@ public function getType(): string return $typ; } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -54,6 +56,7 @@ public function getVerifiableCredentialType(): string return $this->helpers->type()->ensureNonEmptyString($vct, $claimKey); } + /** * @throws \SimpleSAML\OpenID\Exceptions\SdJwtVcException */ @@ -80,6 +83,7 @@ protected function ensureNonSelectivelyDisclosableClaims(): void } } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException * @throws \SimpleSAML\OpenID\Exceptions\SdJwtVcException @@ -105,6 +109,7 @@ protected function ensureNoSdClaimWhenNoDisclosures(): void } } + protected function validate(): void { $this->validateByCallbacks( diff --git a/src/VerifiableCredentials/TxCode.php b/src/VerifiableCredentials/TxCode.php index cf0377f..491a74c 100644 --- a/src/VerifiableCredentials/TxCode.php +++ b/src/VerifiableCredentials/TxCode.php @@ -13,6 +13,7 @@ class TxCode implements \JsonSerializable protected readonly int $length; + public function __construct( protected readonly int|string $code, protected readonly string $description, @@ -25,31 +26,37 @@ public function __construct( $this->length = mb_strlen((string)$this->code); } + public function getCode(): int|string { return $this->code; } + public function getCodeAsString(): string { return (string)$this->code; } + public function getDescription(): string { return $this->description; } + public function getInputMode(): TxCodeInputModeEnum { return $this->inputMode; } + public function getLength(): int { return $this->length; } + /** * @return array */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php index 7c3149a..37e74ad 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValue.php @@ -12,6 +12,7 @@ abstract class AbstractIdentifiedTypedClaimValue implements ClaimInterface /** @var non-empty-array */ protected readonly array $data; + /** * @param non-empty-string $id, * @param mixed[] $otherClaims @@ -28,6 +29,7 @@ public function __construct( ); } + /** * @return non-empty-string */ @@ -36,18 +38,22 @@ public function getId(): string return $this->id; } + public function getType(): TypeClaimValue { return $this->typeClaimValue; } + public function getKey(int|string $key): mixed { return $this->data[$key] ?? null; } + abstract public function getName(): string; + /** * @return non-empty-array */ @@ -56,6 +62,7 @@ public function getValue(): array return $this->data; } + /** * @return non-empty-array */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValue.php index f48698d..3289ab1 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValue.php @@ -12,6 +12,7 @@ abstract class AbstractTypedClaimValue implements ClaimInterface /** @var non-empty-array */ protected readonly array $data; + /** * @param mixed[] $otherClaims */ @@ -25,18 +26,22 @@ public function __construct( ); } + public function getType(): TypeClaimValue { return $this->typeClaimValue; } + public function getKey(int|string $key): mixed { return $this->data[$key] ?? null; } + abstract public function getName(): string; + /** * @return non-empty-array */ @@ -45,6 +50,7 @@ public function getValue(): array return $this->data; } + /** * @return non-empty-array */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValue.php index f08b92a..0e1df1d 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValue.php @@ -17,11 +17,13 @@ public function __construct( ) { } + public function getName(): string { return ClaimsEnum::Type->value; } + /** * @return non-empty-string[] */ @@ -30,6 +32,7 @@ public function getValue(): array return $this->types; } + /** * @return non-empty-string|non-empty-string[] */ @@ -44,6 +47,7 @@ public function jsonSerialize(): string|array return $value; } + /** * @param non-empty-string $type */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php index 7ad3008..6acda38 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php @@ -28,6 +28,7 @@ public function __construct( } } + /** * @return mixed[] */ @@ -36,11 +37,13 @@ public function jsonSerialize(): array return $this->getValue(); } + public function getBaseContext(): string { return $this->baseContext; } + /** * @return mixed[] */ @@ -49,11 +52,13 @@ public function getOtherContexts(): array return $this->otherContexts; } + public function getName(): string { return ClaimsEnum::AtContext->name; } + /** * @return mixed[] */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php index 9646e89..02632ef 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php @@ -31,6 +31,7 @@ public function __construct( ) { } + /** * @return mixed[] */ @@ -39,11 +40,13 @@ public function jsonSerialize(): array return $this->getValue(); } + public function getAtContext(): VcAtContextClaimValue { return $this->atContextClaimValue; } + /** * @return non-empty-string|null */ @@ -52,51 +55,61 @@ public function getId(): ?string return $this->id; } + public function getType(): TypeClaimValue { return $this->typeClaimValue; } + public function getCredentialSubject(): VcCredentialSubjectClaimBag { return $this->credentialSubjectClaimBag; } + public function getIssuer(): VcIssuerClaimValue { return $this->issuerClaimValue; } + public function getIssuanceDate(): DateTimeImmutable { return $this->issuanceDate; } + public function getProof(): ?VcProofClaimValue { return $this->proofClaimValue; } + public function getExpirationDate(): ?DateTimeImmutable { return $this->expirationDate; } + public function getCredentialStatus(): ?VcCredentialStatusClaimValue { return $this->credentialStatusClaimValue; } + public function getCredentialSchema(): ?VcCredentialSchemaClaimBag { return $this->credentialSchemaClaimBag; } + public function getName(): string { return ClaimsEnum::Vc->value; } + /** * @return mixed[] */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php index dc3b2ad..cd60966 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBag.php @@ -12,6 +12,7 @@ class VcCredentialSchemaClaimBag implements ClaimInterface /** @var \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSchemaClaimValue[] */ protected array $vcCredentialSchemaClaimValueValues; + public function __construct( VcCredentialSchemaClaimValue $vcCredentialSchemaClaimValue, VcCredentialSchemaClaimValue ...$vcCredentialSchemaClaimValueValues, @@ -22,6 +23,7 @@ public function __construct( ]; } + /** * @return mixed[] */ @@ -35,11 +37,13 @@ public function jsonSerialize(): array ); } + public function getName(): string { return ClaimsEnum::Credential_Schema->value; } + /** * @return \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSchemaClaimValue[] */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php index 89389c1..e9714af 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBag.php @@ -12,6 +12,7 @@ class VcCredentialSubjectClaimBag implements ClaimInterface /** @var \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimValue[] */ protected array $vcCredentialSubjectClaimValueValues; + public function __construct( VcCredentialSubjectClaimValue $vcCredentialSubjectClaimValue, VcCredentialSubjectClaimValue ...$vcCredentialSubjectClaimValueValues, @@ -22,6 +23,7 @@ public function __construct( ]; } + /** * @return mixed[] */ @@ -35,11 +37,13 @@ public function jsonSerialize(): array ); } + public function getName(): string { return ClaimsEnum::Credential_Subject->value; } + /** * @return \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcCredentialSubjectClaimValue[] */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php index 77811d7..ae93e33 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValue.php @@ -16,11 +16,13 @@ public function __construct(protected readonly array $data) { } + public function get(int|string $key): mixed { return $this->data[$key] ?? null; } + /** * @return non-empty-array */ @@ -29,11 +31,13 @@ public function jsonSerialize(): array return $this->getValue(); } + public function getName(): string { return ClaimsEnum::Credential_Subject->value; } + /** * @return non-empty-array */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php index 142e33e..6da2648 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBag.php @@ -12,6 +12,7 @@ class VcEvidenceClaimBag implements ClaimInterface /** @var \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcEvidenceClaimValue[] */ protected array $vcEvidenceClaimValueValues; + public function __construct( VcEvidenceClaimValue $vcEvidenceClaimValue, VcEvidenceClaimValue ...$vcEvidenceClaimValueValues, @@ -22,6 +23,7 @@ public function __construct( ]; } + /** * @return mixed[] */ @@ -35,11 +37,13 @@ public function jsonSerialize(): array ); } + public function getName(): string { return ClaimsEnum::Evidence->value; } + /** * @return \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcEvidenceClaimValue[] */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php index 7de1117..d349eba 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValue.php @@ -12,6 +12,7 @@ class VcIssuerClaimValue implements ClaimInterface /** @var non-empty-array */ protected array $data; + /** * @param non-empty-string $id * @param mixed[] $otherClaims @@ -26,6 +27,7 @@ public function __construct( ); } + /** * @return non-empty-string */ @@ -34,16 +36,19 @@ public function getId(): string return $this->id; } + public function getKey(int|string $key): mixed { return $this->data[$key] ?? null; } + public function getName(): string { return ClaimsEnum::Issuer->value; } + /** * @return non-empty-array */ @@ -52,6 +57,7 @@ public function getValue(): array return $this->data; } + /** * @return non-empty-array */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php index cf515af..cf4924d 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBag.php @@ -12,6 +12,7 @@ class VcRefreshServiceClaimBag implements ClaimInterface /** @var \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcRefreshServiceClaimValue[] */ protected array $vcRefreshServiceClaimValueValues; + public function __construct( VcRefreshServiceClaimValue $vcRefreshServiceClaimValue, VcRefreshServiceClaimValue ...$vcRefreshServiceClaimValueValues, @@ -22,6 +23,7 @@ public function __construct( ]; } + /** * @return mixed[] */ @@ -35,11 +37,13 @@ public function jsonSerialize(): array ); } + public function getName(): string { return ClaimsEnum::Refresh_Service->value; } + /** * @return \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcRefreshServiceClaimValue[] */ diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php b/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php index 542754f..81c0ad8 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBag.php @@ -12,6 +12,7 @@ class VcTermsOfUseClaimBag implements ClaimInterface /** @var \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcTermsOfUseClaimValue[] */ protected array $vcTermsOfUseClaimValueValues; + public function __construct( VcTermsOfUseClaimValue $vcTermsOfUseClaimValue, VcTermsOfUseClaimValue ...$vcTermsOfUseClaimValueValues, @@ -22,6 +23,7 @@ public function __construct( ]; } + /** * @return mixed[] */ @@ -35,11 +37,13 @@ public function jsonSerialize(): array ); } + public function getName(): string { return ClaimsEnum::Terms_Of_Use->value; } + /** * @return \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcTermsOfUseClaimValue[] */ diff --git a/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactory.php index 612e909..36f27c9 100644 --- a/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactory.php @@ -26,6 +26,7 @@ public function fromToken(string $token): JwtVcJson ); } + /** * @param array $payload * @param array $header diff --git a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php index e510023..d7faccf 100644 --- a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php @@ -34,6 +34,7 @@ public function __construct( ) { } + /** * @param non-empty-string|null $vcId */ @@ -69,6 +70,7 @@ public function buildVcClaimValue( ); } + /** * @param mixed[] $otherContexts * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException @@ -78,6 +80,7 @@ public function buildVcAtContextClaimValue(string $baseContext, array $otherCont return new VcAtContextClaimValue($baseContext, $otherContexts); } + /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException @@ -99,6 +102,7 @@ public function buildTypeClaimValue(mixed $data): TypeClaimValue ); } + /** * @param non-empty-array $data */ @@ -107,6 +111,7 @@ public function buildVcCredentialSubjectClaimValue(array $data, ?string $id = nu return new VcCredentialSubjectClaimValue($data); } + /** * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -155,6 +160,7 @@ public function buildVcCredentialSubjectClaimBag( ); } + /** * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException @@ -171,6 +177,7 @@ public function buildVcIssuerClaimValue(array $data): VcIssuerClaimValue return new VcIssuerClaimValue($id, $data); } + /** * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException @@ -187,6 +194,7 @@ public function buildVcProofClaimValue(array $data): VcProofClaimValue return new VcProofClaimValue($typeClaimValue, $data); } + /** * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -207,6 +215,7 @@ public function buildVcCredentialStatusClaimValue(array $data): VcCredentialStat return new VcCredentialStatusClaimValue($id, $typeClaimValue, $data); } + /** * @param non-empty-array $data * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -227,6 +236,7 @@ public function buildVcCredentialSchemaClaimValue(array $data): VcCredentialSche return new VcCredentialSchemaClaimValue($id, $typeClaimValue, $data); } + /** * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -255,6 +265,7 @@ public function buildVcCredentialSchemaClaimBag(array $data): VcCredentialSchema ); } + /** * @param non-empty-array $data * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -275,6 +286,7 @@ public function buildVcRefreshServiceClaimValue(array $data): VcRefreshServiceCl return new VcRefreshServiceClaimValue($id, $typeClaimValue, $data); } + /** * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -303,6 +315,7 @@ public function buildVcRefreshServiceClaimBag(array $data): VcRefreshServiceClai ); } + /** * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException @@ -319,6 +332,7 @@ public function buildVcTermsOfUseClaimValue(array $data): VcTermsOfUseClaimValue return new VcTermsOfUseClaimValue($typeClaimValue, $data); } + /** * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -347,6 +361,7 @@ public function buildVcTermsOfUseClaimBag(array $data): VcTermsOfUseClaimBag ); } + /** * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException @@ -363,6 +378,7 @@ public function buildVcEvidenceClaimValue(array $data): VcEvidenceClaimValue return new VcEvidenceClaimValue($typeClaimValue, $data); } + /** * @param mixed[] $data * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException diff --git a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php index b8e9eed..95ec799 100644 --- a/src/VerifiableCredentials/VcDataModel/JwtVcJson.php +++ b/src/VerifiableCredentials/VcDataModel/JwtVcJson.php @@ -54,11 +54,13 @@ class JwtVcJson extends ParsedJws implements VerifiableCredentialInterface protected null|false|VcEvidenceClaimBag $vcEvidenceClaimBag = null; + public function getCredentialFormatIdentifier(): CredentialFormatIdentifiersEnum { return CredentialFormatIdentifiersEnum::JwtVcJson; } + /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -95,6 +97,7 @@ public function getVc(): VcClaimValue ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException */ @@ -120,6 +123,7 @@ public function getVcAtContext(): VcAtContextClaimValue ); } + /** * @return ?non-empty-string * @throws \SimpleSAML\OpenID\Exceptions\ClientAssertionException @@ -148,6 +152,7 @@ public function getVcId(): ?string return $this->vcId = $this->helpers->type()->enforceUri($vcId); } + /** * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -170,6 +175,7 @@ public function getVcType(): TypeClaimValue return $this->vcTypeClaimValue = $this->claimFactory->forVcDataModel()->buildTypeClaimValue($vcType); } + /** * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -196,6 +202,7 @@ public function getVcCredentialSubject(): VcCredentialSubjectClaimBag ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException @@ -236,6 +243,7 @@ public function getVcIssuer(): VcIssuerClaimValue throw new VcDataModelException('Invalid VC Issuer claim.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -270,6 +278,7 @@ public function getVcIssuanceDate(): DateTimeImmutable } } + /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException @@ -300,6 +309,7 @@ public function getVcProof(): ?VcProofClaimValue throw new VcDataModelException('Invalid VC Proof claim.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -345,6 +355,7 @@ public function getVcExpirationDate(): ?DateTimeImmutable throw new VcDataModelException('Invalid VC Expiration Date claim.'); } + /** * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -376,6 +387,7 @@ public function getVcCredentialStatus(): ?VcCredentialStatusClaimValue ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -407,6 +419,7 @@ public function getVcCredentialSchema(): ?VcCredentialSchemaClaimBag ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -438,6 +451,7 @@ public function getVcRefreshService(): ?VcRefreshServiceClaimBag ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -469,6 +483,7 @@ public function getVcTermsOfUse(): ?VcTermsOfUseClaimBag ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\VcDataModelException * @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException @@ -500,6 +515,7 @@ public function getVcEvidence(): ?VcEvidenceClaimBag ); } + /** * @throws \SimpleSAML\OpenID\Exceptions\JwsException */ diff --git a/tests/src/Algorithms/AlgorithmManagerDecoratorTest.php b/tests/src/Algorithms/AlgorithmManagerDecoratorTest.php index 59791e6..de24ada 100644 --- a/tests/src/Algorithms/AlgorithmManagerDecoratorTest.php +++ b/tests/src/Algorithms/AlgorithmManagerDecoratorTest.php @@ -15,6 +15,7 @@ final class AlgorithmManagerDecoratorTest extends TestCase { protected AlgorithmManager $algorithmManager; + protected function setUp(): void { $signatureAlgorithmMock = $this->createMock(SignatureAlgorithm::class); @@ -23,6 +24,7 @@ protected function setUp(): void $this->algorithmManager = new AlgorithmManager($signatureAlgorithmMocks); } + protected function sut( ?AlgorithmManager $algorithmManager = null, ): AlgorithmManagerDecorator { @@ -31,11 +33,13 @@ protected function sut( return new AlgorithmManagerDecorator($algorithmManager); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(AlgorithmManagerDecorator::class, $this->sut()); } + public function testCanGetAlgorithmManager(): void { $this->assertInstanceOf(AlgorithmManager::class, $this->sut()->algorithmManager()); diff --git a/tests/src/Algorithms/SignatureAlgorithmBagTest.php b/tests/src/Algorithms/SignatureAlgorithmBagTest.php index d646213..9c5584e 100644 --- a/tests/src/Algorithms/SignatureAlgorithmBagTest.php +++ b/tests/src/Algorithms/SignatureAlgorithmBagTest.php @@ -16,11 +16,13 @@ final class SignatureAlgorithmBagTest extends TestCase { protected SignatureAlgorithmEnum $signatureAlgorithmEnumRs256; + protected function setUp(): void { $this->signatureAlgorithmEnumRs256 = SignatureAlgorithmEnum::RS256; } + protected function sut(SignatureAlgorithmEnum ...$signatureAlgorithmEnums): SignatureAlgorithmBag { $signatureAlgorithmEnums = $signatureAlgorithmEnums === [] ? @@ -30,11 +32,13 @@ protected function sut(SignatureAlgorithmEnum ...$signatureAlgorithmEnums): Sign return new SignatureAlgorithmBag(...$signatureAlgorithmEnums); } + public function testCanInstantiate(): void { $this->assertInstanceOf(SignatureAlgorithmBag::class, $this->sut()); } + public function testCanAddAndGetAll(): void { $signatureAlgorithmBag = $this->sut(); @@ -44,6 +48,7 @@ public function testCanAddAndGetAll(): void $this->assertCount(2, $signatureAlgorithmBag->getAll()); } + public function testCanGetAllInstances(): void { $this->assertNotEmpty($this->sut()->getAllInstances()); diff --git a/tests/src/Claims/GenericClaimTest.php b/tests/src/Claims/GenericClaimTest.php index 50906df..90f84ca 100644 --- a/tests/src/Claims/GenericClaimTest.php +++ b/tests/src/Claims/GenericClaimTest.php @@ -15,12 +15,14 @@ final class GenericClaimTest extends TestCase protected string $nameSample; + protected function setUp(): void { $this->valueSample = 'sample-value'; $this->nameSample = 'sample-name'; } + protected function sut( mixed $value = null, ?string $name = null, @@ -34,11 +36,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(GenericClaim::class, $this->sut()); } + public function testCanGetProperties(): void { $this->assertSame($this->valueSample, $this->sut()->getValue()); diff --git a/tests/src/Claims/JwksClaimTest.php b/tests/src/Claims/JwksClaimTest.php index 8065b36..2b49e4f 100644 --- a/tests/src/Claims/JwksClaimTest.php +++ b/tests/src/Claims/JwksClaimTest.php @@ -16,6 +16,7 @@ final class JwksClaimTest extends TestCase protected string $nameSample; + protected function setUp(): void { $this->valueSample = [ @@ -34,6 +35,7 @@ protected function setUp(): void $this->nameSample = ClaimsEnum::Jwks->value; } + protected function sut( ?array $value = null, ?string $name = null, @@ -47,11 +49,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwksClaim::class, $this->sut()); } + public function testCanGetProperties(): void { $this->assertSame($this->valueSample, $this->sut()->getValue()); diff --git a/tests/src/Codebooks/MetadataPolicyOperatorsEnumTest.php b/tests/src/Codebooks/MetadataPolicyOperatorsEnumTest.php index 3292755..8573b5e 100644 --- a/tests/src/Codebooks/MetadataPolicyOperatorsEnumTest.php +++ b/tests/src/Codebooks/MetadataPolicyOperatorsEnumTest.php @@ -30,6 +30,7 @@ public function testItReturnsValuesInOrder(): void ); } + public function testItHasProperSupportedOperatorValueTypes(): void { $this->assertSame( @@ -93,6 +94,7 @@ public function testItHasProperSupportedOperatorValueTypes(): void ); } + public function testItHasProperSupportedParameterValueTypes(): void { $this->assertSame( @@ -164,6 +166,7 @@ public function testItHasProperSupportedParameterValueTypes(): void ); } + public function testItHasProperSupportedOperatorContainedValueTypes(): void { $this->assertSame( @@ -211,24 +214,28 @@ public function testItHasProperSupportedOperatorContainedValueTypes(): void ); } + public function testSupportedOperatorContainedValueTypesThrowsForValue(): void { $this->expectException(MetadataPolicyException::class); MetadataPolicyOperatorsEnum::Value->getSupportedOperatorContainedValueTypes(); } + public function testSupportedOperatorContainedValueTypesThrowsForDefault(): void { $this->expectException(MetadataPolicyException::class); MetadataPolicyOperatorsEnum::Default->getSupportedOperatorContainedValueTypes(); } + public function testSupportedOperatorContainedValueTypesThrowsForEssential(): void { $this->expectException(MetadataPolicyException::class); MetadataPolicyOperatorsEnum::Essential->getSupportedOperatorContainedValueTypes(); } + public function testItHasProperSupportedParameterContainedValueTypes(): void { $this->assertSame( @@ -265,30 +272,35 @@ public function testItHasProperSupportedParameterContainedValueTypes(): void ); } + public function testSupportedParameterContainedValueTypesThrowsForValue(): void { $this->expectException(MetadataPolicyException::class); MetadataPolicyOperatorsEnum::Value->getSupportedParameterContainedValueTypes(); } + public function testSupportedParameterContainedValueTypesThrowsForDefault(): void { $this->expectException(MetadataPolicyException::class); MetadataPolicyOperatorsEnum::Default->getSupportedParameterContainedValueTypes(); } + public function testSupportedParameterContainedValueTypesThrowsForOneOf(): void { $this->expectException(MetadataPolicyException::class); MetadataPolicyOperatorsEnum::OneOf->getSupportedParameterContainedValueTypes(); } + public function testSupportedParameterContainedValueTypesThrowsForEssential(): void { $this->expectException(MetadataPolicyException::class); MetadataPolicyOperatorsEnum::Essential->getSupportedParameterContainedValueTypes(); } + public function testCanCheckIsValueSubsetOf(): void { $this->assertTrue( @@ -313,6 +325,7 @@ public function testCanCheckIsValueSubsetOf(): void ); } + public function testCanCheckIsValueSupersetOf(): void { $this->assertFalse( @@ -337,6 +350,7 @@ public function testCanCheckIsValueSupersetOf(): void ); } + public function testCanCheckIsOperatorValueTypeSupported(): void { // Assert true. @@ -401,6 +415,7 @@ public function testCanCheckIsOperatorValueTypeSupported(): void )); } + public function testCanCheckIsParameterValueTypeSupported(): void { // Assert true. @@ -468,6 +483,7 @@ public function testCanCheckIsParameterValueTypeSupported(): void )); } + public function testCanGetSupportedOperatorCombinations(): void { $this->assertSame( @@ -556,6 +572,7 @@ public function testCanGetSupportedOperatorCombinations(): void ); } + public function testCanCheckIsOperatorCombinationSupported(): void { // We have ensured proper operator combinations above, so just take a few cases to check that this method works. @@ -585,6 +602,7 @@ public function testCanCheckIsOperatorCombinationSupported(): void ); } + public function testCanValidateGeneralParameterOperationRules(): void { // We have already ensured rules above, so just take a few cases to check that this method works in happy flow. @@ -604,6 +622,7 @@ public function testCanValidateGeneralParameterOperationRules(): void $this->addToAssertionCount(1); } + public function testValidateGeneralParameterOperationRulesThrowsForNonSupportedOperatorValue(): void { $this->expectException(MetadataPolicyException::class); @@ -616,6 +635,7 @@ public function testValidateGeneralParameterOperationRulesThrowsForNonSupportedO ); } + public function testValidateGeneralParameterOperationRulesThrowsForNonSupportedOperatorCombinations(): void { $this->expectException(MetadataPolicyException::class); @@ -629,6 +649,7 @@ public function testValidateGeneralParameterOperationRulesThrowsForNonSupportedO ); } + public function testValidateSpecificParameterOperationRulesForAddAndSubsetOf(): void { // If add is combined with subset_of, the values of add MUST be a subset of the values of subset_of. @@ -650,6 +671,7 @@ public function testValidateSpecificParameterOperationRulesForAddAndSubsetOf(): ); } + public function testValidateSpecificParameterOperationRulesForSubsetOfAndSupersetOf(): void { // If subset_of is combined with superset_of, the values of subset_of MUST be a superset of the values of @@ -671,6 +693,7 @@ public function testValidateSpecificParameterOperationRulesForSubsetOfAndSuperse ); } + public function testCanValidateMetadataParameterValueType(): void { // We have already ensured rules, so just take a few cases to check that this method works. diff --git a/tests/src/Codebooks/WellKnownEnumTest.php b/tests/src/Codebooks/WellKnownEnumTest.php index c13e0e8..b3fccda 100644 --- a/tests/src/Codebooks/WellKnownEnumTest.php +++ b/tests/src/Codebooks/WellKnownEnumTest.php @@ -19,6 +19,7 @@ public function testReturnsPrefixOnlyPathForPrefixCase(): void ); } + public function testReturnsPrefixAndValueForNonPrefixCase(): void { $path = WellKnownEnum::OpenIdFederation->path(); @@ -33,6 +34,7 @@ public function testReturnsPrefixAndValueForNonPrefixCase(): void ); } + public function testCanGetWellKnownUri(): void { $entityId = 'https://example.org'; diff --git a/tests/src/Core/ClientAssertionTest.php b/tests/src/Core/ClientAssertionTest.php index 52e1f7e..ca5f6aa 100644 --- a/tests/src/Core/ClientAssertionTest.php +++ b/tests/src/Core/ClientAssertionTest.php @@ -38,7 +38,6 @@ final class ClientAssertionTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected array $expiredPayload = [ @@ -57,6 +56,7 @@ final class ClientAssertionTest extends TestCase protected array $validPayload; + protected function setUp(): void { $jwsMock = $this->createMock(JWS::class); @@ -86,6 +86,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -114,12 +115,14 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); $this->assertInstanceOf(ClientAssertion::class, $this->sut()); } + public function testCanGetPayloadClaims(): void { $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); @@ -129,6 +132,7 @@ public function testCanGetPayloadClaims(): void $this->assertSame($this->validPayload['iss'], $sut->getIssuer()); } + public function testThrowsOnExpiredJws(): void { $this->jsonHelperMock->method('decode')->willReturn($this->expiredPayload); @@ -137,6 +141,7 @@ public function testThrowsOnExpiredJws(): void $this->sut(); } + public function testThrowsOnNonSameIssuerAndSubject(): void { $invalidPayload = $this->validPayload; diff --git a/tests/src/Core/Factories/ClientAssertionFactoryTest.php b/tests/src/Core/Factories/ClientAssertionFactoryTest.php index 1763032..ab9d89c 100644 --- a/tests/src/Core/Factories/ClientAssertionFactoryTest.php +++ b/tests/src/Core/Factories/ClientAssertionFactoryTest.php @@ -42,7 +42,6 @@ final class ClientAssertionFactoryTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected array $expiredPayload = [ @@ -61,6 +60,7 @@ final class ClientAssertionFactoryTest extends TestCase protected array $validPayload; + protected function setUp(): void { $jwsMock = $this->createMock(JWS::class); @@ -93,6 +93,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -121,6 +122,7 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(ClientAssertionFactory::class, $this->sut()); diff --git a/tests/src/Core/Factories/RequestObjectFactoryTest.php b/tests/src/Core/Factories/RequestObjectFactoryTest.php index 0160b8a..82390d7 100644 --- a/tests/src/Core/Factories/RequestObjectFactoryTest.php +++ b/tests/src/Core/Factories/RequestObjectFactoryTest.php @@ -30,8 +30,6 @@ final class RequestObjectFactoryTest extends TestCase { protected MockObject $signatureMock; - - protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -44,7 +42,6 @@ final class RequestObjectFactoryTest extends TestCase protected MockObject $helpersMock; - protected MockObject $claimFactoryMock; protected array $sampleHeader = [ @@ -53,6 +50,7 @@ final class RequestObjectFactoryTest extends TestCase 'kid' => 'LfgZECDYkSTHmbllBD5_Tkwvy3CtOpNYQ7-DfQawTww', ]; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -78,6 +76,7 @@ protected function setUp(): void $this->claimFactoryMock = $this->createMock(ClaimFactory::class); } + protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -106,6 +105,7 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(RequestObjectFactory::class, $this->sut()); diff --git a/tests/src/Core/RequestObjectTest.php b/tests/src/Core/RequestObjectTest.php index dea2eab..13a045a 100644 --- a/tests/src/Core/RequestObjectTest.php +++ b/tests/src/Core/RequestObjectTest.php @@ -27,7 +27,6 @@ final class RequestObjectTest extends TestCase { protected MockObject $signatureMock; - protected MockObject $jwsDecoratorMock; protected MockObject $jwsVerifierDecoratorMock; @@ -40,8 +39,6 @@ final class RequestObjectTest extends TestCase protected MockObject $helpersMock; - - protected MockObject $claimFactoryMock; protected array $sampleHeader = [ @@ -50,6 +47,7 @@ final class RequestObjectTest extends TestCase 'kid' => 'LfgZECDYkSTHmbllBD5_Tkwvy3CtOpNYQ7-DfQawTww', ]; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -76,6 +74,7 @@ protected function setUp(): void $this->claimFactoryMock = $this->createMock(ClaimFactory::class); } + protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -104,11 +103,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(RequestObject::class, $this->sut()); } + public function testCanCheckIsProtected(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); @@ -116,6 +117,7 @@ public function testCanCheckIsProtected(): void $this->assertTrue($this->sut()->isProtected()); } + public function testCanCheckIsNotProtected(): void { $header = $this->sampleHeader; @@ -126,6 +128,7 @@ public function testCanCheckIsNotProtected(): void $this->assertFalse($this->sut()->isProtected()); } + public function testThrowsForNonExistingAlgHeader(): void { diff --git a/tests/src/CoreTest.php b/tests/src/CoreTest.php index fa87851..fadf588 100644 --- a/tests/src/CoreTest.php +++ b/tests/src/CoreTest.php @@ -53,6 +53,7 @@ final class CoreTest extends TestCase protected MockObject $loggerMock; + protected function setUp(): void { $this->supportedAlgorithmsMock = $this->createMock(SupportedAlgorithms::class); @@ -61,6 +62,7 @@ protected function setUp(): void $this->loggerMock = $this->createMock(LoggerInterface::class); } + protected function sut( ?SupportedAlgorithms $supportedAlgorithms = null, ?SupportedSerializers $supportedSerializers = null, @@ -80,6 +82,7 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf( @@ -88,6 +91,7 @@ public function testCanCreateInstance(): void ); } + public function testCanBuildTools(): void { $sut = $this->sut(); diff --git a/tests/src/Decorators/CacheDecoratorTest.php b/tests/src/Decorators/CacheDecoratorTest.php index 92e59b9..7469fed 100644 --- a/tests/src/Decorators/CacheDecoratorTest.php +++ b/tests/src/Decorators/CacheDecoratorTest.php @@ -15,11 +15,13 @@ final class CacheDecoratorTest extends TestCase { protected MockObject $cacheMock; + protected function setUp(): void { $this->cacheMock = $this->createMock(CacheInterface::class); } + protected function sut( ?CacheInterface $cache = null, ): CacheDecorator { @@ -28,29 +30,34 @@ protected function sut( return new CacheDecorator($cache); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(CacheDecorator::class, $this->sut()); } + public function testGet(): void { $this->cacheMock->expects($this->once())->method('get')->willReturn('value'); $this->assertEquals('value', $this->sut()->get(null, 'key')); } + public function testSet(): void { $this->cacheMock->expects($this->once())->method('set'); $this->sut()->set('value', 10, 'key'); } + public function testHas(): void { $this->cacheMock->expects($this->once())->method('has')->willReturn(true); $this->assertTrue($this->sut()->has('key')); } + public function testDelete(): void { $this->cacheMock->expects($this->once())->method('delete'); diff --git a/tests/src/Decorators/DateIntervalDecoratorTest.php b/tests/src/Decorators/DateIntervalDecoratorTest.php index fa3b222..683aa00 100644 --- a/tests/src/Decorators/DateIntervalDecoratorTest.php +++ b/tests/src/Decorators/DateIntervalDecoratorTest.php @@ -14,11 +14,13 @@ final class DateIntervalDecoratorTest extends TestCase { protected DateInterval $dateInterval; + protected function setUp(): void { $this->dateInterval = new DateInterval('P1D'); } + protected function sut( ?DateInterval $dateInterval = null, ): DateIntervalDecorator { @@ -27,16 +29,19 @@ protected function sut( return new DateIntervalDecorator($dateInterval); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(DateIntervalDecorator::class, $this->sut()); } + public function testCanGetInSeconds(): void { $this->assertSame(86400, $this->sut()->getInSeconds()); } + public function testCanGetLowestInSecondsComparedToExpirationTime(): void { $expirationTime = time() + 100; diff --git a/tests/src/Decorators/HttpClientDecoratorTest.php b/tests/src/Decorators/HttpClientDecoratorTest.php index f90af40..bd3bb2c 100644 --- a/tests/src/Decorators/HttpClientDecoratorTest.php +++ b/tests/src/Decorators/HttpClientDecoratorTest.php @@ -20,12 +20,14 @@ final class HttpClientDecoratorTest extends TestCase protected MockObject $responseInterfaceMock; + protected function setUp(): void { $this->clientMock = $this->createMock(Client::class); $this->responseInterfaceMock = $this->createMock(ResponseInterface::class); } + protected function sut( ?Client $client = null, ): HttpClientDecorator { @@ -34,11 +36,13 @@ protected function sut( return new HttpClientDecorator($client); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(HttpClientDecorator::class, $this->sut()); } + public function testRequestThrowsForRequestError(): void { $this->clientMock->method('request')->willThrowException(new \Exception('error')); @@ -48,6 +52,7 @@ public function testRequestThrowsForRequestError(): void $this->sut()->request(HttpMethodsEnum::GET, 'https://example.com'); } + public function testRequestThrowsForNon200Response(): void { $this->responseInterfaceMock->method('getStatusCode')->willReturn(500); @@ -58,6 +63,7 @@ public function testRequestThrowsForNon200Response(): void $this->sut()->request(HttpMethodsEnum::GET, 'https://example.com'); } + public function testCanRequest(): void { $this->responseInterfaceMock->method('getStatusCode')->willReturn(200); diff --git a/tests/src/Did/DidKeyJwkResolverTest.php b/tests/src/Did/DidKeyJwkResolverTest.php index 43b2ac3..f96ddb9 100644 --- a/tests/src/Did/DidKeyJwkResolverTest.php +++ b/tests/src/Did/DidKeyJwkResolverTest.php @@ -22,6 +22,7 @@ final class DidKeyJwkResolverTest extends TestCase { private MockObject $helpersMock; + protected function setUp(): void { $base64UrlMock = $this->createMock(Base64Url::class); @@ -35,6 +36,7 @@ protected function setUp(): void ->willReturn($base64UrlMock); } + protected function sut( ?Helpers $helpers = null, ): DidKeyJwkResolver { @@ -43,6 +45,7 @@ protected function sut( return new DidKeyJwkResolver($helpers); } + /** * Test that invalid did:key format throws an exception */ @@ -54,6 +57,7 @@ public function testInvalidDidKeyFormatThrowsException(): void $this->sut()->extractJwkFromDidKey('invalid:key:value'); } + /** * Test that unsupported multibase encoding throws an exception */ @@ -67,6 +71,7 @@ public function testUnsupportedMultibaseEncodingThrowsException(): void $this->sut()->extractJwkFromDidKey('did:key:a123'); // 'a' prefix is not supported } + /** * Test base58BtcDecode method with invalid character */ @@ -78,6 +83,7 @@ public function testBase58BtcDecodeWithInvalidCharacter(): void $this->sut()->base58BtcDecode('invalid*string'); // '*' is not in the base58 alphabet } + /** * Test base58BtcDecode method with valid input */ @@ -90,6 +96,7 @@ public function testBase58BtcDecodeWithValidInput(): void $this->assertSame("\0\0\0", $result); } + /** * Test createEd25519Jwk method */ @@ -110,6 +117,7 @@ public function testCreateEd25519Jwk(): void $this->assertSame($expectedJwk, $jwk); } + /** * Test createX25519Jwk method */ @@ -130,6 +138,7 @@ public function testCreateX25519Jwk(): void $this->assertSame($expectedJwk, $jwk); } + /** * Test createSecp256k1Jwk method with valid uncompressed point */ @@ -154,6 +163,7 @@ public function testCreateSecp256k1JwkWithValidUncompressedPoint(): void $this->assertSame($expectedJwk, $jwk); } + /** * Test createSecp256k1Jwk method with invalid key format */ @@ -168,6 +178,7 @@ public function testCreateSecp256k1JwkWithInvalidKeyFormat(): void $this->sut()->createSecp256k1Jwk($rawKeyBytes); } + /** * Test createSecp256k1Jwk method with compressed point format */ @@ -182,6 +193,7 @@ public function testCreateSecp256k1JwkWithCompressedPoint(): void $this->sut()->createSecp256k1Jwk($rawKeyBytes); } + /** * Test createP256Jwk method with valid uncompressed point */ @@ -205,6 +217,7 @@ public function testCreateP256JwkWithValidUncompressedPoint(): void $this->assertSame($expectedJwk, $jwk); } + /** * Test createP256Jwk method with invalid key format */ @@ -219,6 +232,7 @@ public function testCreateP256JwkWithInvalidKeyFormat(): void $this->sut()->createP256Jwk($rawKeyBytes); } + /** * Test createP384Jwk method with valid uncompressed point */ @@ -242,6 +256,7 @@ public function testCreateP384JwkWithValidUncompressedPoint(): void $this->assertSame($expectedJwk, $jwk); } + /** * Test createP384Jwk method with invalid key format */ @@ -256,6 +271,7 @@ public function testCreateP384JwkWithInvalidKeyFormat(): void $this->sut()->createP384Jwk($rawKeyBytes); } + /** * Test createP521Jwk method with valid uncompressed point */ @@ -279,6 +295,7 @@ public function testCreateP521JwkWithValidUncompressedPoint(): void $this->assertSame($expectedJwk, $jwk); } + /** * Test createP521Jwk method with invalid key format */ @@ -293,6 +310,7 @@ public function testCreateP521JwkWithInvalidKeyFormat(): void $this->sut()->createP521Jwk($rawKeyBytes); } + public function testExtractJwkFromDidKeyWithUnsupportedMulticodecIdentifier(): void { $resolverMock = $this->getMockBuilder(DidKeyJwkResolver::class) @@ -310,6 +328,7 @@ public function testExtractJwkFromDidKeyWithUnsupportedMulticodecIdentifier(): v $resolverMock->extractJwkFromDidKey('did:key:z123'); } + /** * Test extractJwkFromDidKey with an Ed25519 key */ @@ -347,6 +366,7 @@ public function testExtractJwkFromDidKeyWithEd25519Key(): void $this->assertEquals($expectedJwk, $jwk); } + /** * Test the integrated flow of extractJwkFromDidKey */ @@ -373,6 +393,7 @@ public function testExtractJwkFromDidKeyIntegrated(): void $this->assertArrayHasKey('x', $jwk); } + /** * Test that an exception in base58BtcDecode is properly wrapped */ @@ -394,6 +415,7 @@ public function testExtractJwkFromDidKeyWithDecodingException(): void $resolverMock->extractJwkFromDidKey('did:key:z123'); } + /** * Test extraction with real Ed25519 did:key value */ @@ -414,6 +436,7 @@ public function testExtractJwkFromRealEd25519DidKey(): void $this->assertArrayHasKey('x', $jwk); } + /** * Test extraction with the provided sample did:key value */ @@ -462,6 +485,7 @@ public function testMultipleRealDidKeys(string $didKey, string $expectedCrv, str } } + /** * Data provider for real did:key values */ @@ -479,6 +503,7 @@ public static function provideRealDidKeys(): \Iterator ]; } + public function testRealDidKeys(): void { $sut = $this->sut(new Helpers()); @@ -495,6 +520,7 @@ public function testRealDidKeys(): void $this->assertEquals('sig', $jwk['use']); } + public function testCreateJwkFromRawJson(): void { $sut = $this->sut(new Helpers()); @@ -511,6 +537,7 @@ public function testCreateJwkFromRawJson(): void $this->assertEquals('sig', $jwk['use']); } + public function testCreateJwkFromInvalidJsonThrows(): void { $sut = $this->sut(new Helpers()); @@ -522,6 +549,7 @@ public function testCreateJwkFromInvalidJsonThrows(): void $sut->createJwkFromRawJson($invalidJson); } + public function testCreateJwkFromInvalidJwkFormatThrows(): void { $sut = $this->sut(new Helpers()); @@ -533,6 +561,7 @@ public function testCreateJwkFromInvalidJwkFormatThrows(): void $sut->createJwkFromRawJson($invalidJwk); } + public function testCreateJwkFromInvalidEcJwkFormatThrows(): void { $sut = $this->sut(new Helpers()); @@ -544,6 +573,7 @@ public function testCreateJwkFromInvalidEcJwkFormatThrows(): void $sut->createJwkFromRawJson($invalidEcJwk); } + #[DataProvider('varintValidDataProvider')] public function testVarintDecodeValid(string $bytes, int $expectedValue, int $expectedLength): void { @@ -552,6 +582,7 @@ public function testVarintDecodeValid(string $bytes, int $expectedValue, int $ex $this->assertSame($expectedLength, $length, "Decoded length mismatch for input: " . bin2hex($bytes)); } + public static function varintValidDataProvider(): \Iterator { // Single byte @@ -589,6 +620,7 @@ public function testVarintDecodeInvalid(string $bytes, string $expectedException $this->sut()->varintDecode($bytes); } + public static function varintInvalidDataProvider(): \Iterator { yield 'empty string' => ['', 'Invalid varint: input is empty']; @@ -618,6 +650,7 @@ public static function varintInvalidDataProvider(): \Iterator ]; } + #[DataProvider('base58DecodeValidDataProvider')] public function testBase58BtcDecodeValidInputs(string $base58encoded, string $expectedDecoded): void { @@ -638,6 +671,7 @@ public function testBase58BtcDecodeValidInputs(string $base58encoded, string $ex $this->assertSame($expectedDecoded, $actualDecoded, "Failed for input: " . $base58encoded); } + public static function base58DecodeValidDataProvider(): \Iterator { yield 'empty string' => ['', '']; @@ -667,6 +701,7 @@ public static function base58DecodeValidDataProvider(): \Iterator yield 'bs58 lib vector 1' => ['111233QC4', "\x00\x00\x00\x28\x7f\xb4\xcd"]; } + #[DataProvider('base58DecodeInvalidCharDataProvider')] public function testBase58BtcDecodeThrowsOnInvalidCharacter(string $base58invalid): void { @@ -676,6 +711,7 @@ public function testBase58BtcDecodeThrowsOnInvalidCharacter(string $base58invali $this->sut()->base58BtcDecode($base58invalid); } + public static function base58DecodeInvalidCharDataProvider(): \Iterator { yield 'invalid char "0" (zero)' => ['0']; diff --git a/tests/src/Factories/AlgorithmManagerDecoratorFactoryTest.php b/tests/src/Factories/AlgorithmManagerDecoratorFactoryTest.php index 2de7983..03336bc 100644 --- a/tests/src/Factories/AlgorithmManagerDecoratorFactoryTest.php +++ b/tests/src/Factories/AlgorithmManagerDecoratorFactoryTest.php @@ -21,7 +21,6 @@ final class AlgorithmManagerDecoratorFactoryTest extends TestCase protected MockObject $supportedAlgorithmsMock; - protected function setUp(): void { $this->supportedAlgorithmsMock = $this->createMock(SupportedAlgorithms::class); @@ -36,11 +35,13 @@ protected function setUp(): void ]); } + protected function sut(): AlgorithmManagerDecoratorFactory { return new AlgorithmManagerDecoratorFactory(); } + public function testCanCreateInstance(): void { $this->assertInstanceOf( @@ -49,6 +50,7 @@ public function testCanCreateInstance(): void ); } + public function testCanBuild(): void { $this->assertInstanceOf( diff --git a/tests/src/Factories/CacheDecoratorFactoryTest.php b/tests/src/Factories/CacheDecoratorFactoryTest.php index 7d4e0f7..a3d264a 100644 --- a/tests/src/Factories/CacheDecoratorFactoryTest.php +++ b/tests/src/Factories/CacheDecoratorFactoryTest.php @@ -18,16 +18,19 @@ final class CacheDecoratorFactoryTest extends TestCase { protected MockObject $cacheInterfaceMock; + protected function setUp(): void { $this->cacheInterfaceMock = $this->createMock(CacheInterface::class); } + protected function sut(): CacheDecoratorFactory { return new CacheDecoratorFactory(); } + public function testCacnCreateInstance(): void { $this->assertInstanceOf( @@ -36,6 +39,7 @@ public function testCacnCreateInstance(): void ); } + public function testCanBuild(): void { $this->assertInstanceOf( diff --git a/tests/src/Factories/ClaimFactoryTest.php b/tests/src/Factories/ClaimFactoryTest.php index 08cdf7a..abeebbd 100644 --- a/tests/src/Factories/ClaimFactoryTest.php +++ b/tests/src/Factories/ClaimFactoryTest.php @@ -38,11 +38,13 @@ final class ClaimFactoryTest extends TestCase ], ]; + protected function setUp(): void { $this->helpers = new Helpers(); } + protected function sut( ?Helpers $helpers = null, ): ClaimFactory { @@ -53,16 +55,19 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(ClaimFactory::class, $this->sut()); } + public function testCanGetForFederation(): void { $this->assertInstanceOf(FederationClaimFactory::class, $this->sut()->forFederation()); } + public function testCanBuildGeneric(): void { $this->assertInstanceOf( @@ -71,6 +76,7 @@ public function testCanBuildGeneric(): void ); } + public function testCanBuildJwks(): void { $this->assertInstanceOf( @@ -79,6 +85,7 @@ public function testCanBuildJwks(): void ); } + public function testBuildJwksThrowsIfNoKeysKey(): void { $this->expectException(JwksException::class); @@ -87,6 +94,7 @@ public function testBuildJwksThrowsIfNoKeysKey(): void $this->sut()->buildJwks([]); } + public function testBuildJwksThrowsIfNoJwkKey(): void { $this->expectException(JwksException::class); diff --git a/tests/src/Factories/DateIntervalDecoratorFactoryTest.php b/tests/src/Factories/DateIntervalDecoratorFactoryTest.php index 54c65ae..1990a5c 100644 --- a/tests/src/Factories/DateIntervalDecoratorFactoryTest.php +++ b/tests/src/Factories/DateIntervalDecoratorFactoryTest.php @@ -17,21 +17,25 @@ final class DateIntervalDecoratorFactoryTest extends TestCase { protected DateInterval $dateInterval; + protected function setUp(): void { $this->dateInterval = new DateInterval('P1D'); } + protected function sut(): DateIntervalDecoratorFactory { return new DateIntervalDecoratorFactory(); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(DateIntervalDecoratorFactory::class, $this->sut()); } + public function testCanBuild(): void { $this->assertInstanceOf( diff --git a/tests/src/Factories/HttpClientDecoratorFactoryTest.php b/tests/src/Factories/HttpClientDecoratorFactoryTest.php index de7af1e..a4b9633 100644 --- a/tests/src/Factories/HttpClientDecoratorFactoryTest.php +++ b/tests/src/Factories/HttpClientDecoratorFactoryTest.php @@ -18,21 +18,25 @@ final class HttpClientDecoratorFactoryTest extends TestCase { protected MockObject $clientMock; + protected function setUp(): void { $this->clientMock = $this->createMock(Client::class); } + protected function sut(): HttpClientDecoratorFactory { return new HttpClientDecoratorFactory(); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(HttpClientDecoratorFactory::class, $this->sut()); } + public function testCanBuild(): void { $this->assertInstanceOf( diff --git a/tests/src/Factories/JwsSerializerManagerDecoratorFactoryTest.php b/tests/src/Factories/JwsSerializerManagerDecoratorFactoryTest.php index 1857eb6..e5ce6fe 100644 --- a/tests/src/Factories/JwsSerializerManagerDecoratorFactoryTest.php +++ b/tests/src/Factories/JwsSerializerManagerDecoratorFactoryTest.php @@ -21,7 +21,6 @@ final class JwsSerializerManagerDecoratorFactoryTest extends TestCase protected MockObject $supportedSerializersMock; - protected function setUp(): void { $this->supportedSerializersMock = $this->createMock(SupportedSerializers::class); @@ -35,11 +34,13 @@ protected function setUp(): void ->willReturn([$jwsSerializerMock]); } + protected function sut(): JwsSerializerManagerDecoratorFactory { return new JwsSerializerManagerDecoratorFactory(); } + public function testCanCreateInstance(): void { $this->assertInstanceOf( @@ -48,6 +49,7 @@ public function testCanCreateInstance(): void ); } + public function testCanBuild(): void { $this->assertInstanceOf( diff --git a/tests/src/Federation/Claims/TrustMarkOwnersClaimBagTest.php b/tests/src/Federation/Claims/TrustMarkOwnersClaimBagTest.php index a5a81af..4d38bac 100644 --- a/tests/src/Federation/Claims/TrustMarkOwnersClaimBagTest.php +++ b/tests/src/Federation/Claims/TrustMarkOwnersClaimBagTest.php @@ -15,6 +15,7 @@ final class TrustMarkOwnersClaimBagTest extends TestCase { protected MockObject $trustMarkOwnersClaimValueMock; + protected function setUp(): void { $this->trustMarkOwnersClaimValueMock = $this->createMock(TrustMarkOwnersClaimValue::class); @@ -22,17 +23,20 @@ protected function setUp(): void $this->trustMarkOwnersClaimValueMock->method('getSubject')->willReturn('subject'); } + protected function sut( TrustMarkOwnersClaimValue ...$trustMarkOwnersClaimValue, ): TrustMarkOwnersClaimBag { return new TrustMarkOwnersClaimBag(...$trustMarkOwnersClaimValue); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustMarkOwnersClaimBag::class, $this->sut()); } + public function testCanAddAndGet(): void { $this->assertEmpty($this->sut()->getAll()); @@ -49,6 +53,7 @@ public function testCanAddAndGet(): void $this->assertSame($trustMarkClaimValueMock2, $sut->get('trustMarkType2')); } + public function testCanJsonSerialize(): void { $trustMarkClaimValueMock2 = $this->createMock(TrustMarkOwnersClaimValue::class); diff --git a/tests/src/Federation/Claims/TrustMarkOwnersClaimValueTest.php b/tests/src/Federation/Claims/TrustMarkOwnersClaimValueTest.php index d128011..854f372 100644 --- a/tests/src/Federation/Claims/TrustMarkOwnersClaimValueTest.php +++ b/tests/src/Federation/Claims/TrustMarkOwnersClaimValueTest.php @@ -21,6 +21,7 @@ final class TrustMarkOwnersClaimValueTest extends TestCase protected array $otherClaims; + protected function setUp(): void { $this->trustMarkType = 'trustMarkType'; @@ -29,6 +30,7 @@ protected function setUp(): void $this->otherClaims = ['key' => 'value']; } + protected function sut( ?string $trustMarkType = null, ?string $subject = null, @@ -48,11 +50,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustMarkOwnersClaimValue::class, $this->sut()); } + public function testCanGetProperties(): void { $sut = $this->sut(); @@ -62,6 +66,7 @@ public function testCanGetProperties(): void $this->assertSame($this->otherClaims, $sut->getOtherClaims()); } + public function testCanJsonSerialize(): void { $this->assertSame( diff --git a/tests/src/Federation/Claims/TrustMarksClaimBagTest.php b/tests/src/Federation/Claims/TrustMarksClaimBagTest.php index cff58dd..b604261 100644 --- a/tests/src/Federation/Claims/TrustMarksClaimBagTest.php +++ b/tests/src/Federation/Claims/TrustMarksClaimBagTest.php @@ -15,6 +15,7 @@ final class TrustMarksClaimBagTest extends TestCase { protected MockObject $trustMarkClaimMock; + protected function setUp(): void { $this->trustMarkClaimMock = $this->createMock(TrustMarksClaimValue::class); @@ -22,18 +23,21 @@ protected function setUp(): void $this->trustMarkClaimMock->method('getTrustMark')->willReturn('token'); } + protected function sut( TrustMarksClaimValue ...$trustMarkClaims, ): TrustMarksClaimBag { return new TrustMarksClaimBag(...$trustMarkClaims); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustMarksClaimBag::class, $this->sut()); $this->assertInstanceOf(TrustMarksClaimBag::class, $this->sut($this->trustMarkClaimMock)); } + public function testCanGetAll(): void { $sut = $this->sut(); @@ -45,6 +49,7 @@ public function testCanGetAll(): void $this->assertCount(3, $sut->getAll()); } + public function testCanGetAllFor(): void { $firstTrustMarkClaim = $this->createMock(TrustMarksClaimValue::class); @@ -60,6 +65,7 @@ public function testCanGetAllFor(): void $this->assertSame($secondTrustMarkClaim->getTrustMarkType(), $allForSecond[0]->getTrustMarkType()); } + public function testCanGetFirstFor(): void { $firstTrustMarkClaim = $this->createMock(TrustMarksClaimValue::class); @@ -74,6 +80,7 @@ public function testCanGetFirstFor(): void $this->assertSame($secondTrustMarkClaim->getTrustMarkType(), $second->getTrustMarkType()); } + public function testGetFirstForReturnNullIfNoneFound(): void { @@ -85,6 +92,7 @@ public function testGetFirstForReturnNullIfNoneFound(): void $this->assertNotInstanceOf(TrustMarksClaimValue::class, $sut->getFirstFor('second')); } + public function testCanJsonSerialize(): void { $sut = $this->sut($this->trustMarkClaimMock); diff --git a/tests/src/Federation/Claims/TrustMarksClaimValueTest.php b/tests/src/Federation/Claims/TrustMarksClaimValueTest.php index 0bacfb3..c8b8048 100644 --- a/tests/src/Federation/Claims/TrustMarksClaimValueTest.php +++ b/tests/src/Federation/Claims/TrustMarksClaimValueTest.php @@ -17,6 +17,7 @@ final class TrustMarksClaimValueTest extends TestCase protected array $otherClaims = []; + protected function setUp(): void { $this->trustMarkType = 'trustMarkType'; @@ -24,6 +25,7 @@ protected function setUp(): void $this->otherClaims = ['something' => 'else']; } + protected function sut( ?string $trustMarkType = null, ?string $trustMark = null, @@ -36,11 +38,13 @@ protected function sut( return new TrustMarksClaimValue($trustMarkType, $trustMark, $otherClaims); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustMarksClaimValue::class, $this->sut()); } + public function testCanGetProperties(): void { $sut = $this->sut(); @@ -49,6 +53,7 @@ public function testCanGetProperties(): void $this->assertSame($this->otherClaims, $sut->getOtherClaims()); } + public function testCanJsonSerialize(): void { $this->assertSame( diff --git a/tests/src/Federation/EntityStatementFetcherTest.php b/tests/src/Federation/EntityStatementFetcherTest.php index e73895a..e218fd9 100644 --- a/tests/src/Federation/EntityStatementFetcherTest.php +++ b/tests/src/Federation/EntityStatementFetcherTest.php @@ -42,6 +42,7 @@ final class EntityStatementFetcherTest extends TestCase protected MockObject $entityStatementMock; + protected function setUp(): void { $this->entityStatementFactoryMock = $this->createMock(EntityStatementFactory::class); @@ -56,6 +57,7 @@ protected function setUp(): void $this->entityStatementMock = $this->createMock(EntityStatement::class); } + protected function sut( ?EntityStatementFactory $entityStatementFactory = null, ?ArtifactFetcher $artifactFetcher = null, @@ -78,11 +80,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(EntityStatementFetcher::class, $this->sut()); } + public function testHasRightExpectedContentTypeHttpHeader(): void { $this->assertSame( @@ -91,6 +95,7 @@ public function testHasRightExpectedContentTypeHttpHeader(): void ); } + public function testCanFetchFromCacheOrWellKnownEndpoint(): void { $this->artifactFetcherMock->expects($this->once())->method('fromCacheAsString') @@ -109,6 +114,7 @@ public function testCanFetchFromCacheOrWellKnownEndpoint(): void ); } + public function testCanFetchFromCacheOrFetchEndpoint(): void { $this->entityStatementMock->expects($this->once()) @@ -124,6 +130,7 @@ public function testCanFetchFromCacheOrFetchEndpoint(): void $this->sut()->fromCacheOrFetchEndpoint('entityId', $this->entityStatementMock); } + public function testFetchFromCacheOrFetchEndpointThrowsIfNoFetchEndpoint(): void { $this->entityStatementMock->expects($this->once()) @@ -136,6 +143,7 @@ public function testFetchFromCacheOrFetchEndpointThrowsIfNoFetchEndpoint(): void $this->sut()->fromCacheOrFetchEndpoint('entityId', $this->entityStatementMock); } + public function testCanFetchFromCache(): void { $this->artifactFetcherMock->expects($this->once())->method('fromCacheAsString') diff --git a/tests/src/Federation/EntityStatementTest.php b/tests/src/Federation/EntityStatementTest.php index 0531bea..53a746e 100644 --- a/tests/src/Federation/EntityStatementTest.php +++ b/tests/src/Federation/EntityStatementTest.php @@ -28,7 +28,6 @@ final class EntityStatementTest extends TestCase { protected MockObject $signatureMock; - protected MockObject $jwsDecoratorMock; protected MockObject $jwsVerifierDecoratorMock; @@ -45,7 +44,6 @@ final class EntityStatementTest extends TestCase protected MockObject $arrHelperMock; - protected MockObject $claimFactoryMock; protected MockObject $federationClaimFactoryMock; @@ -109,6 +107,7 @@ final class EntityStatementTest extends TestCase protected array $validPayload; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -146,6 +145,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -174,6 +174,7 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); @@ -185,6 +186,7 @@ public function testCanCreateInstance(): void ); } + public function testThrowsOnInvalidJwks(): void { $this->validPayload['jwks'] = null; @@ -198,6 +200,7 @@ public function testThrowsOnInvalidJwks(): void $this->sut(); } + public function testIsConfiguration(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); @@ -206,6 +209,7 @@ public function testIsConfiguration(): void $this->assertTrue($this->sut()->isConfiguration()); } + public function testIsNotConfiguration(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); @@ -218,6 +222,7 @@ public function testIsNotConfiguration(): void $this->assertFalse($this->sut()->isConfiguration()); } + public function testVerifyWithKeySetRuns(): void { $this->jwsVerifierDecoratorMock->expects($this->once())->method('verifyWithKeySet') @@ -229,6 +234,7 @@ public function testVerifyWithKeySetRuns(): void $this->sut()->verifyWithKeySet(); } + public function testThrowsOnInvalidAuthorityHints(): void { $this->validPayload['authority_hints'] = 'invalid'; @@ -242,6 +248,7 @@ public function testThrowsOnInvalidAuthorityHints(): void $this->sut(); } + public function testThrowsOnEmptyAuthorityHints(): void { $this->validPayload['authority_hints'] = []; @@ -255,6 +262,7 @@ public function testThrowsOnEmptyAuthorityHints(): void $this->sut(); } + public function testThrowsIfAuthorityHintsNotInConfigurationStatement(): void { $this->validPayload['iss'] = 'something-else'; @@ -268,6 +276,7 @@ public function testThrowsIfAuthorityHintsNotInConfigurationStatement(): void $this->sut(); } + public function testTrustMarksAreOptional(): void { $payload = $this->validPayload; @@ -282,6 +291,7 @@ public function testTrustMarksAreOptional(): void ); } + public function testTrustMarkOwnersIsOptional(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); @@ -293,6 +303,7 @@ public function testTrustMarkOwnersIsOptional(): void ); } + public function testTrustMarkOwnersIsBuildUsingFactoryOptional(): void { $this->validPayload['trust_mark_owners'] = [ @@ -311,6 +322,7 @@ public function testTrustMarkOwnersIsBuildUsingFactoryOptional(): void $this->sut()->getTrustMarkOwners(); } + public function testThrowsOnInvalidTrustMarks(): void { $this->validPayload['trust_marks'] = 'invalid'; @@ -324,6 +336,7 @@ public function testThrowsOnInvalidTrustMarks(): void $this->sut(); } + public function testThrowsOnInvalidTypeHeader(): void { $this->sampleHeader['typ'] = 'invalid'; @@ -337,6 +350,7 @@ public function testThrowsOnInvalidTypeHeader(): void $this->sut(); } + public function testCanGetFederationFetchEndpoint(): void { $payload = $this->validPayload; @@ -356,6 +370,7 @@ public function testCanGetFederationFetchEndpoint(): void $this->assertSame('uri', $this->sut()->getFederationFetchEndpoint()); } + public function testFederationFetchEndpointIsOptional(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); @@ -365,6 +380,7 @@ public function testFederationFetchEndpointIsOptional(): void $this->assertNull($this->sut()->getFederationFetchEndpoint()); } + public function testCanGetFederationTrustMarkEndpoint(): void { $payload = $this->validPayload; @@ -384,6 +400,7 @@ public function testCanGetFederationTrustMarkEndpoint(): void $this->assertSame('uri', $this->sut()->getFederationTrustMarkEndpoint()); } + public function testMetadataIsOptional(): void { $payload = $this->validPayload; @@ -395,6 +412,7 @@ public function testMetadataIsOptional(): void $this->assertNull($this->sut()->getMetadata()); } + public function testThrowsForInvalidMetadataClaim(): void { $payload = $this->validPayload; @@ -409,6 +427,7 @@ public function testThrowsForInvalidMetadataClaim(): void $this->sut()->getMetadata(); } + public function testCanGetMetadataPolicyClaim(): void { $payload = $this->validPayload; @@ -428,6 +447,7 @@ public function testCanGetMetadataPolicyClaim(): void $this->assertSame($payload['metadata_policy'], $this->sut()->getMetadataPolicy()); } + public function testThrowsForInvalidMetadataPolicyClaim(): void { $payload = $this->validPayload; @@ -444,6 +464,7 @@ public function testThrowsForInvalidMetadataPolicyClaim(): void $this->sut()->getMetadataPolicy(); } + public function testThrowsIfMetadataPolicyIsSetInConfigurationStatement(): void { $payload = $this->validPayload; diff --git a/tests/src/Federation/Factories/EntityStatementFactoryTest.php b/tests/src/Federation/Factories/EntityStatementFactoryTest.php index f0c9d50..a31387d 100644 --- a/tests/src/Federation/Factories/EntityStatementFactoryTest.php +++ b/tests/src/Federation/Factories/EntityStatementFactoryTest.php @@ -45,7 +45,6 @@ final class EntityStatementFactoryTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected array $sampleHeader = [ @@ -100,6 +99,7 @@ final class EntityStatementFactoryTest extends TestCase protected array $validPayload; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -135,6 +135,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -163,11 +164,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(EntityStatementFactory::class, $this->sut()); } + public function testCanBuildFromToken(): void { $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); diff --git a/tests/src/Federation/Factories/FederationClaimFactoryTest.php b/tests/src/Federation/Factories/FederationClaimFactoryTest.php index 9953844..5b38b43 100644 --- a/tests/src/Federation/Factories/FederationClaimFactoryTest.php +++ b/tests/src/Federation/Factories/FederationClaimFactoryTest.php @@ -46,12 +46,14 @@ final class FederationClaimFactoryTest extends TestCase ], ]; + protected function setUp(): void { $this->helpers = new Helpers(); $this->claimFactory = new ClaimFactory($this->helpers); } + public function sut( ?Helpers $helpers = null, ?ClaimFactory $claimFactory = null, @@ -65,11 +67,13 @@ public function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(FederationClaimFactory::class, $this->sut()); } + public function testCanBuildTrustMarksClaimValue(): void { $this->assertInstanceOf(TrustMarksClaimValue::class, $this->sut()->buildTrustMarksClaimValue( @@ -78,6 +82,7 @@ public function testCanBuildTrustMarksClaimValue(): void )); } + public function testCanBuildTrustMarksClaimValueFrom(): void { $trustMarksClaimData = [ @@ -92,6 +97,7 @@ public function testCanBuildTrustMarksClaimValueFrom(): void ); } + public function testCanBuildTrustMarksClaimBag(): void { $this->assertInstanceOf( @@ -100,6 +106,7 @@ public function testCanBuildTrustMarksClaimBag(): void ); } + public function testCanBuildTrustMarkOwnersClaimValue(): void { $this->assertInstanceOf(TrustMarkOwnersClaimValue::class, $this->sut()->buildTrustMarkOwnersClaimValue( @@ -109,6 +116,7 @@ public function testCanBuildTrustMarkOwnersClaimValue(): void )); } + public function testCanBuildTrustMarkOwnersClaimBagFrom(): void { $trustMarkOwnersClaimData = [ diff --git a/tests/src/Federation/Factories/RequestObjectFactoryTest.php b/tests/src/Federation/Factories/RequestObjectFactoryTest.php index 9c596fd..d5736e3 100644 --- a/tests/src/Federation/Factories/RequestObjectFactoryTest.php +++ b/tests/src/Federation/Factories/RequestObjectFactoryTest.php @@ -31,8 +31,6 @@ final class RequestObjectFactoryTest extends TestCase { protected MockObject $signatureMock; - - protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -47,7 +45,6 @@ final class RequestObjectFactoryTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected array $sampleHeader = [ @@ -70,6 +67,7 @@ final class RequestObjectFactoryTest extends TestCase protected array $validPayload; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -104,6 +102,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -132,11 +131,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(RequestObjectFactory::class, $this->sut()); } + public function testCanBuildFromToken(): void { $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); diff --git a/tests/src/Federation/Factories/TrustChainBagFactoryTest.php b/tests/src/Federation/Factories/TrustChainBagFactoryTest.php index 151ef01..fcca2b8 100644 --- a/tests/src/Federation/Factories/TrustChainBagFactoryTest.php +++ b/tests/src/Federation/Factories/TrustChainBagFactoryTest.php @@ -18,21 +18,25 @@ final class TrustChainBagFactoryTest extends TestCase { protected MockObject $trustChainMock; + protected function setUp(): void { $this->trustChainMock = $this->createMock(TrustChain::class); } + protected function sut(): TrustChainBagFactory { return new TrustChainBagFactory(); } + public function tetCanCreateInstance(): void { $this->assertInstanceOf(TrustChainBag::class, $this->sut()); } + public function testCanBuildTrustChainBag(): void { $this->assertInstanceOf(TrustChainBag::class, $this->sut()->build($this->trustChainMock)); diff --git a/tests/src/Federation/Factories/TrustChainFactoryTest.php b/tests/src/Federation/Factories/TrustChainFactoryTest.php index 648bb37..8fee7e4 100644 --- a/tests/src/Federation/Factories/TrustChainFactoryTest.php +++ b/tests/src/Federation/Factories/TrustChainFactoryTest.php @@ -32,6 +32,7 @@ final class TrustChainFactoryTest extends TestCase protected MockObject $helpersMock; + protected function setUp(): void { $this->entityStatementFactoryMock = $this->createMock(EntityStatementFactory::class); @@ -41,6 +42,7 @@ protected function setUp(): void $this->helpersMock = $this->createMock(Helpers::class); } + protected function sut( ?EntityStatementFactory $entityStatementFactory = null, ?DateIntervalDecorator $timestampValidationLeewayMock = null, @@ -63,16 +65,19 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustChainFactory::class, $this->sut()); } + public function testCanBuildEmptyTrustChain(): void { $this->assertInstanceOf(TrustChain::class, $this->sut()->empty()); } + public function testCanBuildFromStatements(): void { $expirationTime = time() + 60; @@ -94,6 +99,7 @@ public function testCanBuildFromStatements(): void $this->assertFalse($trustChain->isEmpty()); } + public function testBuildFromStatementsThrowsForLessThanThreeEntityStatements(): void { $expirationTime = time() + 60; @@ -112,6 +118,7 @@ public function testBuildFromStatementsThrowsForLessThanThreeEntityStatements(): $this->sut()->fromStatements($leaf, $subordinate); } + public function testCanBuildFromTokens(): void { $expirationTime = time() + 60; @@ -138,6 +145,7 @@ public function testCanBuildFromTokens(): void ); } + public function testCanBuildForTrustAnchor(): void { $expirationTime = time() + 60; diff --git a/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php b/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php index 3348800..b1584f1 100644 --- a/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php +++ b/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php @@ -27,8 +27,6 @@ final class TrustMarkDelegationFactoryTest extends TestCase { protected MockObject $signatureMock; - - protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -43,7 +41,6 @@ final class TrustMarkDelegationFactoryTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected array $sampleHeader = [ @@ -65,6 +62,7 @@ final class TrustMarkDelegationFactoryTest extends TestCase protected array $validPayload; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -100,6 +98,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -128,13 +127,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustMarkDelegationFactory::class, $this->sut()); } - public function testCanBuild(): void { $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); diff --git a/tests/src/Federation/Factories/TrustMarkFactoryTest.php b/tests/src/Federation/Factories/TrustMarkFactoryTest.php index 2bcbb64..ba6efc4 100644 --- a/tests/src/Federation/Factories/TrustMarkFactoryTest.php +++ b/tests/src/Federation/Factories/TrustMarkFactoryTest.php @@ -31,8 +31,6 @@ final class TrustMarkFactoryTest extends TestCase { protected MockObject $signatureMock; - - protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -47,7 +45,6 @@ final class TrustMarkFactoryTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected array $sampleHeader = [ @@ -68,6 +65,7 @@ final class TrustMarkFactoryTest extends TestCase protected array $validPayload; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -103,6 +101,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -131,11 +130,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustMarkFactory::class, $this->sut()); } + public function testCanBuild(): void { $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); diff --git a/tests/src/Federation/MetadataPolicyApplicatorTest.php b/tests/src/Federation/MetadataPolicyApplicatorTest.php index e60a179..3b26a24 100644 --- a/tests/src/Federation/MetadataPolicyApplicatorTest.php +++ b/tests/src/Federation/MetadataPolicyApplicatorTest.php @@ -70,11 +70,13 @@ final class MetadataPolicyApplicatorTest extends TestCase ], ]; + protected function setUp(): void { $this->helpers = new Helpers(); } + protected function sut( ?Helpers $helpers = null, ): MetadataPolicyApplicator { @@ -83,11 +85,13 @@ protected function sut( return new MetadataPolicyApplicator($helpers); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(MetadataPolicyApplicator::class, $this->sut()); } + public function testCanApplyBasicMetadataPolicy(): void { $expectedResolvedMetadata = [ @@ -119,6 +123,7 @@ public function testCanApplyBasicMetadataPolicy(): void $this->assertSame($expectedResolvedMetadata, $resolvedMetadata); } + public function testCanHandleScopeClaims(): void { $metadataPolicy = [ @@ -142,6 +147,7 @@ public function testCanHandleScopeClaims(): void ); } + public function testCanUnsetMetadataValue(): void { $metadataPolicy = [ @@ -160,6 +166,7 @@ public function testCanUnsetMetadataValue(): void ); } + public function testCanAddNonExistingMetadataValue(): void { $metadataPolicy = [ @@ -176,6 +183,7 @@ public function testCanAddNonExistingMetadataValue(): void ); } + public function testHasEmptyParameterOnNonExistingParameterForSubsetOf(): void { $metadataPolicy = [ @@ -191,6 +199,7 @@ public function testHasEmptyParameterOnNonExistingParameterForSubsetOf(): void ); } + public function testHasEmptyParameterOnNonExistingParameterForSupersetOf(): void { $metadataPolicy = [ @@ -206,6 +215,7 @@ public function testHasEmptyParameterOnNonExistingParameterForSupersetOf(): void ); } + public function testHasEmptyParameterOnNonEssentialParameter(): void { $metadataPolicy = [ @@ -221,6 +231,7 @@ public function testHasEmptyParameterOnNonEssentialParameter(): void ); } + public function testCanHandleValueRule(): void { $metadataPolicy = [ @@ -238,6 +249,7 @@ public function testCanHandleValueRule(): void ); } + public function testCanHandleAddRule(): void { $metadataPolicy = [ @@ -255,6 +267,7 @@ public function testCanHandleAddRule(): void ); } + public function testCanHandleDefaultRule(): void { $metadataPolicy = [ @@ -270,6 +283,7 @@ public function testCanHandleDefaultRule(): void ); } + public function testCanHandleOneOfRule(): void { $metadataPolicy = [ @@ -287,6 +301,7 @@ public function testCanHandleOneOfRule(): void ); } + public function testCanHandleOneOfBreakRule(): void { $metadataPolicy = [ @@ -304,6 +319,7 @@ public function testCanHandleOneOfBreakRule(): void $this->sut()->for($metadataPolicy, $metadata); } + public function testCanHandleSubsetOfRule(): void { $metadataPolicy = [ @@ -321,6 +337,7 @@ public function testCanHandleSubsetOfRule(): void ); } + public function testCanHandleSubsetOfBreakRule(): void { $metadataPolicy = [ @@ -338,6 +355,7 @@ public function testCanHandleSubsetOfBreakRule(): void ); } + public function testCanHandleSupersetOfRule(): void { $metadataPolicy = [ @@ -355,6 +373,7 @@ public function testCanHandleSupersetOfRule(): void ); } + public function testCanHandleSupersetOfBreakRule(): void { $metadataPolicy = [ @@ -372,6 +391,7 @@ public function testCanHandleSupersetOfBreakRule(): void $this->sut()->for($metadataPolicy, $metadata); } + public function testCanHandleEssentialRule(): void { $metadataPolicy = [ @@ -389,6 +409,7 @@ public function testCanHandleEssentialRule(): void ); } + public function testCanHandleEssentialBreakRule(): void { $metadataPolicy = [ diff --git a/tests/src/Federation/MetadataPolicyResolverTest.php b/tests/src/Federation/MetadataPolicyResolverTest.php index 701cd2f..9b2a3ae 100644 --- a/tests/src/Federation/MetadataPolicyResolverTest.php +++ b/tests/src/Federation/MetadataPolicyResolverTest.php @@ -61,11 +61,13 @@ final class MetadataPolicyResolverTest extends TestCase ], ]; + protected function setUp(): void { $this->helpers = new Helpers(); } + protected function sut( ?Helpers $helpers = null, ): MetadataPolicyResolver { @@ -74,11 +76,13 @@ protected function sut( return new MetadataPolicyResolver($helpers); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(MetadataPolicyResolver::class, $this->sut()); } + public function testForHappyFlow(): void { $metadataPolicy = $this->sut()->for( @@ -92,6 +96,7 @@ public function testForHappyFlow(): void $this->assertNotEmpty($metadataPolicy); } + public function testReturnsEmptyArrayIfEntityTypeNotPresent(): void { $this->assertEmpty( @@ -105,6 +110,7 @@ public function testReturnsEmptyArrayIfEntityTypeNotPresent(): void ); } + public function testThrowsInCaseOfDifferentValueOperatorValue(): void { $this->expectException(MetadataPolicyException::class); @@ -122,6 +128,7 @@ public function testThrowsInCaseOfDifferentValueOperatorValue(): void ); } + public function testThrowsForInvalidEssentialOperatorValueChange(): void { $this->expectException(MetadataPolicyException::class); @@ -139,6 +146,7 @@ public function testThrowsForInvalidEssentialOperatorValueChange(): void ); } + public function testSetsEssentialOperatorValueInCaseOfCurrentFalseValue(): void { $trustAnchorMetadataPolicy = $this->trustAnchorMetadataPolicySample; @@ -158,6 +166,7 @@ public function testSetsEssentialOperatorValueInCaseOfCurrentFalseValue(): void $this->assertTrue($metadataPolicy['token_endpoint_auth_method']['essential']); } + public function testThrowsForUnsupportedCriticalOperator(): void { $this->expectException(MetadataPolicyException::class); @@ -176,6 +185,7 @@ public function testThrowsForUnsupportedCriticalOperator(): void ); } + public function testThrowsForEmptyIntersectionForOneOf(): void { $this->expectException(MetadataPolicyException::class); @@ -193,6 +203,7 @@ public function testThrowsForEmptyIntersectionForOneOf(): void ); } + public function testCanEnsureFormat(): void { $this->assertSame( @@ -201,6 +212,7 @@ public function testCanEnsureFormat(): void ); } + public function testEnsureFormatThrowsOnNonStringForEntityTypeKey(): void { $this->expectException(MetadataPolicyException::class); @@ -210,6 +222,7 @@ public function testEnsureFormatThrowsOnNonStringForEntityTypeKey(): void $this->sut()->ensureFormat($policy); } + public function testEnsureFormatThrowsOnNonSArrayForEntityTypeValue(): void { $this->expectException(MetadataPolicyException::class); @@ -219,6 +232,7 @@ public function testEnsureFormatThrowsOnNonSArrayForEntityTypeValue(): void $this->sut()->ensureFormat($policy); } + public function testEnsureFormatThrowsOnNonStringForParameterKey(): void { $this->expectException(MetadataPolicyException::class); @@ -228,6 +242,7 @@ public function testEnsureFormatThrowsOnNonStringForParameterKey(): void $this->sut()->ensureFormat($policy); } + public function testEnsureFormatThrowsOnNonStringForParameterValue(): void { $this->expectException(MetadataPolicyException::class); @@ -237,6 +252,7 @@ public function testEnsureFormatThrowsOnNonStringForParameterValue(): void $this->sut()->ensureFormat($policy); } + public function testEnsureFormatThrowsOnNonStringForOperatorKey(): void { $this->expectException(MetadataPolicyException::class); diff --git a/tests/src/Federation/MetadataPolicyTestVectorsTest.php b/tests/src/Federation/MetadataPolicyTestVectorsTest.php index 3d876ee..be9ec84 100644 --- a/tests/src/Federation/MetadataPolicyTestVectorsTest.php +++ b/tests/src/Federation/MetadataPolicyTestVectorsTest.php @@ -31,6 +31,7 @@ final class MetadataPolicyTestVectorsTest extends TestCase protected static MetadataPolicyApplicator $metadataPolicyApplicator; + public static function setUpBeforeClass(): void { self::$testVectors = json_decode( @@ -45,11 +46,13 @@ public static function setUpBeforeClass(): void self::$metadataPolicyApplicator = new MetadataPolicyApplicator($helpers); } + public function testCanLoadTestVectors(): void { $this->assertNotEmpty(self::$testVectors); } + public function testConformsToAllTestVectors(): void { // Just so we can act as if we are resolving metadata for specific entity type. Not really important. diff --git a/tests/src/Federation/RequestObjectTest.php b/tests/src/Federation/RequestObjectTest.php index efc34a3..c7d6747 100644 --- a/tests/src/Federation/RequestObjectTest.php +++ b/tests/src/Federation/RequestObjectTest.php @@ -39,7 +39,6 @@ final class RequestObjectTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected array $expiredPayload = [ @@ -56,6 +55,7 @@ final class RequestObjectTest extends TestCase protected array $validPayload; + protected function setUp(): void { $signatureMock = $this->createMock(Signature::class); @@ -88,6 +88,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -116,6 +117,7 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); @@ -126,6 +128,7 @@ public function testCanCreateInstance(): void ); } + public function testThrowsIfSubjectIsPresent(): void { $this->validPayload['sub'] = 'sample'; @@ -137,6 +140,7 @@ public function testThrowsIfSubjectIsPresent(): void $this->sut(); } + public function testCanGetTrustChain(): void { $trustChain = ['token', 'token2']; @@ -146,6 +150,7 @@ public function testCanGetTrustChain(): void $this->assertSame($this->sut()->getTrustChain(), $trustChain); } + public function testThrowsForInvalidTrustChainFormat(): void { $this->validPayload['trust_chain'] = 'invalid'; diff --git a/tests/src/Federation/TrustChainBagTest.php b/tests/src/Federation/TrustChainBagTest.php index 86b6ef7..02d70bb 100644 --- a/tests/src/Federation/TrustChainBagTest.php +++ b/tests/src/Federation/TrustChainBagTest.php @@ -16,11 +16,13 @@ final class TrustChainBagTest extends TestCase { protected MockObject $trustChainMock; + protected function setUp(): void { $this->trustChainMock = $this->createMock(TrustChain::class); } + protected function sut( ?TrustChain $trustChain = null, ): TrustChainBag { @@ -29,11 +31,13 @@ protected function sut( return new TrustChainBag($trustChain); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustChainBag::class, $this->sut()); } + public function testCanAdd(): void { $sut = $this->sut(); @@ -42,6 +46,7 @@ public function testCanAdd(): void $this->assertCount(2, $sut->getAll()); } + public function testCanGetShortest(): void { $shortest = $this->createMock(TrustChain::class); @@ -65,6 +70,7 @@ public function testCanGetShortest(): void $this->assertSame(2, $sut->getShortest()->getResolvedLength()); } + public function testCanGetShortestByTrustAnchorPriority(): void { $trustAnchor1 = $this->createMock(EntityStatement::class); @@ -99,6 +105,7 @@ public function testCanGetShortestByTrustAnchorPriority(): void $this->assertNotInstanceOf(TrustChain::class, $sut->getShortestByTrustAnchorPriority('unknown')); } + public function testCanGetCount(): void { $this->assertSame(1, $this->sut()->getCount()); diff --git a/tests/src/Federation/TrustChainResolverTest.php b/tests/src/Federation/TrustChainResolverTest.php index 6bf4e14..d889f8e 100644 --- a/tests/src/Federation/TrustChainResolverTest.php +++ b/tests/src/Federation/TrustChainResolverTest.php @@ -44,6 +44,7 @@ final class TrustChainResolverTest extends TestCase protected array $configChainSample = []; + protected function setUp(): void { $this->entityStatementFetcherMock = $this->createMock(EntityStatementFetcher::class); @@ -66,6 +67,7 @@ protected function setUp(): void ]; } + protected function sut( ?EntityStatementFetcher $entityStatementFetcher = null, ?TrustChainFactory $trustChainFactory = null, @@ -97,11 +99,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustChainResolver::class, $this->sut()); } + public function testCanGetConfigurationChains(): void { $this->entityStatementFetcherMock @@ -130,6 +134,7 @@ public function testCanGetConfigurationChains(): void $this->assertCount(3, $configurationChains[0]); } + public function testWontStartGettingConfigurationChainsIfNoTrustAnchorIds(): void { $this->loggerMock @@ -140,6 +145,7 @@ public function testWontStartGettingConfigurationChainsIfNoTrustAnchorIds(): voi $this->assertEmpty($this->sut()->getConfigurationChains('l', [])); } + public function testCanLimitMaximumConfigurationChainDepth(): void { $sut = $this->sut(maxTrustChainDepth: 2); @@ -168,6 +174,7 @@ public function testCanLimitMaximumConfigurationChainDepth(): void $this->assertEmpty($sut->getConfigurationChains('l', ['t'])); } + public function testCanDetectLoopInConfigurationChains(): void { $this->entityStatementFetcherMock @@ -187,6 +194,7 @@ public function testCanDetectLoopInConfigurationChains(): void $this->assertEmpty($this->sut()->getConfigurationChains('l', ['t'])); } + public function testConfigurationChainIsEmptyOnConfigurationFetchError(): void { $this->entityStatementFetcherMock->method('fromCacheOrWellKnownEndpoint') @@ -195,6 +203,7 @@ public function testConfigurationChainIsEmptyOnConfigurationFetchError(): void $this->assertEmpty($this->sut()->getConfigurationChains('l', ['t'])); } + public function testCanBailOnMaxAuthorityHintsRule(): void { $sut = $this->sut(maxAuthorityHints: 1); @@ -216,6 +225,7 @@ public function testCanBailOnMaxAuthorityHintsRule(): void $this->assertEmpty($sut->getConfigurationChains('l', ['t'])); } + public function testCanResolveTrustChain(): void { $this->entityStatementFetcherMock @@ -241,6 +251,7 @@ public function testCanResolveTrustChain(): void $this->sut()->for('l', ['t']); } + public function testCanResolveMultipleTrustChains(): void { $this->entityStatementFetcherMock @@ -259,6 +270,7 @@ public function testCanResolveMultipleTrustChains(): void $this->sut()->for('l', ['i', 't']); } + public function testCanResolveTrustChainForTrustAnchorOnly(): void { $this->entityStatementFetcherMock @@ -274,6 +286,7 @@ public function testCanResolveTrustChainForTrustAnchorOnly(): void $this->sut()->for('t', ['t']); } + public function testTrustChainResolveChecksCacheFirst(): void { $this->cacheDecoratorMock @@ -294,6 +307,7 @@ public function testTrustChainResolveChecksCacheFirst(): void $this->sut()->for('l', ['t']); } + public function testCanWarnOnCacheErrorDuringTrustChainResolution(): void { $this->cacheDecoratorMock @@ -316,6 +330,7 @@ public function testCanWarnOnCacheErrorDuringTrustChainResolution(): void $this->sut()->for('l', ['t']); } + public function testCanWarnOnTrustChainResolutionSubordinateStatementFetchError(): void { $this->entityStatementFetcherMock @@ -345,6 +360,7 @@ public function testCanWarnOnTrustChainResolutionSubordinateStatementFetchError( $this->sut()->for('l', ['t']); } + public function testTrustChainResolveThrowsOnTrustChainBagFactoryError(): void { $this->entityStatementFetcherMock @@ -374,6 +390,7 @@ public function testTrustChainResolveThrowsOnTrustChainBagFactoryError(): void $this->sut()->for('l', ['t']); } + public function testTrustChainResolveThrowsOnValidationStartError(): void { $this->expectException(TrustChainException::class); diff --git a/tests/src/Federation/TrustChainTest.php b/tests/src/Federation/TrustChainTest.php index 1908527..4fee77c 100644 --- a/tests/src/Federation/TrustChainTest.php +++ b/tests/src/Federation/TrustChainTest.php @@ -36,6 +36,7 @@ final class TrustChainTest extends TestCase protected int $expirationTime; + protected function setUp(): void { $this->timestampValidationLeewayDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -57,6 +58,7 @@ protected function setUp(): void $this->trustAnchorMock->method('getExpirationTime')->willReturn($this->expirationTime); } + protected function sut( ?DateIntervalDecorator $timestampValidationLeewayDecorator = null, ?MetadataPolicyResolver $metadataPolicyResolver = null, @@ -76,17 +78,20 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustChain::class, $this->sut()); } + public function testCanCheckIfEmpty(): void { $this->assertTrue($this->sut()->isEmpty()); $this->assertEmpty($this->sut()->getEntities()); } + public function testCanCreateBasicTrustChain(): void { $sut = $this->sut(); @@ -105,6 +110,7 @@ public function testCanCreateBasicTrustChain(): void $this->assertNull($sut->getResolvedMetadata(EntityTypesEnum::OpenIdRelyingParty)); } + public function testCanCreateTrustChainForTrustAnchorOnly(): void { $sut = $this->sut(); @@ -121,6 +127,7 @@ public function testCanCreateTrustChainForTrustAnchorOnly(): void $this->assertNull($sut->getResolvedMetadata(EntityTypesEnum::OpenIdRelyingParty)); } + public function testThrowsForNonConfigurationStatementForLeaf(): void { $this->expectException(EntityStatementException::class); @@ -129,6 +136,7 @@ public function testThrowsForNonConfigurationStatementForLeaf(): void $this->sut()->addLeaf($this->subordinateMock); } + public function testThrowsForConfigurationStatementForSubordinate(): void { $this->expectException(EntityStatementException::class); @@ -139,6 +147,7 @@ public function testThrowsForConfigurationStatementForSubordinate(): void $sut->addSubordinate($this->leafMock); } + public function testThrowsForInvalidSubordinateSubject(): void { $this->expectException(EntityStatementException::class); @@ -151,12 +160,14 @@ public function testThrowsForInvalidSubordinateSubject(): void $sut->addSubordinate($this->subordinateMock); } + public function testCanValidateExpirationTimeOnEmptyTrustChain(): void { $this->sut()->validateExpirationTime(); $this->addToAssertionCount(1); } + public function testThrowsForInvalidExpirationTime(): void { $leafMock = $this->createMock(EntityStatement::class); @@ -170,6 +181,7 @@ public function testThrowsForInvalidExpirationTime(): void $sut->addLeaf($leafMock); } + public function testThrowsForNonResolvedState(): void { $this->expectException(TrustChainException::class); @@ -178,6 +190,7 @@ public function testThrowsForNonResolvedState(): void $this->sut()->getResolvedLength(); } + public function testThrowsForResolvedState(): void { $sut = $this->sut(); @@ -191,6 +204,7 @@ public function testThrowsForResolvedState(): void $sut->addTrustAnchor($this->trustAnchorMock); } + public function testCanGetResolvedMetadata(): void { $leafMetadata = [ @@ -253,6 +267,7 @@ public function testCanGetResolvedMetadata(): void $this->assertIsArray($sut->getResolvedMetadata(EntityTypesEnum::OpenIdRelyingParty)); } + public function testCanGetResolvedMetadataIfNoPoliciesAreDefined(): void { $leafMetadata = [ @@ -283,6 +298,7 @@ public function testCanGetResolvedMetadataIfNoPoliciesAreDefined(): void $this->assertIsArray($sut->getResolvedMetadata(EntityTypesEnum::OpenIdRelyingParty)); } + public function testThrowsOnAttemptToAddMultipleLeafs(): void { $this->expectException(TrustChainException::class); @@ -293,6 +309,7 @@ public function testThrowsOnAttemptToAddMultipleLeafs(): void $sut->addLeaf($this->leafMock); } + public function testThrowsOnAttemtpToAddSubodrinateWithoutLeaf(): void { $this->expectException(TrustChainException::class); @@ -302,6 +319,7 @@ public function testThrowsOnAttemtpToAddSubodrinateWithoutLeaf(): void $sut->addSubordinate($this->subordinateMock); } + public function testThrowsOnAttemptToAddTrustAnchorWithoutSubordinate(): void { $this->expectException(TrustChainException::class); diff --git a/tests/src/Federation/TrustMarkDelegationTest.php b/tests/src/Federation/TrustMarkDelegationTest.php index 1f3eabc..9a21b8a 100644 --- a/tests/src/Federation/TrustMarkDelegationTest.php +++ b/tests/src/Federation/TrustMarkDelegationTest.php @@ -26,7 +26,6 @@ final class TrustMarkDelegationTest extends TestCase { protected MockObject $signatureMock; - protected MockObject $jwsDecoratorMock; protected MockObject $jwsVerifierDecoratorMock; @@ -41,7 +40,6 @@ final class TrustMarkDelegationTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected array $expiredPayload = [ @@ -63,6 +61,7 @@ final class TrustMarkDelegationTest extends TestCase protected array $validPayload; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -95,6 +94,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -123,6 +123,7 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); @@ -134,6 +135,7 @@ public function testCanCreateInstance(): void ); } + public function testReferenceIsOptional(): void { $validPayload = $this->validPayload; diff --git a/tests/src/Federation/TrustMarkFetcherTest.php b/tests/src/Federation/TrustMarkFetcherTest.php index 11f847b..523e9ab 100644 --- a/tests/src/Federation/TrustMarkFetcherTest.php +++ b/tests/src/Federation/TrustMarkFetcherTest.php @@ -43,6 +43,7 @@ final class TrustMarkFetcherTest extends TestCase protected MockObject $entityStatementMock; + protected function setUp(): void { $this->trustMarkFactoryMock = $this->createMock(TrustMarkFactory::class); @@ -57,6 +58,7 @@ protected function setUp(): void $this->entityStatementMock = $this->createMock(EntityStatement::class); } + protected function sut( ?TrustMarkFactory $trustMarkFactoryMock = null, ?ArtifactFetcher $artifactFetcher = null, @@ -79,11 +81,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustMarkFetcher::class, $this->sut()); } + public function testHasRightExpectedContentTypeHttpHeader(): void { $this->assertSame( @@ -92,6 +96,7 @@ public function testHasRightExpectedContentTypeHttpHeader(): void ); } + public function testCanFetchFromCacheOrTrustMarkEndpointWhenCached(): void { $this->entityStatementMock->expects($this->once()) @@ -111,6 +116,7 @@ public function testCanFetchFromCacheOrTrustMarkEndpointWhenCached(): void ); } + public function testCanFetchFromCacheOrTrustMarkEndpointWhenNotCached(): void { $this->entityStatementMock->expects($this->once()) @@ -133,6 +139,7 @@ public function testCanFetchFromCacheOrTrustMarkEndpointWhenNotCached(): void ); } + public function testFetchFromCacheOrTrustMarkEndpointThrowsIfNoFetchEndpoint(): void { $this->entityStatementMock->expects($this->once()) @@ -149,6 +156,7 @@ public function testFetchFromCacheOrTrustMarkEndpointThrowsIfNoFetchEndpoint(): ); } + public function testCanFetchFromCache(): void { $this->artifactFetcherMock->expects($this->once())->method('fromCacheAsString') diff --git a/tests/src/Federation/TrustMarkTest.php b/tests/src/Federation/TrustMarkTest.php index 22da0ec..34968d4 100644 --- a/tests/src/Federation/TrustMarkTest.php +++ b/tests/src/Federation/TrustMarkTest.php @@ -28,7 +28,6 @@ final class TrustMarkTest extends TestCase { protected MockObject $signatureMock; - protected MockObject $jwsDecoratorMock; protected MockObject $jwsVerifierDecoratorMock; @@ -43,7 +42,6 @@ final class TrustMarkTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected JwtTypesEnum $expectedJwtType; @@ -66,6 +64,7 @@ final class TrustMarkTest extends TestCase protected array $validPayload; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -100,6 +99,7 @@ protected function setUp(): void $this->expectedJwtType = JwtTypesEnum::TrustMarkJwt; } + protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -131,6 +131,7 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); @@ -142,6 +143,7 @@ public function testCanCreateInstance(): void ); } + public function testThrowsOnUnexpectedJwtType(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); diff --git a/tests/src/Federation/TrustMarkValidatorTest.php b/tests/src/Federation/TrustMarkValidatorTest.php index 37fbba1..3d76e00 100644 --- a/tests/src/Federation/TrustMarkValidatorTest.php +++ b/tests/src/Federation/TrustMarkValidatorTest.php @@ -54,6 +54,7 @@ final class TrustMarkValidatorTest extends TestCase protected MockObject $trustMarkDelegationMock; + protected function setUp(): void { $this->trustChainResolverMock = $this->createMock(TrustChainResolver::class); @@ -79,6 +80,7 @@ protected function setUp(): void $this->trustMarkDelegationMock = $this->createMock(TrustMarkDelegation::class); } + protected function sut( ?TrustChainResolver $trustChainResolver = null, ?TrustMarkFactory $trustMarkFactory = null, @@ -104,11 +106,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(TrustMarkValidator::class, $this->sut()); } + public function testCanGetIsValidationCachedFor(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -129,6 +133,7 @@ public function testCanGetIsValidationCachedFor(): void ); } + public function testIsValidationCachedForReturnsFalseIfNotCached(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -149,6 +154,7 @@ public function testIsValidationCachedForReturnsFalseIfNotCached(): void ); } + public function testIsValidationCachedForReturnsFalseIfNoCacheInstance(): void { $sut = new TrustMarkValidator( @@ -167,6 +173,7 @@ public function testIsValidationCachedForReturnsFalseIfNoCacheInstance(): void ); } + public function testFromCacheOrDoForTrustMarkTypeChecksCache(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -186,6 +193,7 @@ public function testFromCacheOrDoForTrustMarkTypeChecksCache(): void ); } + public function testFromCacheOrDoForTrustMarkTypeRuns(): void { $this->cacheDecoratorMock->expects($this->once())->method('get'); @@ -211,6 +219,7 @@ public function testFromCacheOrDoForTrustMarkTypeRuns(): void ); } + public function testDoForTrustMarkTypeThrowsIfNoTrustMarks(): void { $this->cacheDecoratorMock->expects($this->never())->method('get'); @@ -227,6 +236,7 @@ public function testDoForTrustMarkTypeThrowsIfNoTrustMarks(): void ); } + public function testDoForTrustMarkTypeThrowsIfNoTrustMarkWithGivenId(): void { $this->cacheDecoratorMock->expects($this->never())->method('get'); @@ -247,6 +257,7 @@ public function testDoForTrustMarkTypeThrowsIfNoTrustMarkWithGivenId(): void ); } + public function testDoForTrustMarkTypeThrowsForInvalidClaimValue(): void { $this->cacheDecoratorMock->expects($this->never())->method('get'); @@ -268,6 +279,7 @@ public function testDoForTrustMarkTypeThrowsForInvalidClaimValue(): void ); } + public function testFromCacheOrDoForTrustMarksClaimValueChecksCache(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -287,6 +299,7 @@ public function testFromCacheOrDoForTrustMarksClaimValueChecksCache(): void ); } + public function testFromCacheOrDoForTrustMarksClaimValueRuns(): void { $this->cacheDecoratorMock->expects($this->once())->method('get'); @@ -307,6 +320,7 @@ public function testFromCacheOrDoForTrustMarksClaimValueRuns(): void ); } + public function testValidateTrustMarksClaimValueThrowsForDifferentPayloadValues(): void { $this->trustMarksClaimValueMock->method('getTrustMarkType')->willReturn('trustMarkType'); @@ -326,6 +340,7 @@ public function testValidateTrustMarksClaimValueThrowsForDifferentPayloadValues( ); } + public function testFromCacheOrDoForTrustMarkChecksCache(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -346,6 +361,7 @@ public function testFromCacheOrDoForTrustMarkChecksCache(): void ); } + public function testFromCacheOrDoForTrustMarkRuns(): void { $this->cacheDecoratorMock->expects($this->once())->method('get'); @@ -363,6 +379,7 @@ public function testFromCacheOrDoForTrustMarkRuns(): void ); } + public function testDoForTrustMarkTakesIntoAccountTrustMarkExpirationForCacheTtl(): void { $this->cacheDecoratorMock->expects($this->never())->method('get'); @@ -388,6 +405,7 @@ public function testDoForTrustMarkTakesIntoAccountTrustMarkExpirationForCacheTtl ); } + public function testDoForTrustMarksLogsCacheError(): void { $this->cacheDecoratorMock->expects($this->never())->method('get'); @@ -426,6 +444,7 @@ public function testDoForTrustMarkCanHandleTrustAnchorAsTrustMarkIssuer(): void ); } + public function testValidateSubjectClaimThrowsForInvalidSubject(): void { $this->trustMarkMock->method('getSubject')->willReturn('invalidSubject'); @@ -439,6 +458,7 @@ public function testValidateSubjectClaimThrowsForInvalidSubject(): void ); } + public function testValidateTrustChainForTrustMarkIssuerThrowsForInvalidChain(): void { $this->trustMarkMock->method('getIssuer') @@ -456,6 +476,7 @@ public function testValidateTrustChainForTrustMarkIssuerThrowsForInvalidChain(): ); } + public function testValidateTrustMarkSignatureThrowsForInvalidSignature(): void { $this->trustMarkMock->expects($this->once()) @@ -473,6 +494,7 @@ public function testValidateTrustMarkSignatureThrowsForInvalidSignature(): void ); } + public function testCanValidateTrustMarkDelegation(): void { $this->trustAnchorConfigurationMock->expects($this->once()) @@ -507,6 +529,7 @@ public function testCanValidateTrustMarkDelegation(): void ); } + public function testValidateTrustMarkDelegationSkipsIfTrustMarkOwnerNotDefinedOnTrustAnchor(): void { $this->trustAnchorConfigurationMock->expects($this->once()) @@ -534,6 +557,7 @@ public function testValidateTrustMarkDelegationSkipsIfTrustMarkOwnerNotDefinedOn $this->assertTrue($debugMessageContainedSkipped); } + public function testValidateTrustMarkDelegationThrowsForMissingDelegationClaim(): void { $this->trustAnchorConfigurationMock->expects($this->once()) @@ -556,6 +580,7 @@ public function testValidateTrustMarkDelegationThrowsForMissingDelegationClaim() ); } + public function testValidateTrustMarkDelegationThrowsForInvalidSignature(): void { $this->trustAnchorConfigurationMock->expects($this->once()) @@ -586,6 +611,7 @@ public function testValidateTrustMarkDelegationThrowsForInvalidSignature(): void ); } + public function testValidateTrustMarkDelegationThrowsForInvalidDelegationIssuer(): void { $this->trustAnchorConfigurationMock->expects($this->once()) @@ -618,6 +644,7 @@ public function testValidateTrustMarkDelegationThrowsForInvalidDelegationIssuer( ); } + public function testValidateTrustMarkDelegationThrowsForInvalidTrustMarkIssuer(): void { $this->trustAnchorConfigurationMock->expects($this->once()) @@ -653,6 +680,7 @@ public function testValidateTrustMarkDelegationThrowsForInvalidTrustMarkIssuer() ); } + public function testValidateTrustMarkDelegationThrowsForInvalidTrustMarkType(): void { $this->trustAnchorConfigurationMock->expects($this->once()) diff --git a/tests/src/FederationTest.php b/tests/src/FederationTest.php index c0dd41e..badc66e 100644 --- a/tests/src/FederationTest.php +++ b/tests/src/FederationTest.php @@ -95,6 +95,7 @@ final class FederationTest extends TestCase protected MockObject $clientMock; + protected function setUp(): void { $this->supportedAlgorithmsMock = $this->createMock(SupportedAlgorithms::class); @@ -107,6 +108,7 @@ protected function setUp(): void $this->clientMock = $this->createMock(Client::class); } + protected function sut( ?SupportedAlgorithms $supportedAlgorithms = null, ?SupportedSerializers $supportedSerializers = null, @@ -138,11 +140,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(Federation::class, $this->sut()); } + public function testCanBuildTools(): void { $sut = $this->sut(); diff --git a/tests/src/Help.php b/tests/src/Help.php index d6e90c0..08d6949 100644 --- a/tests/src/Help.php +++ b/tests/src/Help.php @@ -8,6 +8,7 @@ class Help { public const DIR_DATA = 'data'; + public function getTestRootDir(string ...$pathElements): string { $testRootDir = dirname(__DIR__); @@ -16,6 +17,7 @@ public function getTestRootDir(string ...$pathElements): string $testRootDir . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $pathElements); } + public function getTestDataDir(string ...$pathElements): string { return $this->getTestRootDir(self::DIR_DATA, ...$pathElements); diff --git a/tests/src/Helpers/ArrTest.php b/tests/src/Helpers/ArrTest.php index 53d3b7d..9d29882 100644 --- a/tests/src/Helpers/ArrTest.php +++ b/tests/src/Helpers/ArrTest.php @@ -17,6 +17,7 @@ protected function sut(): Arr return new Arr(); } + public function testCanEnsureArrayDepth(): void { $arr = []; @@ -28,6 +29,7 @@ public function testCanEnsureArrayDepth(): void $this->assertIsArray($arr[1][2]); } + public function testThrowsIfTooDeepArrayDepth(): void { $this->expectException(OpenIDException::class); @@ -37,6 +39,7 @@ public function testThrowsIfTooDeepArrayDepth(): void $this->sut()->ensureArrayDepth($arr, ...range(0, 100)); } + public function testCanGetNestedValue(): void { $arr = ['a' => ['b' => ['c' => 'd']]]; @@ -72,6 +75,7 @@ public function testCanGetNestedValue(): void ); } + public function testGetNestedValueThrowsIfTooDeep(): void { $this->expectException(OpenIDException::class); @@ -81,6 +85,7 @@ public function testGetNestedValueThrowsIfTooDeep(): void $this->sut()->getNestedValue($arr, ...range(0, 100)); } + public function testCanGetNestedValueReference(): void { $arr = []; @@ -93,6 +98,7 @@ public function testCanGetNestedValueReference(): void $this->assertSame('c', $reference); } + public function testGetNestedValueThrowsForNonArrayPathElements(): void { $this->expectException(OpenIDException::class); @@ -102,6 +108,7 @@ public function testGetNestedValueThrowsForNonArrayPathElements(): void $this->sut()->getNestedValueReference($arr, 'a', 'b', 'c'); } + public function testCanSetNestedValue(): void { $arr = []; @@ -117,6 +124,7 @@ public function testCanSetNestedValue(): void $this->assertSame(['a' => ['b' => 'c']], $arr); } + public function testCanAddNestedValue(): void { $arr = []; @@ -144,6 +152,7 @@ public function testCanAddNestedValue(): void $this->assertSame(['a' => ['b', 'b' => [['c']]]], $arr); } + public function testAddNestedValueThrowsForNonArrayPathElements(): void { $this->expectException(OpenIDException::class); diff --git a/tests/src/Helpers/JsonTest.php b/tests/src/Helpers/JsonTest.php index 02bec4d..2a151b0 100644 --- a/tests/src/Helpers/JsonTest.php +++ b/tests/src/Helpers/JsonTest.php @@ -16,6 +16,7 @@ protected function sut(): Json return new Json(); } + public function testEncodeDecode(): void { $arr = ['a' => 'b']; diff --git a/tests/src/Helpers/TypeTest.php b/tests/src/Helpers/TypeTest.php index a2caa29..4a88e15 100644 --- a/tests/src/Helpers/TypeTest.php +++ b/tests/src/Helpers/TypeTest.php @@ -19,6 +19,7 @@ protected function sut(): Type return new Type(); } + public function testCanEnsureString(): void { $this->assertSame('a', $this->sut()->ensureString('a')); @@ -28,6 +29,7 @@ public function testCanEnsureString(): void $this->assertSame('', $this->sut()->ensureString(false)); } + public function testEnsureStringThrowsForNonScalar(): void { $this->expectException(InvalidValueException::class); @@ -36,6 +38,7 @@ public function testEnsureStringThrowsForNonScalar(): void $this->sut()->ensureString(null); } + public function testEnsureStringThrowsForNonStringableObject(): void { $this->expectException(InvalidValueException::class); @@ -44,12 +47,14 @@ public function testEnsureStringThrowsForNonStringableObject(): void $this->sut()->ensureString(new \stdClass()); } + public function testCanEnsureNonEmptyString(): void { $this->assertSame('a', $this->sut()->ensureNonEmptyString('a')); $this->assertSame('1', $this->sut()->ensureNonEmptyString(true)); } + public function testEnsureNonEmptyStringThrowsForEmptyString(): void { $this->expectException(InvalidValueException::class); @@ -58,6 +63,7 @@ public function testEnsureNonEmptyStringThrowsForEmptyString(): void $this->sut()->ensureNonEmptyString(''); } + public function testEnsureNonEmptyStringThrowsForNull(): void { $this->expectException(InvalidValueException::class); @@ -66,6 +72,7 @@ public function testEnsureNonEmptyStringThrowsForNull(): void $this->sut()->ensureNonEmptyString(null); } + public function testEnsureNonEmptyStringThrowsForFalse(): void { $this->expectException(InvalidValueException::class); @@ -74,6 +81,7 @@ public function testEnsureNonEmptyStringThrowsForFalse(): void $this->sut()->ensureNonEmptyString(false); } + public function testCanEnsureArray(): void { $this->assertSame( @@ -114,6 +122,7 @@ public function jsonSerialize(): mixed ); } + public function testEnsureArrayThrowsForUnsafeCasting(): void { $this->expectException(InvalidValueException::class); @@ -122,6 +131,7 @@ public function testEnsureArrayThrowsForUnsafeCasting(): void $this->sut()->ensureArray(null); } + public function testCanEnsureArrayWithKeysAsStrings(): void { $this->assertSame( @@ -147,6 +157,7 @@ public function testCanEnsureArrayWithKeysAsStrings(): void ); } + public function testCanEnsureArrayWithKeysAsNonEmptyStrings(): void { $this->assertSame( @@ -172,6 +183,7 @@ public function testCanEnsureArrayWithKeysAsNonEmptyStrings(): void ); } + public function testCanEnsureArrayWithValuesAsStrings(): void { $this->assertSame( @@ -180,6 +192,7 @@ public function testCanEnsureArrayWithValuesAsStrings(): void ); } + public function testCanEnsureArrayWithValuesAsNonEmptyStrings(): void { $this->assertSame( @@ -188,6 +201,7 @@ public function testCanEnsureArrayWithValuesAsNonEmptyStrings(): void ); } + public function testCanEnsureArrayWithKeysAndValuesAsStrings(): void { $this->assertSame( @@ -196,6 +210,7 @@ public function testCanEnsureArrayWithKeysAndValuesAsStrings(): void ); } + public function testCanEnsureArrayWithKeysAndValuesAsNonEmptyStrings(): void { $this->assertSame( @@ -213,12 +228,14 @@ public function testCanEnsureArrayWithKeysAndValuesAsNonEmptyStrings(): void ); } + public function testCanEnsureInt(): void { $this->assertSame(1, $this->sut()->ensureInt(1)); $this->assertSame(1, $this->sut()->ensureInt('1')); } + public function testEnsureIntThrowsForNonNull(): void { $this->expectException(InvalidValueException::class); diff --git a/tests/src/Helpers/UrlTest.php b/tests/src/Helpers/UrlTest.php index 8478967..cc02f35 100644 --- a/tests/src/Helpers/UrlTest.php +++ b/tests/src/Helpers/UrlTest.php @@ -16,12 +16,14 @@ protected function sut(): Url return new Url(); } + public function testCanCheckUrl(): void { $this->assertTrue($this->sut()->isValid('https://example.com/')); $this->assertFalse($this->sut()->isValid('abc123')); } + public function testCanAddParams(): void { $url = 'https://example.com/'; diff --git a/tests/src/HelpersTest.php b/tests/src/HelpersTest.php index 7dad045..38cad5d 100644 --- a/tests/src/HelpersTest.php +++ b/tests/src/HelpersTest.php @@ -25,11 +25,13 @@ protected function sut(): Helpers return new Helpers(); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(Helpers::class, $this->sut()); } + public function testCanBuildTools(): void { $sut = $this->sut(); diff --git a/tests/src/Jwks/Factories/JwksDecoratorFactoryTest.php b/tests/src/Jwks/Factories/JwksDecoratorFactoryTest.php index aec23ff..8fc0e19 100644 --- a/tests/src/Jwks/Factories/JwksDecoratorFactoryTest.php +++ b/tests/src/Jwks/Factories/JwksDecoratorFactoryTest.php @@ -30,20 +30,24 @@ final class JwksDecoratorFactoryTest extends TestCase ], ]; + protected function setUp(): void { } + protected function sut(): JwksDecoratorFactory { return new JwksDecoratorFactory(); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwksDecoratorFactory::class, $this->sut()); } + public function testCanBuildFromKeyData(): void { $this->assertInstanceOf(Jwks\JwksDecorator::class, $this->sut()->fromKeySetData($this->jwksArraySample)); diff --git a/tests/src/Jwks/Factories/SignedJwksFactoryTest.php b/tests/src/Jwks/Factories/SignedJwksFactoryTest.php index 33deed0..ddd6425 100644 --- a/tests/src/Jwks/Factories/SignedJwksFactoryTest.php +++ b/tests/src/Jwks/Factories/SignedJwksFactoryTest.php @@ -31,8 +31,6 @@ final class SignedJwksFactoryTest extends TestCase { protected MockObject $signatureMock; - - protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; @@ -47,7 +45,6 @@ final class SignedJwksFactoryTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected array $sampleHeader = [ @@ -77,6 +74,7 @@ final class SignedJwksFactoryTest extends TestCase protected array $validPayload; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -112,6 +110,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -140,11 +139,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(SignedJwksFactory::class, $this->sut()); } + public function testCanBuildFromToken(): void { $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); diff --git a/tests/src/Jwks/JwksDecoratorTest.php b/tests/src/Jwks/JwksDecoratorTest.php index 122b793..19c4f55 100644 --- a/tests/src/Jwks/JwksDecoratorTest.php +++ b/tests/src/Jwks/JwksDecoratorTest.php @@ -16,7 +16,6 @@ final class JwksDecoratorTest extends TestCase { protected MockObject $jwkSetMock; - protected array $jwkArraySample = [ 'alg' => 'RS256', 'use' => 'sig', @@ -27,6 +26,7 @@ final class JwksDecoratorTest extends TestCase 'kid' => 'F4VFObNusj3PHmrHxpqh4GNiuFHlfh-2s6xMJ95fLYA', ]; + protected function setUp(): void { $jwkMock = $this->createMock(JWK::class); @@ -35,6 +35,7 @@ protected function setUp(): void $this->jwkSetMock->method('all')->willReturn([$jwkMock]); } + protected function sut( ?JWKSet $jwkSet = null, ): JwksDecorator { @@ -45,16 +46,19 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwksDecorator::class, $this->sut()); } + public function testCanGetJwkSet(): void { $this->assertSame($this->jwkSetMock, $this->sut()->jwks()); } + public function testCanJsonSerialize(): void { $serialized = $this->sut()->jsonSerialize(); diff --git a/tests/src/Jwks/JwksFetcherTest.php b/tests/src/Jwks/JwksFetcherTest.php index fa73b1a..a2f3e76 100644 --- a/tests/src/Jwks/JwksFetcherTest.php +++ b/tests/src/Jwks/JwksFetcherTest.php @@ -46,7 +46,6 @@ final class JwksFetcherTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $responseMock; protected MockObject $responseBodyMock; @@ -69,6 +68,7 @@ final class JwksFetcherTest extends TestCase ], ]; + protected function setUp(): void { $this->httpClientDecoratorMock = $this->createMock(HttpClientDecorator::class); @@ -95,6 +95,7 @@ protected function setUp(): void $this->jwksClaimMock = $this->createMock(JwksClaim::class); } + protected function sut( ?HttpClientDecorator $httpClientDecorator = null, ?JwksDecoratorFactory $jwksDecoratorFactory = null, @@ -126,11 +127,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwksFetcher::class, $this->sut()); } + public function testCanGetFromCache(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -155,6 +158,7 @@ public function testCanGetFromCache(): void $this->assertInstanceOf(JwksDecorator::class, $this->sut()->fromCache('uri')); } + public function testLogsErrorInCaseOfCacheError(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -167,6 +171,7 @@ public function testLogsErrorInCaseOfCacheError(): void $this->assertNotInstanceOf(\SimpleSAML\OpenID\Jwks\JwksDecorator::class, $this->sut()->fromCache('uri')); } + public function testReturnsNullInCaseOfNonStringValueInCache(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -176,6 +181,7 @@ public function testReturnsNullInCaseOfNonStringValueInCache(): void $this->assertNotInstanceOf(\SimpleSAML\OpenID\Jwks\JwksDecorator::class, $this->sut()->fromCache('uri')); } + public function testLogsErrorInCaseOfCacheValueDecodeError(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -192,6 +198,7 @@ public function testLogsErrorInCaseOfCacheValueDecodeError(): void $this->assertNotInstanceOf(\SimpleSAML\OpenID\Jwks\JwksDecorator::class, $this->sut()->fromCache('uri')); } + public function testCanGetFromJwksUri(): void { $this->httpClientDecoratorMock->expects($this->once())->method('request') @@ -222,6 +229,7 @@ public function testCanGetFromJwksUri(): void $this->sut()->fromJwksUri('uri'); } + public function testJwksUriReturnsNullOnHttpError(): void { $this->httpClientDecoratorMock->expects($this->once())->method('request') @@ -234,6 +242,7 @@ public function testJwksUriReturnsNullOnHttpError(): void $this->assertNotInstanceOf(\SimpleSAML\OpenID\Jwks\JwksDecorator::class, $this->sut()->fromJwksUri('uri')); } + public function testJwksUriReturnsNullOnJsonDecodeError(): void { $this->httpClientDecoratorMock->expects($this->once())->method('request') @@ -253,6 +262,7 @@ public function testJwksUriReturnsNullOnJsonDecodeError(): void $this->assertNotInstanceOf(\SimpleSAML\OpenID\Jwks\JwksDecorator::class, $this->sut()->fromJwksUri('uri')); } + public function testJwksUriLogsErrorInCaseOfCacheSetError(): void { $this->httpClientDecoratorMock->expects($this->once())->method('request') @@ -287,6 +297,7 @@ public function testJwksUriLogsErrorInCaseOfCacheSetError(): void $this->sut()->fromJwksUri('uri'); } + public function testCanGetFromCacheOrJwksUri(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -320,6 +331,7 @@ public function testCanGetFromCacheOrJwksUri(): void $this->assertInstanceOf(JwksDecorator::class, $this->sut()->fromCacheOrJwksUri('uri')); } + public function testCanGetFromSignedJwksUri(): void { $this->httpClientDecoratorMock->expects($this->once())->method('request') @@ -349,6 +361,7 @@ public function testCanGetFromSignedJwksUri(): void $this->sut()->fromSignedJwksUri('uri', ['not-important-for-sut']); } + public function testSignedJwksUriTakesExpClaimIntoAccountForCaching(): void { $this->httpClientDecoratorMock->expects($this->once())->method('request') @@ -386,6 +399,7 @@ public function testSignedJwksUriTakesExpClaimIntoAccountForCaching(): void $this->sut()->fromSignedJwksUri('uri', ['not-important-for-sut']); } + public function testSignedJwksUriReturnsNullOnHttpError(): void { $this->httpClientDecoratorMock->expects($this->once())->method('request') @@ -398,6 +412,7 @@ public function testSignedJwksUriReturnsNullOnHttpError(): void $this->sut()->fromSignedJwksUri('uri', ['not-important-for-sut']); } + public function testSignedJwksUriLogsErrorOnCacheSetError(): void { $this->httpClientDecoratorMock->expects($this->once())->method('request') @@ -430,6 +445,7 @@ public function testSignedJwksUriLogsErrorOnCacheSetError(): void $this->sut()->fromSignedJwksUri('uri', ['not-important-for-sut']); } + public function testCanGetFromCacheOrSignedJwksUri(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') diff --git a/tests/src/Jwks/SignedJwksTest.php b/tests/src/Jwks/SignedJwksTest.php index 073a47d..f4e0cd6 100644 --- a/tests/src/Jwks/SignedJwksTest.php +++ b/tests/src/Jwks/SignedJwksTest.php @@ -26,7 +26,6 @@ final class SignedJwksTest extends TestCase { protected MockObject $signatureMock; - protected MockObject $jwsDecoratorMock; protected MockObject $jwsVerifierDecoratorMock; @@ -41,7 +40,6 @@ final class SignedJwksTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected array $sampleHeader = [ @@ -71,6 +69,7 @@ final class SignedJwksTest extends TestCase protected array $validPayload; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -103,6 +102,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -131,6 +131,7 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); @@ -142,6 +143,7 @@ public function testCanCreateInstance(): void ); } + public function testCanGetJsonSerialize(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); diff --git a/tests/src/JwksTest.php b/tests/src/JwksTest.php index beddec6..5d5e01f 100644 --- a/tests/src/JwksTest.php +++ b/tests/src/JwksTest.php @@ -71,6 +71,7 @@ final class JwksTest extends TestCase protected DateInterval $timestampValidationLeeway; + protected function setUp(): void { $this->supportedAlgorithmsMock = $this->createMock(SupportedAlgorithms::class); @@ -82,6 +83,7 @@ protected function setUp(): void $this->httpClientMock = $this->createMock(Client::class); } + protected function sut( ?SupportedAlgorithms $supportedAlgorithms = null, ?SupportedSerializers $supportedSerializers = null, @@ -110,11 +112,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(Jwks::class, $this->sut()); } + public function testCanBuildTools(): void { $sut = $this->sut(); diff --git a/tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php b/tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php index 28b1e9e..d6ad382 100644 --- a/tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php +++ b/tests/src/Jws/Factories/JwsDecoratorBuilderFactoryTest.php @@ -27,6 +27,7 @@ final class JwsDecoratorBuilderFactoryTest extends TestCase protected MockObject $helpersMock; + protected function setUp(): void { $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); @@ -38,16 +39,19 @@ protected function setUp(): void $this->helpersMock = $this->createMock(Helpers::class); } + protected function sut(): JwsDecoratorBuilderFactory { return new JwsDecoratorBuilderFactory(); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwsDecoratorBuilderFactory::class, $this->sut()); } + public function testCanBuild(): void { $this->assertInstanceOf( diff --git a/tests/src/Jws/Factories/JwsVerifierDecoratorFactoryTest.php b/tests/src/Jws/Factories/JwsVerifierDecoratorFactoryTest.php index ca8a9d7..63be5c2 100644 --- a/tests/src/Jws/Factories/JwsVerifierDecoratorFactoryTest.php +++ b/tests/src/Jws/Factories/JwsVerifierDecoratorFactoryTest.php @@ -19,6 +19,7 @@ final class JwsVerifierDecoratorFactoryTest extends TestCase { protected AlgorithmManagerDecorator $algorithmManagerDecorator; + protected function setUp(): void { // Includes final class, so can't mock. @@ -27,11 +28,13 @@ protected function setUp(): void ); } + protected function sut(): JwsVerifierDecoratorFactory { return new JwsVerifierDecoratorFactory(); } + public function testCanCreateInstance(): void { $this->assertInstanceOf( @@ -40,6 +43,7 @@ public function testCanCreateInstance(): void ); } + public function testCanBuild(): void { $this->assertInstanceOf( diff --git a/tests/src/Jws/Factories/ParsedJwsFactoryTest.php b/tests/src/Jws/Factories/ParsedJwsFactoryTest.php index df2c935..c17c304 100644 --- a/tests/src/Jws/Factories/ParsedJwsFactoryTest.php +++ b/tests/src/Jws/Factories/ParsedJwsFactoryTest.php @@ -36,6 +36,7 @@ final class ParsedJwsFactoryTest extends TestCase protected MockObject $claimFactoryMock; + protected function setUp(): void { $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); @@ -47,6 +48,7 @@ protected function setUp(): void $this->claimFactoryMock = $this->createMock(ClaimFactory::class); } + protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -75,11 +77,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(ParsedJwsFactory::class, $this->sut()); } + public function testCanBuildFromToken(): void { $this->assertInstanceOf( diff --git a/tests/src/Jws/JwsDecoratorBuilderTest.php b/tests/src/Jws/JwsDecoratorBuilderTest.php index 64f5138..44504bf 100644 --- a/tests/src/Jws/JwsDecoratorBuilderTest.php +++ b/tests/src/Jws/JwsDecoratorBuilderTest.php @@ -27,6 +27,7 @@ final class JwsDecoratorBuilderTest extends TestCase protected MockObject $jwsDecoratorMock; + protected function setUp(): void { $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); @@ -35,6 +36,7 @@ protected function setUp(): void $this->jwsDecoratorMock = $this->createMock(JwsDecorator::class); } + protected function sut( ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?JwsBuilder $jwsBuilder = null, @@ -51,11 +53,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwsDecoratorBuilder::class, $this->sut()); } + public function testCanParseToken(): void { $this->jwsSerializerManagerDecoratorMock->expects($this->once())->method('unserialize') @@ -64,6 +68,7 @@ public function testCanParseToken(): void $this->assertInstanceOf(JwsDecorator::class, $this->sut()->fromToken('token')); } + public function testThrowsOnTokenParseError(): void { $this->jwsSerializerManagerDecoratorMock->expects($this->once())->method('unserialize') diff --git a/tests/src/Jws/JwsDecoratorTest.php b/tests/src/Jws/JwsDecoratorTest.php index 4532cb3..8bcb7ea 100644 --- a/tests/src/Jws/JwsDecoratorTest.php +++ b/tests/src/Jws/JwsDecoratorTest.php @@ -14,11 +14,13 @@ final class JwsDecoratorTest extends TestCase { protected \PHPUnit\Framework\MockObject\MockObject $jwsMock; + protected function setUp(): void { $this->jwsMock = $this->createMock(JWS::class); } + protected function sut( ?JWS $jws = null, ): JwsDecorator { @@ -27,11 +29,13 @@ protected function sut( return new JwsDecorator($jws); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwsDecorator::class, $this->sut()); } + public function testCanGetJws(): void { $this->assertInstanceOf(JWS::class, $this->sut()->jws()); diff --git a/tests/src/Jws/JwsFetcherTest.php b/tests/src/Jws/JwsFetcherTest.php index 841b8ce..15f5fd7 100644 --- a/tests/src/Jws/JwsFetcherTest.php +++ b/tests/src/Jws/JwsFetcherTest.php @@ -35,9 +35,9 @@ final class JwsFetcherTest extends TestCase protected MockObject $responseMock; - protected MockObject $parsedJwsMock; + protected function setUp(): void { $this->parsedJwsFactoryMock = $this->createMock(ParsedJwsFactory::class); @@ -54,6 +54,7 @@ protected function setUp(): void $this->parsedJwsMock = $this->createMock(ParsedJws::class); } + protected function sut( ?ParsedJwsFactory $parsedJwsFactory = null, ?ArtifactFetcher $artifactFetcher = null, @@ -76,11 +77,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwsFetcher::class, $this->sut()); } + public function testCanFetchFromCache(): void { $this->artifactFetcherMock->expects($this->once())->method('fromCacheAsString') @@ -93,6 +96,7 @@ public function testCanFetchFromCache(): void $this->assertInstanceOf(ParsedJws::class, $this->sut()->fromCache('uri')); } + public function testCanFetchFromCacheOrNetwork(): void { $this->artifactFetcherMock->expects($this->once())->method('fromCacheAsString') @@ -111,6 +115,7 @@ public function testCanFetchFromCacheOrNetwork(): void ); } + public function testFetchFromNetworkThrowsForInvalidResponseStatusCode(): void { $this->artifactFetcherMock->expects($this->once())->method('fromNetwork') @@ -127,6 +132,7 @@ public function testFetchFromNetworkThrowsForInvalidResponseStatusCode(): void $this->sut()->fromNetwork('uri'); } + public function testChecksForExpectedContentTypeHttpHeader(): void { $sut = new class ( @@ -150,6 +156,7 @@ public function getExpectedContentTypeHttpHeader(): string $sut->fromNetwork('uri'); } + public function testWillUseJwsExpirationTimeWhenConsideringTtlForCaching(): void { $expirationTime = time() + 60; diff --git a/tests/src/Jws/JwsVerifierDecoratorTest.php b/tests/src/Jws/JwsVerifierDecoratorTest.php index cb161a3..f5f2f32 100644 --- a/tests/src/Jws/JwsVerifierDecoratorTest.php +++ b/tests/src/Jws/JwsVerifierDecoratorTest.php @@ -21,6 +21,7 @@ final class JwsVerifierDecoratorTest extends TestCase protected MockObject $jwksDecoratorMock; + protected function setUp(): void { $this->jwsVerifierMock = $this->createMock(JWSVerifier::class); @@ -29,6 +30,7 @@ protected function setUp(): void $this->jwksDecoratorMock = $this->createMock(JwksDecorator::class); } + protected function sut( ?JWSVerifier $jwsVerifier = null, ): JwsVerifierDecorator { @@ -39,16 +41,19 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwsVerifierDecorator::class, $this->sut()); } + public function testCanGetJwsVerifier(): void { $this->assertInstanceOf(JwsVerifier::class, $this->sut()->jwsVerifier()); } + public function testCanVerifyWithKeySet(): void { $this->jwsVerifierMock->expects($this->once())->method('verifyWithKeySet'); diff --git a/tests/src/Jws/ParsedJwsTest.php b/tests/src/Jws/ParsedJwsTest.php index 7e66fe3..1d15919 100644 --- a/tests/src/Jws/ParsedJwsTest.php +++ b/tests/src/Jws/ParsedJwsTest.php @@ -40,7 +40,6 @@ final class ParsedJwsTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; protected array $sampleHeader = [ @@ -102,6 +101,7 @@ final class ParsedJwsTest extends TestCase protected array $validPayload; + protected function setUp(): void { $this->jwsDecoratorMock = $this->createMock(JwsDecorator::class); @@ -132,6 +132,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -160,11 +161,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(ParsedJws::class, $this->sut()); } + public function testCanValidateByCallbacks(): void { $sut = new class ( @@ -181,6 +184,7 @@ protected function validate(): void $this->validateByCallbacks($this->simulateOk(...)); } + protected function simulateOk(): void { } @@ -189,6 +193,7 @@ protected function simulateOk(): void $this->assertInstanceOf(ParsedJws::class, $sut); } + public function testThrowsOnValidateByCallbacksError(): void { $this->expectException(JwsException::class); @@ -209,6 +214,7 @@ protected function validate(): void $this->validateByCallbacks($this->simulateError(...)); } + protected function simulateError(): never { throw new \Exception('Error'); @@ -216,6 +222,7 @@ protected function simulateError(): never }; } + public function testCanGetHeader(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); @@ -223,6 +230,7 @@ public function testCanGetHeader(): void $this->assertSame($this->sampleHeader, $this->sut()->getHeader()); } + public function testCanGetHeaderClaims(): void { $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); @@ -232,6 +240,7 @@ public function testCanGetHeaderClaims(): void $this->assertSame($this->sampleHeader['typ'], $this->sut()->getType()); } + public function testThrowsOnGetHeaderError(): void { $this->jwsMock->method('getSignature')->willThrowException(new \Exception('Error')); @@ -242,6 +251,7 @@ public function testThrowsOnGetHeaderError(): void $this->sut()->getHeader(); } + public function testCanGetPayload(): void { $this->jwsMock->expects($this->once())->method('getPayload')->willReturn('payload-json'); @@ -254,6 +264,7 @@ public function testCanGetPayload(): void $this->assertSame($this->validPayload, $sut->getPayload()); } + public function testCanGetEmptyPayload(): void { $this->jwsMock->expects($this->once())->method('getPayload')->willReturn(''); @@ -262,6 +273,7 @@ public function testCanGetEmptyPayload(): void $this->sut()->getPayload(); } + public function testThrowsOnPayloadDecodingError(): void { $this->jwsMock->expects($this->once())->method('getPayload')->willReturn('payload-json'); @@ -274,6 +286,7 @@ public function testThrowsOnPayloadDecodingError(): void $this->sut()->getPayload(); } + public function testCanGetPayloadClaims(): void { $this->jwsMock->expects($this->once())->method('getPayload')->willReturn('payload-json'); @@ -288,6 +301,7 @@ public function testCanGetPayloadClaims(): void $this->assertSame($this->validPayload['iat'], $sut->getIssuedAt()); } + public function testCanGetEmptyPayloadClaims(): void { @@ -303,6 +317,7 @@ public function testCanGetEmptyPayloadClaims(): void $this->assertNull($sut->getIssuer()); } + public function testCanGetAudienceArrayFromString(): void { $this->jwsMock->expects($this->once())->method('getPayload')->willReturn('payload-json'); @@ -312,6 +327,7 @@ public function testCanGetAudienceArrayFromString(): void $this->assertSame(['sample'], $this->sut()->getAudience()); } + public function testCanGetAudienceArrayFromArray(): void { $this->jwsMock->expects($this->once())->method('getPayload')->willReturn('payload-json'); @@ -321,6 +337,7 @@ public function testCanGetAudienceArrayFromArray(): void $this->assertSame(['sample'], $this->sut()->getAudience()); } + public function testThrowsOnInvalidAudienceValue(): void { $this->jwsMock->expects($this->once())->method('getPayload')->willReturn('payload-json'); @@ -333,6 +350,7 @@ public function testThrowsOnInvalidAudienceValue(): void $this->sut()->getAudience(); } + public function testCanSerializeToToken(): void { $this->jwsSerializerManagerDecoratorMock->expects($this->once())->method('serialize') @@ -345,6 +363,7 @@ public function testCanSerializeToToken(): void $this->assertSame('token', $sut->getToken()); } + public function testCanVerifyWithKeySet(): void { $this->jwsVerifierDecoratorMock->expects($this->once())->method('verifyWithKeySet') @@ -353,6 +372,7 @@ public function testCanVerifyWithKeySet(): void $this->sut()->verifyWithKeySet(['jwks']); } + public function testThrowsOnVerifyWithKeySetError(): void { $this->jwsVerifierDecoratorMock->expects($this->once())->method('verifyWithKeySet') @@ -364,6 +384,7 @@ public function testThrowsOnVerifyWithKeySetError(): void $this->sut()->verifyWithKeySet(['jwks']); } + public function testThrowsIfExpired(): void { $this->jwsMock->expects($this->once())->method('getPayload')->willReturn('payload-json'); @@ -375,6 +396,7 @@ public function testThrowsIfExpired(): void $this->sut()->getExpirationTime(); } + public function testThrowsIfIssuedAtInTheFuture(): void { $this->jwsMock->expects($this->once())->method('getPayload')->willReturn('payload-json'); diff --git a/tests/src/SdJwt/DisclosureTest.php b/tests/src/SdJwt/DisclosureTest.php index 271a46d..c1c7e0f 100644 --- a/tests/src/SdJwt/DisclosureTest.php +++ b/tests/src/SdJwt/DisclosureTest.php @@ -28,6 +28,7 @@ final class DisclosureTest extends TestCase protected HashAlgorithmsEnum $selectiveDisclosureAlgorithm; + protected function setUp(): void { $this->helpers = new Helpers(); @@ -38,6 +39,7 @@ protected function setUp(): void $this->selectiveDisclosureAlgorithm = HashAlgorithmsEnum::SHA_256; } + protected function sut( ?Helpers $helpers = null, ?string $salt = null, @@ -63,11 +65,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(Disclosure::class, $this->sut()); } + public function testThrowsForInvalidName(): void { $this->expectException(SdJwtException::class); @@ -78,6 +82,7 @@ public function testThrowsForInvalidName(): void ); } + public function testThrowsForEmptyNameAndPath(): void { $this->expectException(SdJwtException::class); @@ -89,6 +94,7 @@ public function testThrowsForEmptyNameAndPath(): void ); } + public function testCanGetCommonProperties(): void { $sut = $this->sut(); @@ -99,6 +105,7 @@ public function testCanGetCommonProperties(): void $this->assertSame($this->path, $sut->getPath()); } + public function testHasProperSerialization(): void { $this->assertSame( @@ -112,6 +119,7 @@ public function testHasProperSerialization(): void ); } + public function testHasProperType(): void { $this->assertSame( diff --git a/tests/src/SdJwt/Factories/SdJwtFactoryTest.php b/tests/src/SdJwt/Factories/SdJwtFactoryTest.php index 5c93f38..f2cadea 100644 --- a/tests/src/SdJwt/Factories/SdJwtFactoryTest.php +++ b/tests/src/SdJwt/Factories/SdJwtFactoryTest.php @@ -83,6 +83,7 @@ final class SdJwtFactoryTest extends TestCase protected array $validPayload; + protected function setUp(): void { $signatureMock = $this->createMock(Signature::class); @@ -123,6 +124,7 @@ protected function setUp(): void $this->disclosureFactoryMock = $this->createMock(DisclosureFactory::class); } + protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -154,11 +156,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(SdJwtFactory::class, $this->sut()); } + public function testCanBuildFromData(): void { $this->assertInstanceOf( @@ -173,6 +177,7 @@ public function testCanBuildFromData(): void ); } + public function testCanUpdatePayloadWithDisclosures(): void { // ["_26bc4LT-ac6q2KI6cBW5es","family_name","Möbius"] diff --git a/tests/src/Serializers/JwsSerializerBagTest.php b/tests/src/Serializers/JwsSerializerBagTest.php index 6be98d1..8948576 100644 --- a/tests/src/Serializers/JwsSerializerBagTest.php +++ b/tests/src/Serializers/JwsSerializerBagTest.php @@ -14,9 +14,10 @@ #[UsesClass(JwsSerializerEnum::class)] final class JwsSerializerBagTest extends TestCase { - /** @var JwsSerializerEnum[] */ + /** @var \SimpleSAML\OpenID\Serializers\JwsSerializerEnum[] */ protected array $jwsSerializers; + protected function setUp(): void { $this->jwsSerializers = [ @@ -24,8 +25,9 @@ protected function setUp(): void ]; } + /** - * @param ?JwsSerializerEnum[] $jwsSerializers + * @param ?\SimpleSAML\OpenID\Serializers\JwsSerializerEnum[] $jwsSerializers */ protected function sut( ?array $jwsSerializers = null, @@ -35,11 +37,13 @@ protected function sut( return new JwsSerializerBag(...$jwsSerializers); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwsSerializerBag::class, $this->sut()); } + public function testCanAddAndGet(): void { $sut = $this->sut(); diff --git a/tests/src/Serializers/JwsSerializerManagerDecoratorTest.php b/tests/src/Serializers/JwsSerializerManagerDecoratorTest.php index e5075dc..3fd7fa5 100644 --- a/tests/src/Serializers/JwsSerializerManagerDecoratorTest.php +++ b/tests/src/Serializers/JwsSerializerManagerDecoratorTest.php @@ -23,6 +23,7 @@ final class JwsSerializerManagerDecoratorTest extends TestCase protected JWSSerializerManager $jwsSerializerManager; + protected function setUp(): void { $this->jwsSerializerMock = $this->createMock(JWSSerializer::class); @@ -36,6 +37,7 @@ protected function setUp(): void ]); } + protected function sut( ?JWSSerializerManager $jwsSerializerManager = null, ): JwsSerializerManagerDecorator { @@ -46,11 +48,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwsSerializerManagerDecorator::class, $this->sut()); } + public function testCanSerialize(): void { $this->jwsSerializerMock->expects($this->once())->method('serialize')->willReturn('token'); @@ -61,6 +65,7 @@ public function testCanSerialize(): void ); } + public function testCanUnserialize(): void { $this->jwsSerializerMock->expects($this->once())->method('unserialize'); diff --git a/tests/src/SupportedAlgorithmsTest.php b/tests/src/SupportedAlgorithmsTest.php index d87f9a3..ab7528e 100644 --- a/tests/src/SupportedAlgorithmsTest.php +++ b/tests/src/SupportedAlgorithmsTest.php @@ -15,11 +15,13 @@ final class SupportedAlgorithmsTest extends TestCase { protected MockObject $signatureAlgorithmBagMock; + protected function setUp(): void { $this->signatureAlgorithmBagMock = $this->createMock(SignatureAlgorithmBag::class); } + protected function sut( ?SignatureAlgorithmBag $signatureAlgorithmBag = null, ): SupportedAlgorithms { @@ -28,11 +30,13 @@ protected function sut( return new SupportedAlgorithms($signatureAlgorithmBag); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(SupportedAlgorithms::class, $this->sut()); } + public function testCanGetSignatureAlgorithmBag(): void { $this->assertInstanceOf(SignatureAlgorithmBag::class, $this->sut()->getSignatureAlgorithmBag()); diff --git a/tests/src/SupportedSerializersTest.php b/tests/src/SupportedSerializersTest.php index 6c73e7b..af3fcdb 100644 --- a/tests/src/SupportedSerializersTest.php +++ b/tests/src/SupportedSerializersTest.php @@ -15,11 +15,13 @@ final class SupportedSerializersTest extends TestCase { protected MockObject $jwsSerializerBagMock; + protected function setUp(): void { $this->jwsSerializerBagMock = $this->createMock(JwsSerializerBag::class); } + protected function sut( ?JwsSerializerBag $jwsSerializerBag = null, ): SupportedSerializers { @@ -28,11 +30,13 @@ protected function sut( return new SupportedSerializers($jwsSerializerBag); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(SupportedSerializers::class, $this->sut()); } + public function testCanGetJwsSerializerBag(): void { $this->assertInstanceOf(JwsSerializerBag::class, $this->sut()->getJwsSerializerBag()); diff --git a/tests/src/Utils/ArtifactFetcherTest.php b/tests/src/Utils/ArtifactFetcherTest.php index 125bd3c..d3a9866 100644 --- a/tests/src/Utils/ArtifactFetcherTest.php +++ b/tests/src/Utils/ArtifactFetcherTest.php @@ -29,6 +29,7 @@ final class ArtifactFetcherTest extends TestCase protected MockObject $responseBodyMock; + protected function setUp(): void { $this->httpClientDecoratorMock = $this->createMock(HttpClientDecorator::class); @@ -40,6 +41,7 @@ protected function setUp(): void $this->responseMock->method('getBody')->willReturn($this->responseBodyMock); } + protected function sut( ?HttpClientDecorator $httpClientDecorator = null, ?CacheDecorator $cacheDecorator = null, @@ -56,11 +58,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(ArtifactFetcher::class, $this->sut()); } + public function testReturnsNullIfCacheNotAvailable(): void { $sut = new ArtifactFetcher($this->httpClientDecoratorMock, null, $this->loggerMock); @@ -71,6 +75,7 @@ public function testReturnsNullIfCacheNotAvailable(): void $this->assertNull($sut->fromCacheAsString('key')); } + public function testReturnsNullIfNotInCache(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -80,6 +85,7 @@ public function testReturnsNullIfNotInCache(): void $this->assertNull($this->sut()->fromCacheAsString('key')); } + public function testReturnsArtifactIfString(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -89,6 +95,7 @@ public function testReturnsArtifactIfString(): void $this->assertSame('artifact', $this->sut()->fromCacheAsString('key')); } + public function testReturnsNullIfNotString(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -101,6 +108,7 @@ public function testReturnsNullIfNotString(): void $this->assertNull($this->sut()->fromCacheAsString('key')); } + public function testReturnsNullOnCacheFailure(): void { $this->cacheDecoratorMock->expects($this->once())->method('get') @@ -113,6 +121,7 @@ public function testReturnsNullOnCacheFailure(): void $this->assertNull($this->sut()->fromCacheAsString('key')); } + public function testCanFetchFromNetwork(): void { $this->httpClientDecoratorMock->expects($this->once())->method('request') @@ -121,6 +130,7 @@ public function testCanFetchFromNetwork(): void $this->assertInstanceOf(ResponseInterface::class, $this->sut()->fromNetwork('uri')); } + public function testFromNetworkThrowsOnNetworkError(): void { $this->httpClientDecoratorMock->expects($this->once())->method('request') @@ -135,6 +145,7 @@ public function testFromNetworkThrowsOnNetworkError(): void $this->sut()->fromNetwork('uri'); } + public function testCanFetchFromNetworkAsString(): void { $this->httpClientDecoratorMock->expects($this->once())->method('request') @@ -144,6 +155,7 @@ public function testCanFetchFromNetworkAsString(): void $this->assertSame('artifact', $this->sut()->fromNetworkAsString('uri')); } + public function testCanCacheArtifact(): void { $this->cacheDecoratorMock->expects($this->once())->method('set') @@ -155,6 +167,7 @@ public function testCanCacheArtifact(): void $this->sut()->cacheIt('artifact', 60, 'key'); } + public function testSkipsCachingIfCacheNotAvailable(): void { $this->loggerMock->expects($this->once())->method('debug') @@ -165,6 +178,7 @@ public function testSkipsCachingIfCacheNotAvailable(): void $sut->cacheIt('artifact', 60, 'key'); } + public function testCanLogCacheError(): void { $this->loggerMock->expects($this->once())->method('error') diff --git a/tests/src/VerifiableCredentials/ClaimsPathPointerResolverTest.php b/tests/src/VerifiableCredentials/ClaimsPathPointerResolverTest.php index 30df2a3..934b2f7 100644 --- a/tests/src/VerifiableCredentials/ClaimsPathPointerResolverTest.php +++ b/tests/src/VerifiableCredentials/ClaimsPathPointerResolverTest.php @@ -39,11 +39,13 @@ final class ClaimsPathPointerResolverTest extends TestCase 'nationalities' => ['British', 'Betelgeusian'], ]; + public function testCanCreateInstance(): void { $this->assertInstanceOf(ClaimsPathPointerResolver::class, $this->sut()); } + protected function sut( ?Helpers $helpers = null, ): ClaimsPathPointerResolver { @@ -52,12 +54,14 @@ protected function sut( return new ClaimsPathPointerResolver($helpers); } + protected function setUp(): void { $this->helpersMock = $this->createMock(Helpers::class); $this->helpersMock->method('arr')->willReturn(new Helpers\Arr()); } + public function testCanResolveForJsonBased(): void { $sut = $this->sut(); @@ -94,6 +98,7 @@ public function testCanResolveForJsonBased(): void ); } + public function testThrowsForInvalidPathComponent(): void { $this->expectException(ClaimsPathPointerException::class); @@ -102,6 +107,7 @@ public function testThrowsForInvalidPathComponent(): void $this->sut()->forJsonBased($this->jsonDataSample, [false]); } + public function testThrowsForNonObjectInStringPathComponent(): void { $this->expectException(ClaimsPathPointerException::class); @@ -110,6 +116,7 @@ public function testThrowsForNonObjectInStringPathComponent(): void $this->sut()->forJsonBased($this->jsonDataSample, ['nationalities', 'invalid']); } + public function testThrowsForNonArrayInNullPathComponent(): void { $this->expectException(ClaimsPathPointerException::class); @@ -118,6 +125,7 @@ public function testThrowsForNonArrayInNullPathComponent(): void $this->sut()->forJsonBased($this->jsonDataSample, ['address', null]); } + public function testThrowsForNonArrayInIntegerPathComponent(): void { $this->expectException(ClaimsPathPointerException::class); @@ -126,6 +134,7 @@ public function testThrowsForNonArrayInIntegerPathComponent(): void $this->sut()->forJsonBased($this->jsonDataSample, ['address', 0]); } + public function testThrowsForEmptySelection(): void { $this->expectException(ClaimsPathPointerException::class); diff --git a/tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php b/tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php index 345ebb0..306d4a1 100644 --- a/tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php +++ b/tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php @@ -45,7 +45,6 @@ final class JwtVcJsonFactoryTest extends TestCase protected MockObject $jsonHelperMock; - protected MockObject $claimFactoryMock; // https://www.w3.org/TR/vc-data-model/#example-jwt-header-of-a-jwt-based-verifiable-credential-using-jws-as-a-proof-non-normative @@ -81,6 +80,7 @@ final class JwtVcJsonFactoryTest extends TestCase protected array $validPayload; + protected function setUp(): void { $this->signatureMock = $this->createMock(Signature::class); @@ -118,6 +118,7 @@ protected function setUp(): void $this->validPayload['exp'] = time() + 3600; } + protected function sut( ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, @@ -146,11 +147,13 @@ protected function sut( ); } + public function testCanCreateInstance(): void { $this->assertInstanceOf(JwtVcJsonFactory::class, $this->sut()); } + public function testCanBuildFromToken(): void { $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); From 2fa09c20813696f5c7ba100644d0cc2fee131531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Thu, 18 Sep 2025 12:22:59 +0200 Subject: [PATCH 46/66] Explicitly add phpstan/phpdoc-parser to require-dev --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 560136d..2fe16f0 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ }, "require-dev": { "phpstan/phpstan": "^2.1", + "phpstan/phpdoc-parser": "^2.3", "phpunit/phpunit": "^10 || ^11", "rector/rector": "^2.0", "simplesamlphp/simplesamlphp-test-framework": "^1", From 3906ce602490cefedbdbdfa47ee6f5da8c1db53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 29 Sep 2025 15:58:37 +0200 Subject: [PATCH 47/66] Revert "Explicitly add phpstan/phpdoc-parser to require-dev" This reverts commit 2fa09c20813696f5c7ba100644d0cc2fee131531. --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 2fe16f0..560136d 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,6 @@ }, "require-dev": { "phpstan/phpstan": "^2.1", - "phpstan/phpdoc-parser": "^2.3", "phpunit/phpunit": "^10 || ^11", "rector/rector": "^2.0", "simplesamlphp/simplesamlphp-test-framework": "^1", From f6c50f3a26ae9c4d171cf98975395dd7ffc0fc81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 29 Sep 2025 16:58:03 +0200 Subject: [PATCH 48/66] WIP tests --- .../Algorithms/SignatureAlgorithmEnumTest.php | 16 +++++++++++ tests/src/Codebooks/GrantTypesEnumTest.php | 21 ++++++++++++++ .../src/Codebooks/HashAlgorithmsEnumTest.php | 28 +++++++++++++++++++ .../Factories/ClientAssertionFactoryTest.php | 23 +++++++++++++++ .../Factories/RequestObjectFactoryTest.php | 23 +++++++++++++++ tests/src/Factories/ClaimFactoryTest.php | 8 ++++++ .../Factories/EntityStatementFactoryTest.php | 24 ++++++++++++++++ .../Factories/RequestObjectFactoryTest.php | 24 ++++++++++++++++ .../TrustMarkDelegationFactoryTest.php | 24 ++++++++++++++++ .../Factories/TrustMarkFactoryTest.php | 24 ++++++++++++++++ 10 files changed, 215 insertions(+) create mode 100644 tests/src/Codebooks/GrantTypesEnumTest.php create mode 100644 tests/src/Codebooks/HashAlgorithmsEnumTest.php diff --git a/tests/src/Algorithms/SignatureAlgorithmEnumTest.php b/tests/src/Algorithms/SignatureAlgorithmEnumTest.php index 9602a13..18ffd6f 100644 --- a/tests/src/Algorithms/SignatureAlgorithmEnumTest.php +++ b/tests/src/Algorithms/SignatureAlgorithmEnumTest.php @@ -71,4 +71,20 @@ public function testCanGetProperSignatureAlgorithmInstance(): void SignatureAlgorithmEnum::RS512->instance(), ); } + + + public function testIsNone(): void + { + $this->assertTrue(SignatureAlgorithmEnum::none->isNone()); + $this->assertFalse(SignatureAlgorithmEnum::EdDSA->isNone()); + $this->assertFalse(SignatureAlgorithmEnum::ES256->isNone()); + $this->assertFalse(SignatureAlgorithmEnum::ES384->isNone()); + $this->assertFalse(SignatureAlgorithmEnum::ES512->isNone()); + $this->assertFalse(SignatureAlgorithmEnum::PS256->isNone()); + $this->assertFalse(SignatureAlgorithmEnum::PS384->isNone()); + $this->assertFalse(SignatureAlgorithmEnum::PS512->isNone()); + $this->assertFalse(SignatureAlgorithmEnum::RS256->isNone()); + $this->assertFalse(SignatureAlgorithmEnum::RS384->isNone()); + $this->assertFalse(SignatureAlgorithmEnum::RS512->isNone()); + } } diff --git a/tests/src/Codebooks/GrantTypesEnumTest.php b/tests/src/Codebooks/GrantTypesEnumTest.php new file mode 100644 index 0000000..a03bc56 --- /dev/null +++ b/tests/src/Codebooks/GrantTypesEnumTest.php @@ -0,0 +1,21 @@ +assertTrue(GrantTypesEnum::PreAuthorizedCode->canBeUsedForVerifiableCredentialIssuance()); + $this->assertTrue(GrantTypesEnum::AuthorizationCode->canBeUsedForVerifiableCredentialIssuance()); + $this->assertFalse(GrantTypesEnum::Implicit->canBeUsedForVerifiableCredentialIssuance()); + $this->assertFalse(GrantTypesEnum::RefreshToken->canBeUsedForVerifiableCredentialIssuance()); + } +} diff --git a/tests/src/Codebooks/HashAlgorithmsEnumTest.php b/tests/src/Codebooks/HashAlgorithmsEnumTest.php new file mode 100644 index 0000000..b91304d --- /dev/null +++ b/tests/src/Codebooks/HashAlgorithmsEnumTest.php @@ -0,0 +1,28 @@ +assertSame(HashAlgorithmsEnum::SHA3_256->ianaName(), HashAlgorithmsEnum::SHA3_256->value); + $this->assertSame(HashAlgorithmsEnum::SHA3_384->ianaName(), HashAlgorithmsEnum::SHA3_384->value); + $this->assertSame(HashAlgorithmsEnum::SHA3_512->ianaName(), HashAlgorithmsEnum::SHA3_512->value); + } + + + public function testPhpName(): void + { + $this->assertSame('sha3-256', HashAlgorithmsEnum::SHA3_256->phpName()); + $this->assertSame('sha3-384', HashAlgorithmsEnum::SHA3_384->phpName()); + $this->assertSame('sha3-512', HashAlgorithmsEnum::SHA3_512->phpName()); + } +} diff --git a/tests/src/Core/Factories/ClientAssertionFactoryTest.php b/tests/src/Core/Factories/ClientAssertionFactoryTest.php index ab9d89c..7e63ed0 100644 --- a/tests/src/Core/Factories/ClientAssertionFactoryTest.php +++ b/tests/src/Core/Factories/ClientAssertionFactoryTest.php @@ -9,11 +9,13 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Core\ClientAssertion; use SimpleSAML\OpenID\Core\Factories\ClientAssertionFactory; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; @@ -60,6 +62,8 @@ final class ClientAssertionFactoryTest extends TestCase protected array $validPayload; + protected MockObject $jwkDecoratorMock; + protected function setUp(): void { @@ -72,6 +76,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock->method('fromData')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); @@ -91,6 +96,8 @@ protected function setUp(): void $this->validPayload = $this->expiredPayload; $this->validPayload['exp'] = time() + 3600; + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); } @@ -138,4 +145,20 @@ public function testCanBuildFromToken(): void $this->sut()->fromToken('token'), ); } + + + public function testCanBuildFromData(): void + { + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $this->assertInstanceOf( + ClientAssertion::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::ES256, + $this->validPayload, + [], + ), + ); + } } diff --git a/tests/src/Core/Factories/RequestObjectFactoryTest.php b/tests/src/Core/Factories/RequestObjectFactoryTest.php index 82390d7..a50eb5d 100644 --- a/tests/src/Core/Factories/RequestObjectFactoryTest.php +++ b/tests/src/Core/Factories/RequestObjectFactoryTest.php @@ -10,11 +10,13 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Core\Factories\RequestObjectFactory; use SimpleSAML\OpenID\Core\RequestObject; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; @@ -50,6 +52,8 @@ final class RequestObjectFactoryTest extends TestCase 'kid' => 'LfgZECDYkSTHmbllBD5_Tkwvy3CtOpNYQ7-DfQawTww', ]; + protected MockObject $jwkDecoratorMock; + protected function setUp(): void { @@ -63,6 +67,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock->method('fromData')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); @@ -74,6 +79,8 @@ protected function setUp(): void $this->helpersMock->method('json')->willReturn($jsonHelperMock); $this->claimFactoryMock = $this->createMock(ClaimFactory::class); + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); } @@ -121,4 +128,20 @@ public function testCanBuildFromToken(): void $this->sut()->fromToken('token'), ); } + + + public function testCanBuildFromData(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + + $this->assertInstanceOf( + RequestObject::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::ES256, + [], + $this->sampleHeader, + ), + ); + } } diff --git a/tests/src/Factories/ClaimFactoryTest.php b/tests/src/Factories/ClaimFactoryTest.php index abeebbd..05e86e9 100644 --- a/tests/src/Factories/ClaimFactoryTest.php +++ b/tests/src/Factories/ClaimFactoryTest.php @@ -13,6 +13,7 @@ use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Federation\Factories\FederationClaimFactory; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Factories\VcDataModelClaimFactory; #[CoversClass(ClaimFactory::class)] #[UsesClass(Helpers::class)] @@ -20,6 +21,7 @@ #[UsesClass(FederationClaimFactory::class)] #[UsesClass(GenericClaim::class)] #[UsesClass(JwksClaim::class)] +#[UsesClass(VcDataModelClaimFactory::class)] final class ClaimFactoryTest extends TestCase { protected Helpers $helpers; @@ -68,6 +70,12 @@ public function testCanGetForFederation(): void } + public function testCanGetForVcDataModel(): void + { + $this->assertInstanceOf(VcDataModelClaimFactory::class, $this->sut()->forVcDataModel()); + } + + public function testCanBuildGeneric(): void { $this->assertInstanceOf( diff --git a/tests/src/Federation/Factories/EntityStatementFactoryTest.php b/tests/src/Federation/Factories/EntityStatementFactoryTest.php index a31387d..94a41a8 100644 --- a/tests/src/Federation/Factories/EntityStatementFactoryTest.php +++ b/tests/src/Federation/Factories/EntityStatementFactoryTest.php @@ -10,11 +10,13 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Federation\EntityStatement; use SimpleSAML\OpenID\Federation\Factories\EntityStatementFactory; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; @@ -99,6 +101,8 @@ final class EntityStatementFactoryTest extends TestCase protected array $validPayload; + protected MockObject $jwkDecoratorMock; + protected function setUp(): void { @@ -114,6 +118,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock->method('fromData')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); @@ -133,6 +138,8 @@ protected function setUp(): void $this->validPayload = $this->expiredPayload; $this->validPayload['exp'] = time() + 3600; + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); } @@ -181,4 +188,21 @@ public function testCanBuildFromToken(): void $this->sut()->fromToken('token'), ); } + + + public function testCanBuildFromData(): void + { + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + + $this->assertInstanceOf( + EntityStatement::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::ES256, + $this->validPayload, + $this->sampleHeader, + ), + ); + } } diff --git a/tests/src/Federation/Factories/RequestObjectFactoryTest.php b/tests/src/Federation/Factories/RequestObjectFactoryTest.php index d5736e3..d8f43ed 100644 --- a/tests/src/Federation/Factories/RequestObjectFactoryTest.php +++ b/tests/src/Federation/Factories/RequestObjectFactoryTest.php @@ -10,11 +10,13 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Federation\Factories\RequestObjectFactory; use SimpleSAML\OpenID\Federation\RequestObject; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; @@ -67,6 +69,8 @@ final class RequestObjectFactoryTest extends TestCase protected array $validPayload; + protected MockObject $jwkDecoratorMock; + protected function setUp(): void { @@ -82,6 +86,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock->method('fromData')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); @@ -100,6 +105,8 @@ protected function setUp(): void $this->validPayload = $this->expiredPayload; $this->validPayload['exp'] = time() + 3600; + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); } @@ -148,4 +155,21 @@ public function testCanBuildFromToken(): void $this->sut()->fromToken('token'), ); } + + + public function testCanBuildFromData(): void + { + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + + $this->assertInstanceOf( + RequestObject::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::ES256, + $this->validPayload, + $this->sampleHeader, + ), + ); + } } diff --git a/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php b/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php index b1584f1..5fb7480 100644 --- a/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php +++ b/tests/src/Federation/Factories/TrustMarkDelegationFactoryTest.php @@ -10,11 +10,13 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Federation\Factories\TrustMarkDelegationFactory; use SimpleSAML\OpenID\Federation\TrustMarkDelegation; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; @@ -62,6 +64,8 @@ final class TrustMarkDelegationFactoryTest extends TestCase protected array $validPayload; + protected MockObject $jwkDecoratorMock; + protected function setUp(): void { @@ -77,6 +81,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock->method('fromData')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); @@ -96,6 +101,8 @@ protected function setUp(): void $this->validPayload = $this->expiredPayload; $this->validPayload['exp'] = time() + 3600; + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); } @@ -144,4 +151,21 @@ public function testCanBuild(): void $this->sut()->fromToken('token'), ); } + + + public function testCanBuildFromData(): void + { + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + + $this->assertInstanceOf( + TrustMarkDelegation::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::ES256, + $this->validPayload, + $this->sampleHeader, + ), + ); + } } diff --git a/tests/src/Federation/Factories/TrustMarkFactoryTest.php b/tests/src/Federation/Factories/TrustMarkFactoryTest.php index ba6efc4..4e93069 100644 --- a/tests/src/Federation/Factories/TrustMarkFactoryTest.php +++ b/tests/src/Federation/Factories/TrustMarkFactoryTest.php @@ -10,11 +10,13 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Federation\Factories\TrustMarkFactory; use SimpleSAML\OpenID\Federation\TrustMark; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; @@ -65,6 +67,8 @@ final class TrustMarkFactoryTest extends TestCase protected array $validPayload; + protected MockObject $jwkDecoratorMock; + protected function setUp(): void { @@ -80,6 +84,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock->method('fromData')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); @@ -99,6 +104,8 @@ protected function setUp(): void $this->validPayload = $this->expiredPayload; $this->validPayload['exp'] = time() + 3600; + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); } @@ -147,4 +154,21 @@ public function testCanBuild(): void $this->sut()->fromToken('token'), ); } + + + public function testCanBuildFromData(): void + { + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + + $this->assertInstanceOf( + TrustMark::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::ES256, + $this->validPayload, + $this->sampleHeader, + ), + ); + } } From 7b521db26ad567baea202024d4ade8f3f1134d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 29 Sep 2025 17:01:35 +0200 Subject: [PATCH 49/66] Use local phpcs --- .github/workflows/php.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index d51c6b2..a893c2f 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -50,7 +50,7 @@ jobs: with: # Should be the higest supported version, so we can use the newest tools php-version: '8.4' - tools: composer, composer-require-checker, composer-unused, phpcs + tools: composer, composer-require-checker, composer-unused extensions: ctype, date, dom, filter, hash, mbstring, openssl, pcre, soap, spl, xml coverage: none @@ -82,7 +82,8 @@ jobs: run: composer-unused - name: PHP Code Sniffer - run: phpcs + run: | + vendor/bin/phpcs -p - name: PHPStan run: | From 5c0f41ef3b485b54e02bfca5983406bfc1d35dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Sat, 25 Oct 2025 21:03:39 +0200 Subject: [PATCH 50/66] Add ext-hash to composer.json --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 560136d..0c44e2d 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "ext-gmp": "*", "ext-filter": "*", "ext-mbstring": "*", + "ext-hash": "*", "guzzlehttp/guzzle": "^7.8", "psr/http-client": "^1", From a56a2340a5b03dfe30c7b5b2a5991ded9245e302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Sat, 25 Oct 2025 21:10:25 +0200 Subject: [PATCH 51/66] Lint --- rector.php | 2 +- src/Federation/Factories/TrustChainFactory.php | 2 +- src/Jws/ParsedJws.php | 2 +- .../VcDataModel/Factories/VcDataModelClaimFactory.php | 8 ++++---- tests/src/Did/DidKeyJwkResolverTest.php | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rector.php b/rector.php index 584c8de..2e0a9db 100644 --- a/rector.php +++ b/rector.php @@ -20,7 +20,7 @@ // naming: true, instanceOf: true, earlyReturn: true, - strictBooleans: true, +// strictBooleans: true, // carbon: true, rectorPreset: true, phpunitCodeQuality: true, diff --git a/src/Federation/Factories/TrustChainFactory.php b/src/Federation/Factories/TrustChainFactory.php index c1238a4..ee26785 100644 --- a/src/Federation/Factories/TrustChainFactory.php +++ b/src/Federation/Factories/TrustChainFactory.php @@ -74,7 +74,7 @@ public function fromStatements(EntityStatement ...$statements): TrustChain public function fromTokens(string ...$tokens): TrustChain { $statements = array_map( - fn(string $token): EntityStatement => $this->entityStatementFactory->fromToken($token), + $this->entityStatementFactory->fromToken(...), $tokens, ); diff --git a/src/Jws/ParsedJws.php b/src/Jws/ParsedJws.php index 2474afd..e3e0cf3 100644 --- a/src/Jws/ParsedJws.php +++ b/src/Jws/ParsedJws.php @@ -134,7 +134,7 @@ public function getPayload(): array } $payloadString = $this->jwsDecorator->jws()->getPayload(); - if ($payloadString === null || $payloadString === '' || $payloadString === '0') { + if (in_array($payloadString, [null, '', '0'], true)) { return $this->payload = []; } diff --git a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php index d7faccf..c7c3f49 100644 --- a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php @@ -255,7 +255,7 @@ public function buildVcCredentialSchemaClaimBag(array $data): VcCredentialSchema $vcCredentialSchemaClaimValue = $this->buildVcCredentialSchemaClaimValue($vcCredentialSchemaClaimValueData); $vcCredentialSchemaClaimValues = array_map( - fn (array $data): VcCredentialSchemaClaimValue => $this->buildVcCredentialSchemaClaimValue($data), + $this->buildVcCredentialSchemaClaimValue(...), $data, ); @@ -305,7 +305,7 @@ public function buildVcRefreshServiceClaimBag(array $data): VcRefreshServiceClai $vcRefreshServiceClaimValue = $this->buildVcRefreshServiceClaimValue($vcRefreshServiceClaimValueData); $vcRefreshServiceClaimValues = array_map( - fn (array $data): VcRefreshServiceClaimValue => $this->buildVcRefreshServiceClaimValue($data), + $this->buildVcRefreshServiceClaimValue(...), $data, ); @@ -351,7 +351,7 @@ public function buildVcTermsOfUseClaimBag(array $data): VcTermsOfUseClaimBag $vcTermsOfUseClaimValue = $this->buildVcTermsOfUseClaimValue($vcTermsOfUseClaimValueData); $vcTermsOfUseClaimValues = array_map( - fn (array $data): VcTermsOfUseClaimValue => $this->buildVcTermsOfUseClaimValue($data), + $this->buildVcTermsOfUseClaimValue(...), $data, ); @@ -397,7 +397,7 @@ public function buildVcEvidenceClaimBag(array $data): VcEvidenceClaimBag $vcEvidenceClaimValue = $this->buildVcEvidenceClaimValue($vcEvidenceClaimValueData); $vcEvidenceClaimValues = array_map( - fn (array $data): VcEvidenceClaimValue => $this->buildVcEvidenceClaimValue($data), + $this->buildVcEvidenceClaimValue(...), $data, ); diff --git a/tests/src/Did/DidKeyJwkResolverTest.php b/tests/src/Did/DidKeyJwkResolverTest.php index f96ddb9..4349b05 100644 --- a/tests/src/Did/DidKeyJwkResolverTest.php +++ b/tests/src/Did/DidKeyJwkResolverTest.php @@ -28,7 +28,7 @@ protected function setUp(): void $base64UrlMock = $this->createMock(Base64Url::class); $base64UrlMock->method('encode') - ->willReturnCallback(fn(string $data): string => base64_encode($data)); + ->willReturnCallback(base64_encode(...)); $this->helpersMock = $this->createMock(Helpers::class); From 4efcbe611602d85a56ed0464644ad13fd9f6e1f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Sat, 25 Oct 2025 22:17:58 +0200 Subject: [PATCH 52/66] Add coverage --- src/Helpers/Random.php | 10 ++-- tests/src/Helpers/Base64UrlTest.php | 82 +++++++++++++++++++++++++ tests/src/Helpers/DateTimeTest.php | 93 +++++++++++++++++++++++++++++ tests/src/Helpers/HashTest.php | 27 +++++++++ tests/src/Helpers/RandomTest.php | 63 +++++++++++++++++++ tests/src/Helpers/TypeTest.php | 45 +++++++++++++- 6 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 tests/src/Helpers/Base64UrlTest.php create mode 100644 tests/src/Helpers/DateTimeTest.php create mode 100644 tests/src/Helpers/HashTest.php create mode 100644 tests/src/Helpers/RandomTest.php diff --git a/src/Helpers/Random.php b/src/Helpers/Random.php index 472a6db..e282b3a 100644 --- a/src/Helpers/Random.php +++ b/src/Helpers/Random.php @@ -11,25 +11,25 @@ class Random { /** * @param string[] $blacklist - * @return non-empty-string + * @return non-empty-string The generated random string in hexadecimal format. * @throws \SimpleSAML\OpenID\Exceptions\OpenIdException */ public function string( - int $length = 40, + int $byteLength = 40, ?array $blacklist = null, ?string $prefix = null, ?string $suffix = null, int $maxTries = 5, ): string { - if ($length < 1) { - throw new OpenIdException('Random string length can not be less than 1.'); + if ($byteLength < 1) { + throw new OpenIdException('Random byte length can not be less than 1.'); } $errors = []; while ($maxTries-- > 0) { try { - $random = bin2hex(random_bytes($length)); + $random = bin2hex(random_bytes($byteLength)); // @codeCoverageIgnoreStart } catch (Throwable $e) { $errors[] = $e->getMessage(); diff --git a/tests/src/Helpers/Base64UrlTest.php b/tests/src/Helpers/Base64UrlTest.php new file mode 100644 index 0000000..33bfc61 --- /dev/null +++ b/tests/src/Helpers/Base64UrlTest.php @@ -0,0 +1,82 @@ +assertSame('', $this->sut()->encode('')); + $this->assertSame('Zg', $this->sut()->encode('f')); // Zg== + $this->assertSame('Zm8', $this->sut()->encode('fo')); // Zm8= + $this->assertSame('Zm9v', $this->sut()->encode('foo')); // Zm9v + $this->assertSame('Zm9vYg', $this->sut()->encode('foob')); // Zm9vYg== -> trimmed + $this->assertSame('Zm9vYmE', $this->sut()->encode('fooba')); + $this->assertSame('Zm9vYmFy', $this->sut()->encode('foobar')); + + // Ensure + becomes - and / becomes _ + $this->assertSame('-w', $this->sut()->encode("\xFB")); // +w== -> -w + $this->assertSame('_w', $this->sut()->encode("\xFF")); // /w== -> _w + $this->assertSame('-_8', $this->sut()->encode("\xFB\xFF")); // +/8= -> -_8 + } + + + public function testDecodeReversesEncodeAndHandlesPadding(): void + { + // Simple known mappings + $this->assertSame('', $this->sut()->decode('')); + $this->assertSame('f', $this->sut()->decode('Zg')); + $this->assertSame('fo', $this->sut()->decode('Zm8')); + $this->assertSame('foo', $this->sut()->decode('Zm9v')); + $this->assertSame('foob', $this->sut()->decode('Zm9vYg')); + $this->assertSame('fooba', $this->sut()->decode('Zm9vYmE')); + $this->assertSame('foobar', $this->sut()->decode('Zm9vYmFy')); + + // Characters that require translation + $this->assertSame("\xFB", $this->sut()->decode('-w')); + $this->assertSame("\xFF", $this->sut()->decode('_w')); + $this->assertSame("\xFB\xFF", $this->sut()->decode('-_8')); + } + + + public function testRoundTripVariousInputs(): void + { + $inputs = [ + '', + 'a', 'ab', 'abc', 'abcd', 'abcde', + "\x00\x01\x02\x03", // binary + 'The quick brown fox jumps over the lazy dog', + '✓ àéîöū', // Latin-1/UTF-8 mix + 'Привет мир', // Cyrillic + '你好,世界', // Chinese + 'emoji: 🚀🔥', + ]; + + foreach ($inputs as $input) { + $encoded = $this->sut()->encode($input); + $decoded = $this->sut()->decode($encoded); + $this->assertSame($input, $decoded, 'Round-trip failed for input: ' . $input); + } + } + + + public function testDecodeThrowsOnInvalidInput(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid Base64URL encoded data'); + $this->sut()->decode('abc*'); // '*' is not a valid Base64URL character + } +} diff --git a/tests/src/Helpers/DateTimeTest.php b/tests/src/Helpers/DateTimeTest.php new file mode 100644 index 0000000..c191752 --- /dev/null +++ b/tests/src/Helpers/DateTimeTest.php @@ -0,0 +1,93 @@ +fromXsDateTime($input, new DateTimeZone('UTC')); + $expected = new NativeDateTimeImmutable($input, new DateTimeZone('UTC')); + + $this->assertSame($expected->getTimestamp(), $result->getTimestamp()); + $this->assertSame($expected->format('u'), $result->format('u')); // microseconds + $this->assertSame($expected->format('P'), $result->format('P')); // timezone offset + } + + + public function testFromXsDateTimeFallsBackToAtomWithoutMicroseconds(): void + { + $helper = new DateTimeHelper(); + $input = '2024-03-01T12:34:56+02:00'; + + $result = $helper->fromXsDateTime($input, new DateTimeZone('UTC')); + $expected = new NativeDateTimeImmutable($input, new DateTimeZone('UTC')); + + $this->assertSame($expected->getTimestamp(), $result->getTimestamp()); + $this->assertSame($expected->format('P'), $result->format('P')); + } + + + public function testFromXsDateTimeFallsBackToConstructorIso8601Zulu(): void + { + $helper = new DateTimeHelper(); + $input = '2024-03-01T12:34:56Z'; + + $result = $helper->fromXsDateTime($input, new DateTimeZone('Europe/Prague')); + $expected = new NativeDateTimeImmutable($input, new DateTimeZone('Europe/Prague')); + + $this->assertSame($expected->getTimestamp(), $result->getTimestamp()); + $this->assertSame('+00:00', $result->format('P')); + } + + + public function testFromXsDateTimeUsesProvidedTimezoneWhenInputHasNoOffset(): void + { + $helper = new DateTimeHelper(); + $input = '2024-03-01T12:34:56'; // no timezone info in the string + $tz = new DateTimeZone('UTC'); + + $result = $helper->fromXsDateTime($input, $tz); + $expected = new NativeDateTimeImmutable($input, $tz); + + $this->assertSame($expected->getTimestamp(), $result->getTimestamp()); + $this->assertSame('+00:00', $result->format('P')); + } + + + public function testGetUtcReturnsDateInUtc(): void + { + $helper = new DateTimeHelper(); + $input = '2024-03-01 12:00:00'; + + $result = $helper->getUtc($input); + $expected = new NativeDateTimeImmutable($input, new DateTimeZone('UTC')); + + $this->assertSame($expected->getTimestamp(), $result->getTimestamp()); + $this->assertSame('+00:00', $result->format('P')); + } + + + public function testFromTimestampCreatesUtcDateTime(): void + { + $helper = new DateTimeHelper(); + $timestamp = 1_728_000; // 20 days after epoch + + $result = $helper->fromTimestamp($timestamp); + + $this->assertSame($timestamp, $result->getTimestamp()); + $this->assertSame('+00:00', $result->format('P')); + } +} diff --git a/tests/src/Helpers/HashTest.php b/tests/src/Helpers/HashTest.php new file mode 100644 index 0000000..7c4522a --- /dev/null +++ b/tests/src/Helpers/HashTest.php @@ -0,0 +1,27 @@ +for($algorithm, $data, $binary, $options); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/src/Helpers/RandomTest.php b/tests/src/Helpers/RandomTest.php new file mode 100644 index 0000000..9a48bc7 --- /dev/null +++ b/tests/src/Helpers/RandomTest.php @@ -0,0 +1,63 @@ +random = new Random(); + } + + + public function testLength(): void + { + $this->assertSame(80, strlen($this->random->string(40))); + } + + + public function testLengthWithPrefixAndSuffix(): void + { + $this->assertSame(92, strlen($this->random->string(40, null, 'prefix', 'suffix'))); + } + + + public function testInvalidLength(): void + { + $this->expectException(OpenIdException::class); + $this->random->string(0); + } + + + public function testBlacklist(): void + { + // This test is tricky because of the random nature. + // We can't guarantee a collision. + // We can try to mock random_bytes, but that's also complex. + // For now, we'll just test that it *can* return a value + // even with a blocklist. + $randomString = $this->random->string(40, ['some-blacklisted-string']); + $this->assertSame(80, strlen($randomString)); + } + + + public function testPrefixAndSuffix(): void + { + $randomString = $this->random->string(10, null, 'prefix-', '-suffix'); + $this->assertStringStartsWith('prefix-', $randomString); + $this->assertStringEndsWith('-suffix', $randomString); + // The random part is 10 bytes, which is 20 hex characters. + $this->assertSame(20 + 7 + 7, strlen($randomString)); + } +} diff --git a/tests/src/Helpers/TypeTest.php b/tests/src/Helpers/TypeTest.php index 4a88e15..91b7030 100644 --- a/tests/src/Helpers/TypeTest.php +++ b/tests/src/Helpers/TypeTest.php @@ -147,7 +147,7 @@ public function testCanEnsureArrayWithKeysAsStrings(): void $this->sut()->ensureArrayWithKeysAsStrings([0, 1, 2]), ); - // Test call for nested array + // Test call for a nested array $this->assertSame( [['0' => 0, '1' => 1], ['0' => 2, '1' => 3]], array_map( @@ -173,7 +173,7 @@ public function testCanEnsureArrayWithKeysAsNonEmptyStrings(): void $this->sut()->ensureArrayWithKeysAsNonEmptyStrings([0, 1, 2]), ); - // Test call for nested array + // Test call for a nested array $this->assertSame( [['0' => 0, '1' => 1], ['0' => 2, '1' => 3]], array_map( @@ -218,7 +218,7 @@ public function testCanEnsureArrayWithKeysAndValuesAsNonEmptyStrings(): void $this->sut()->ensureArrayWithKeysAndValuesAsNonEmptyStrings([0, 1, 2]), ); - // Test call for nested array + // Test call for a nested array $this->assertSame( [['0' => '0', '1' => '1'], ['0' => '2', '1' => '3']], array_map( @@ -243,4 +243,43 @@ public function testEnsureIntThrowsForNonNull(): void $this->sut()->ensureInt(null); } + + public function testCanEnforceRegex(): void + { + $this->assertSame('a', $this->sut()->enforceRegex('a', '/^a$/')); + } + + public function testEnforceRegexThrowsForInvalidValue(): void + { + $this->expectException(InvalidValueException::class); + $this->expectExceptionMessage('Regex'); + + $this->sut()->enforceRegex('a', '/^b$/'); + } + + public function testCanEnforceUri(): void + { + $this->assertSame('https://example.com', $this->sut()->enforceUri('https://example.com')); + } + + public function testEnforceUriThrowsForInvalidValue(): void + { + $this->expectException(InvalidValueException::class); + $this->expectExceptionMessage('URI'); + + $this->sut()->enforceUri('a'); + } + + public function testCanEnforceArrayOfArrays(): void + { + $a = ['a' => ['b' => 'c']]; + $this->assertSame($a, $this->sut()->enforceArrayOfArrays($a)); + } + + public function testEnforceArrayOfArraysThrowsForInvalidValue(): void + { + $this->expectException(InvalidValueException::class); + $this->expectExceptionMessage('Non-array'); + $this->sut()->enforceArrayOfArrays(['a' => 'b']);; + } } From 36055808b0338d4e99d011d945157450f88cfe94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Sat, 25 Oct 2025 22:59:20 +0200 Subject: [PATCH 53/66] Add coverage --- tests/src/Helpers/TypeTest.php | 52 ++++++++++- .../Jwk/Factories/JwkDecoratorFactoryTest.php | 91 +++++++++++++++++++ tests/src/Jwk/JwkDecoratorTest.php | 50 ++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 tests/src/Jwk/Factories/JwkDecoratorFactoryTest.php create mode 100644 tests/src/Jwk/JwkDecoratorTest.php diff --git a/tests/src/Helpers/TypeTest.php b/tests/src/Helpers/TypeTest.php index 91b7030..2416ea0 100644 --- a/tests/src/Helpers/TypeTest.php +++ b/tests/src/Helpers/TypeTest.php @@ -244,11 +244,13 @@ public function testEnsureIntThrowsForNonNull(): void $this->sut()->ensureInt(null); } + public function testCanEnforceRegex(): void { $this->assertSame('a', $this->sut()->enforceRegex('a', '/^a$/')); } + public function testEnforceRegexThrowsForInvalidValue(): void { $this->expectException(InvalidValueException::class); @@ -257,11 +259,13 @@ public function testEnforceRegexThrowsForInvalidValue(): void $this->sut()->enforceRegex('a', '/^b$/'); } + public function testCanEnforceUri(): void { $this->assertSame('https://example.com', $this->sut()->enforceUri('https://example.com')); } + public function testEnforceUriThrowsForInvalidValue(): void { $this->expectException(InvalidValueException::class); @@ -270,16 +274,62 @@ public function testEnforceUriThrowsForInvalidValue(): void $this->sut()->enforceUri('a'); } + public function testCanEnforceArrayOfArrays(): void { $a = ['a' => ['b' => 'c']]; $this->assertSame($a, $this->sut()->enforceArrayOfArrays($a)); } + public function testEnforceArrayOfArraysThrowsForInvalidValue(): void { $this->expectException(InvalidValueException::class); $this->expectExceptionMessage('Non-array'); - $this->sut()->enforceArrayOfArrays(['a' => 'b']);; + $this->sut()->enforceArrayOfArrays(['a' => 'b']); + ; + } + + + public function testCanEnforceNonEmptyArray(): void + { + $this->assertSame(['a'], $this->sut()->enforceNonEmptyArray(['a'])); + } + + + public function testEnforceNonEmptyArrayThrowsForInvalidValue(): void + { + $this->expectException(InvalidValueException::class); + $this->expectExceptionMessage('Empty'); + $this->sut()->enforceNonEmptyArray([]); + } + + + public function testCanEnforceNonEmptyArrayWithValuesAsNonEmptyStrings(): void + { + $this->assertSame(['a'], $this->sut()->enforceNonEmptyArrayWithValuesAsNonEmptyStrings(['a'])); + } + + + public function testEnforceNonEmptyArrayWithValuesAsNonEmptyStringsThrowsForInvalidValue(): void + { + $this->expectException(InvalidValueException::class); + $this->expectExceptionMessage('Empty'); + $this->sut()->enforceNonEmptyArrayWithValuesAsNonEmptyStrings([]); + } + + + public function testCanEnforceNonEmptyArrayOfNonEmptyArrays(): void + { + $a = [['a' => 'b']]; + $this->assertSame($a, $this->sut()->enforceNonEmptyArrayOfNonEmptyArrays($a)); + } + + + public function testEnforceNonEmptyArrayOfNonEmptyArraysThrowsForInvalidValue(): void + { + $this->expectException(InvalidValueException::class); + $this->expectExceptionMessage('Non-array'); + $this->sut()->enforceNonEmptyArrayOfNonEmptyArrays(['a' => 'b']); } } diff --git a/tests/src/Jwk/Factories/JwkDecoratorFactoryTest.php b/tests/src/Jwk/Factories/JwkDecoratorFactoryTest.php new file mode 100644 index 0000000..746285b --- /dev/null +++ b/tests/src/Jwk/Factories/JwkDecoratorFactoryTest.php @@ -0,0 +1,91 @@ +sut()->fromData(['kty' => 'oct', 'k' => 'abc']); + + $this->assertInstanceOf(JwkDecorator::class, $decorator); + $this->assertInstanceOf(JWK::class, $decorator->jwk()); + } + + + public function testFromDataPassesValuesToUnderlyingJwk(): void + { + $data = ['kty' => 'oct', 'k' => 'xyz', 'alg' => 'HS256']; + $decorator = $this->sut()->fromData($data); + + $this->assertSame($data, $decorator->jwk()->all()); + } + + + public function testFromDataThrowsWhenKtyIsMissing(): void + { + $this->expectException(InvalidArgumentException::class); + $this->sut()->fromData(['alg' => 'HS256']); + } + + + public function testFromPkcs1Or8KeyFileThrowsOnMissingFile(): void + { + $this->expectException(InvalidArgumentException::class); + // Non-existing file path should cause KeyConverter to fail reading the file + @$this->sut()->fromPkcs1Or8KeyFile(__DIR__ . DIRECTORY_SEPARATOR . 'no_such_key.pem'); + } + + + public function testFromPkcs12CertificateFileThrowsOnMissingFile(): void + { + // The vendor code wraps any error into a RuntimeException with a generic message + $this->expectException(RuntimeException::class); + @$this->sut()->fromPkcs12CertificateFile(__DIR__ . DIRECTORY_SEPARATOR . 'no_such_cert.p12', 'secret'); + } + + + public function testFromX509CertificateFileThrowsOnMissingFile(): void + { + $this->expectException(InvalidArgumentException::class); + @$this->sut()->fromX509CertificateFile(__DIR__ . DIRECTORY_SEPARATOR . 'no_such_cert.crt'); + } + + + public function testFromPkcs1Or8KeyThrowsOnInvalidKeyString(): void + { + // Depending on the environment (OpenSSL availability), the vendor may throw RuntimeException or + // InvalidArgumentException + $this->expectException(Throwable::class); + $this->sut()->fromPkcs1Or8Key('not a valid key'); + } + + + public function testFromX509CertificateThrowsOnInvalidCertificateString(): void + { + // Depending on the environment (OpenSSL availability), the vendor may throw RuntimeException or + // InvalidArgumentException + $this->expectException(Throwable::class); + $this->sut()->fromX509Certificate('not a valid certificate'); + } +} diff --git a/tests/src/Jwk/JwkDecoratorTest.php b/tests/src/Jwk/JwkDecoratorTest.php new file mode 100644 index 0000000..a4018bc --- /dev/null +++ b/tests/src/Jwk/JwkDecoratorTest.php @@ -0,0 +1,50 @@ +jwkMock = $this->createMock(JWK::class); + } + + + protected function sut( + ?JWK $jwk = null, + ): JwkDecorator { + $jwk ??= $this->jwkMock; + + return new JwkDecorator($jwk); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf( + JwkDecorator::class, + $this->sut(), + ); + } + + + public function testCanGetJwk(): void + { + $this->assertSame( + $this->jwkMock, + $this->sut()->jwk(), + ); + } +} From 9203355ee8c5dbef144370132d0bcca7f1e7c94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Tue, 11 Nov 2025 10:50:05 +0100 Subject: [PATCH 54/66] Update master code to new API --- src/Federation.php | 4 +-- .../TrustMarkStatusResponseFactory.php | 4 +-- .../TrustMarkStatusResponseFactoryTest.php | 26 +++++++++---------- .../TrustMarkStatusResponseTest.php | 12 ++++----- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Federation.php b/src/Federation.php index b3a643b..eb8eba6 100644 --- a/src/Federation.php +++ b/src/Federation.php @@ -267,9 +267,9 @@ public function trustMarkDelegationFactory(): TrustMarkDelegationFactory public function trustMarkStatusResponseFactory(): TrustMarkStatusResponseFactory { return $this->trustMarkStatusResponseFactory ??= new TrustMarkStatusResponseFactory( - $this->jwsParser(), + $this->jwsDecoratorBuilder(), $this->jwsVerifierDecorator(), - $this->jwksFactory(), + $this->jwksDecoratorFactory(), $this->jwsSerializerManagerDecorator(), $this->timestampValidationLeewayDecorator, $this->helpers(), diff --git a/src/Federation/Factories/TrustMarkStatusResponseFactory.php b/src/Federation/Factories/TrustMarkStatusResponseFactory.php index 58e7f18..f5ca5b3 100644 --- a/src/Federation/Factories/TrustMarkStatusResponseFactory.php +++ b/src/Federation/Factories/TrustMarkStatusResponseFactory.php @@ -12,9 +12,9 @@ class TrustMarkStatusResponseFactory extends ParsedJwsFactory public function fromToken(string $token): TrustMarkStatusResponse { return new TrustMarkStatusResponse( - $this->jwsParser->parse($token), + $this->jwsDecoratorBuilder->fromToken($token), $this->jwsVerifierDecorator, - $this->jwksFactory, + $this->jwksDecoratorFactory, $this->jwsSerializerManagerDecorator, $this->timestampValidationLeeway, $this->helpers, diff --git a/tests/src/Federation/Factories/TrustMarkStatusResponseFactoryTest.php b/tests/src/Federation/Factories/TrustMarkStatusResponseFactoryTest.php index e9d1cae..168f71b 100644 --- a/tests/src/Federation/Factories/TrustMarkStatusResponseFactoryTest.php +++ b/tests/src/Federation/Factories/TrustMarkStatusResponseFactoryTest.php @@ -15,10 +15,10 @@ use SimpleSAML\OpenID\Federation\Factories\TrustMarkStatusResponseFactory; use SimpleSAML\OpenID\Federation\TrustMarkStatusResponse; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; -use SimpleSAML\OpenID\Jws\JwsParser; +use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; @@ -31,11 +31,11 @@ final class TrustMarkStatusResponseFactoryTest extends TestCase { protected MockObject $signatureMock; - protected MockObject $jwsParserMock; + protected MockObject $jwsDecoratorBuilderMock; protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerMock; @@ -73,11 +73,11 @@ protected function setUp(): void $jwsDecoratorMock = $this->createMock(JwsDecorator::class); $jwsDecoratorMock->method('jws')->willReturn($jwsMock); - $this->jwsParserMock = $this->createMock(JwsParser::class); - $this->jwsParserMock->method('parse')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -95,26 +95,26 @@ protected function setUp(): void protected function sut( - ?JwsParser $jwsParser = null, + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, ?ClaimFactory $claimFactory = null, ): TrustMarkStatusResponseFactory { - $jwsParser ??= $this->jwsParserMock; + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; $claimFactory ??= $this->claimFactoryMock; return new TrustMarkStatusResponseFactory( - $jwsParser, + $jwsDecoratorBuilder, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, diff --git a/tests/src/Federation/TrustMarkStatusResponseTest.php b/tests/src/Federation/TrustMarkStatusResponseTest.php index 7963055..2f73a6d 100644 --- a/tests/src/Federation/TrustMarkStatusResponseTest.php +++ b/tests/src/Federation/TrustMarkStatusResponseTest.php @@ -15,7 +15,7 @@ use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Federation\TrustMarkStatusResponse; use SimpleSAML\OpenID\Helpers; -use SimpleSAML\OpenID\Jwks\Factories\JwksFactory; +use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsVerifierDecorator; use SimpleSAML\OpenID\Jws\ParsedJws; @@ -31,7 +31,7 @@ final class TrustMarkStatusResponseTest extends TestCase protected MockObject $jwsVerifierDecoratorMock; - protected MockObject $jwksFactoryMock; + protected MockObject $jwksDecoratorFactoryMock; protected MockObject $jwsSerializerManagerDecoratorMock; @@ -71,7 +71,7 @@ protected function setUp(): void $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); - $this->jwksFactoryMock = $this->createMock(JwksFactory::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); @@ -91,7 +91,7 @@ protected function setUp(): void protected function sut( ?JwsDecorator $jwsDecorator = null, ?JwsVerifierDecorator $jwsVerifierDecorator = null, - ?JwksFactory $jwksFactory = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, ?DateIntervalDecorator $dateIntervalDecorator = null, ?Helpers $helpers = null, @@ -99,7 +99,7 @@ protected function sut( ): TrustMarkStatusResponse { $jwsDecorator ??= $this->jwsDecoratorMock; $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; - $jwksFactory ??= $this->jwksFactoryMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; $helpers ??= $this->helpersMock; @@ -108,7 +108,7 @@ protected function sut( return new TrustMarkStatusResponse( $jwsDecorator, $jwsVerifierDecorator, - $jwksFactory, + $jwksDecoratorFactory, $jwsSerializerManagerDecorator, $dateIntervalDecorator, $helpers, From 5ad7f9520697e62ffbc599c5a83278292e767f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Tue, 11 Nov 2025 16:18:26 +0100 Subject: [PATCH 55/66] WIP coverage --- .../Factories/TrustMarkFactoryTest.php | 2 +- .../Jwks/Factories/SignedJwksFactoryTest.php | 24 +++++++++ .../Jws/Factories/ParsedJwsFactoryTest.php | 20 +++++++ tests/src/Jws/JwsDecoratorBuilderTest.php | 37 +++++++++++++ tests/src/Jws/ParsedJwsTest.php | 41 +++++++++++++++ .../Factories/DisclosureBagFactoryTest.php | 52 +++++++++++++++++++ 6 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 tests/src/SdJwt/Factories/DisclosureBagFactoryTest.php diff --git a/tests/src/Federation/Factories/TrustMarkFactoryTest.php b/tests/src/Federation/Factories/TrustMarkFactoryTest.php index 4e93069..5b818f3 100644 --- a/tests/src/Federation/Factories/TrustMarkFactoryTest.php +++ b/tests/src/Federation/Factories/TrustMarkFactoryTest.php @@ -144,7 +144,7 @@ public function testCanCreateInstance(): void } - public function testCanBuild(): void + public function testCanBuildFromToken(): void { $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); diff --git a/tests/src/Jwks/Factories/SignedJwksFactoryTest.php b/tests/src/Jwks/Factories/SignedJwksFactoryTest.php index ddd6425..a41c6a5 100644 --- a/tests/src/Jwks/Factories/SignedJwksFactoryTest.php +++ b/tests/src/Jwks/Factories/SignedJwksFactoryTest.php @@ -10,9 +10,11 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jwks\Factories\SignedJwksFactory; use SimpleSAML\OpenID\Jwks\SignedJwks; @@ -74,6 +76,8 @@ final class SignedJwksFactoryTest extends TestCase protected array $validPayload; + protected MockObject $jwkDecoratorMock; + protected function setUp(): void { @@ -89,6 +93,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock->method('fromData')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); @@ -108,6 +113,8 @@ protected function setUp(): void $this->validPayload = $this->expiredPayload; $this->validPayload['exp'] = time() + 3600; + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); } @@ -156,4 +163,21 @@ public function testCanBuildFromToken(): void $this->sut()->fromToken('token'), ); } + + + public function testCanBuildFromData(): void + { + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + + $this->assertInstanceOf( + SignedJwks::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::RS256, + $this->validPayload, + $this->sampleHeader, + ), + ); + } } diff --git a/tests/src/Jws/Factories/ParsedJwsFactoryTest.php b/tests/src/Jws/Factories/ParsedJwsFactoryTest.php index c17c304..79a0e6e 100644 --- a/tests/src/Jws/Factories/ParsedJwsFactoryTest.php +++ b/tests/src/Jws/Factories/ParsedJwsFactoryTest.php @@ -8,9 +8,11 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; @@ -36,6 +38,8 @@ final class ParsedJwsFactoryTest extends TestCase protected MockObject $claimFactoryMock; + protected MockObject $jwkDecoratorMock; + protected function setUp(): void { @@ -46,6 +50,8 @@ protected function setUp(): void $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); $this->helpersMock = $this->createMock(Helpers::class); $this->claimFactoryMock = $this->createMock(ClaimFactory::class); + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); } @@ -91,4 +97,18 @@ public function testCanBuildFromToken(): void $this->sut()->fromToken('token'), ); } + + + public function testCanBuildFromData(): void + { + $this->assertInstanceOf( + ParsedJws::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::RS256, + [], + [], + ), + ); + } } diff --git a/tests/src/Jws/JwsDecoratorBuilderTest.php b/tests/src/Jws/JwsDecoratorBuilderTest.php index 44504bf..2fe13a2 100644 --- a/tests/src/Jws/JwsDecoratorBuilderTest.php +++ b/tests/src/Jws/JwsDecoratorBuilderTest.php @@ -9,8 +9,10 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Exceptions\JwsException; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator; @@ -27,6 +29,8 @@ final class JwsDecoratorBuilderTest extends TestCase protected MockObject $jwsDecoratorMock; + protected MockObject $jwkDecoratorMock; + protected function setUp(): void { @@ -34,6 +38,8 @@ protected function setUp(): void $this->jwsBuilderMock = $this->createMock(JWSBuilder::class); $this->helpersMock = $this->createMock(Helpers::class); $this->jwsDecoratorMock = $this->createMock(JwsDecorator::class); + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); } @@ -79,4 +85,35 @@ public function testThrowsOnTokenParseError(): void $this->sut()->fromToken('token'); } + + + public function testCanBuildFromData(): void + { + $this->assertInstanceOf( + JwsDecorator::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::RS256, + [], + [], + ), + ); + } + + + public function testBuildFromDataThrowsJwsException(): void + { + $this->jwsBuilderMock->expects($this->once())->method('create') + ->willThrowException(new \Exception('Error')); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('build'); + + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::RS256, + [], + [], + ); + } } diff --git a/tests/src/Jws/ParsedJwsTest.php b/tests/src/Jws/ParsedJwsTest.php index 1d15919..563d5e7 100644 --- a/tests/src/Jws/ParsedJwsTest.php +++ b/tests/src/Jws/ParsedJwsTest.php @@ -40,6 +40,8 @@ final class ParsedJwsTest extends TestCase protected MockObject $jsonHelperMock; + protected MockObject $arrHelperMock; + protected MockObject $claimFactoryMock; protected array $sampleHeader = [ @@ -121,6 +123,8 @@ protected function setUp(): void $this->helpersMock->method('json')->willReturn($this->jsonHelperMock); $typeHelperMock = $this->createMock(Helpers\Type::class); $this->helpersMock->method('type')->willReturn($typeHelperMock); + $this->arrHelperMock = $this->createMock(Helpers\Arr::class); + $this->helpersMock->method('arr')->willReturn($this->arrHelperMock); $typeHelperMock->method('ensureNonEmptyString')->willReturnArgument(0); $typeHelperMock->method('ensureArrayWithValuesAsStrings')->willReturnArgument(0); @@ -238,6 +242,7 @@ public function testCanGetHeaderClaims(): void $this->assertSame($this->sampleHeader['kid'], $this->sut()->getHeaderClaim('kid')); $this->assertSame($this->sampleHeader['kid'], $this->sut()->getKeyId()); $this->assertSame($this->sampleHeader['typ'], $this->sut()->getType()); + $this->assertSame($this->sampleHeader['alg'], $this->sut()->getAlgorithm()); } @@ -299,6 +304,7 @@ public function testCanGetPayloadClaims(): void $this->assertSame($this->validPayload['sub'], $sut->getSubject()); $this->assertSame($this->validPayload['exp'], $sut->getExpirationTime()); $this->assertSame($this->validPayload['iat'], $sut->getIssuedAt()); + $this->assertSame($this->validPayload['nbf'], $sut->getNotBefore()); } @@ -315,6 +321,18 @@ public function testCanGetEmptyPayloadClaims(): void $this->assertNull($sut->getIssuedAt()); $this->assertNull($sut->getIdentifier()); $this->assertNull($sut->getIssuer()); + $this->assertNull($sut->getNotBefore()); + } + + + public function testCanGetNestedPayloadClaims(): void + { + $this->jwsMock->expects($this->once())->method('getPayload')->willReturn('payload-json'); + $this->jsonHelperMock->expects($this->once())->method('decode')->willReturn($this->validPayload); + + $this->arrHelperMock->expects($this->once())->method('getNestedValue'); + + $this->sut()->getNestedPayloadClaim('metadata'); } @@ -373,6 +391,15 @@ public function testCanVerifyWithKeySet(): void } + public function testCanVerifyWithKey(): void + { + $this->jwsVerifierDecoratorMock->expects($this->once())->method('verifyWithKeySet') + ->willReturn(true); + + $this->sut()->verifyWithKey(['key']); + } + + public function testThrowsOnVerifyWithKeySetError(): void { $this->jwsVerifierDecoratorMock->expects($this->once())->method('verifyWithKeySet') @@ -409,4 +436,18 @@ public function testThrowsIfIssuedAtInTheFuture(): void $this->sut()->getIssuedAt(); } + + + public function testThrowsIfNotBeforeInTheFuture(): void + { + $this->jwsMock->expects($this->once())->method('getPayload')->willReturn('payload-json'); + $payload = $this->validPayload; + $payload['nbf'] = time() + 60; + $this->jsonHelperMock->expects($this->once())->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Not Before'); + + $this->sut()->getNotBefore(); + } } diff --git a/tests/src/SdJwt/Factories/DisclosureBagFactoryTest.php b/tests/src/SdJwt/Factories/DisclosureBagFactoryTest.php new file mode 100644 index 0000000..03a2236 --- /dev/null +++ b/tests/src/SdJwt/Factories/DisclosureBagFactoryTest.php @@ -0,0 +1,52 @@ +disclosureMock = $this->createMock(Disclosure::class); + } + + + protected function sut(): DisclosureBagFactory + { + return new DisclosureBagFactory(); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(DisclosureBagFactory::class, $this->sut()); + } + + + public function testCanBuild(): void + { + $this->assertInstanceOf( + DisclosureBag::class, + $this->sut()->build(), + ); + + $this->assertInstanceOf( + DisclosureBag::class, + $this->sut()->build($this->disclosureMock), + ); + } +} From b378d70cbaab86c216865292b14921397728180d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 17 Nov 2025 10:43:30 +0100 Subject: [PATCH 56/66] WIP coverage --- src/SdJwt/DisclosureBag.php | 4 +- src/SdJwt/Factories/SdJwtFactory.php | 10 +- src/SdJwt/SdJwt.php | 8 + tests/src/Jws/ParsedJwsTest.php | 1 - tests/src/SdJwt/DisclosureBagTest.php | 70 +++++ tests/src/SdJwt/DisclosureTest.php | 28 ++ .../SdJwt/Factories/DisclosureFactoryTest.php | 56 ++++ tests/src/SdJwt/SdJwtTest.php | 261 ++++++++++++++++++ .../CredentialOfferGrantsBagTest.php | 43 +++ .../CredentialOfferGrantsValueTest.php | 154 +++++++++++ 10 files changed, 630 insertions(+), 5 deletions(-) create mode 100644 tests/src/SdJwt/DisclosureBagTest.php create mode 100644 tests/src/SdJwt/Factories/DisclosureFactoryTest.php create mode 100644 tests/src/SdJwt/SdJwtTest.php create mode 100644 tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsBagTest.php create mode 100644 tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValueTest.php diff --git a/src/SdJwt/DisclosureBag.php b/src/SdJwt/DisclosureBag.php index d0f90b3..041e5d3 100644 --- a/src/SdJwt/DisclosureBag.php +++ b/src/SdJwt/DisclosureBag.php @@ -11,12 +11,12 @@ class DisclosureBag /** * @var \SimpleSAML\OpenID\SdJwt\Disclosure[] */ - protected array $disclosures; + protected array $disclosures = []; public function __construct(Disclosure ...$disclosures) { - $this->disclosures = $disclosures; + $this->add(...$disclosures); } diff --git a/src/SdJwt/Factories/SdJwtFactory.php b/src/SdJwt/Factories/SdJwtFactory.php index 1050b74..e38e670 100644 --- a/src/SdJwt/Factories/SdJwtFactory.php +++ b/src/SdJwt/Factories/SdJwtFactory.php @@ -112,6 +112,8 @@ public function updatePayloadWithDisclosures( $disclosurePaths = []; + $usedDecoys = 0; + foreach ($disclosures as $disclosure) { $disclosurePath = $disclosure->getPath(); @@ -131,8 +133,12 @@ public function updatePayloadWithDisclosures( ...$disclosurePath, ); - // Randomly add decoys. - if (random_int(0, 1) !== 0) { + // Ensure minimum and maximum number of decoys, otherwise add them + // randomly. + if ( + ($usedDecoys < $minDecoys && $usedDecoys <= $maxDecoys) || + random_int(0, 1) !== 0 + ) { $disclosurePathReference =& $this->helpers->arr()->getNestedValueReference( $payload, ...$disclosurePath, diff --git a/src/SdJwt/SdJwt.php b/src/SdJwt/SdJwt.php index c4d0d26..a316b28 100644 --- a/src/SdJwt/SdJwt.php +++ b/src/SdJwt/SdJwt.php @@ -92,6 +92,11 @@ public function getKbJwt(): ?KbJwt } + /** + * @param \SimpleSAML\OpenID\SdJwt\DisclosureBag|null $disclosureBag If provided, only disclosures with matching + * salts will be included. + * @throws \JsonException + */ public function getToken( JwsSerializerEnum $jwsSerializerEnum = JwsSerializerEnum::Compact, ?int $signatureIndex = null, @@ -138,6 +143,9 @@ protected function validate(): void { $this->validateByCallbacks( $this->getSelectiveDisclosureAlgorithm(...), + $this->getExpirationTime(...), + $this->getIssuedAt(...), + $this->getNotBefore(...), ); } } diff --git a/tests/src/Jws/ParsedJwsTest.php b/tests/src/Jws/ParsedJwsTest.php index 563d5e7..2895f47 100644 --- a/tests/src/Jws/ParsedJwsTest.php +++ b/tests/src/Jws/ParsedJwsTest.php @@ -203,7 +203,6 @@ public function testThrowsOnValidateByCallbacksError(): void $this->expectException(JwsException::class); $this->expectExceptionMessage('not valid'); - /** @phpstan-ignore expr.resultUnused (Validation is invoked from constructor.) */ new class ( $this->jwsDecoratorMock, $this->jwsVerifierDecoratorMock, diff --git a/tests/src/SdJwt/DisclosureBagTest.php b/tests/src/SdJwt/DisclosureBagTest.php new file mode 100644 index 0000000..1227089 --- /dev/null +++ b/tests/src/SdJwt/DisclosureBagTest.php @@ -0,0 +1,70 @@ +disclosureMock = $this->createMock(Disclosure::class); + $this->disclosureMock->method('getSalt')->willReturn('salt'); + } + + + protected function sut( + Disclosure ...$disclosures, + ): DisclosureBag { + return new DisclosureBag(...$disclosures); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(DisclosureBag::class, $this->sut()); + } + + + public function testCanAddAndGet(): void + { + $sut = $this->sut($this->disclosureMock); + + $this->assertCount(1, $sut->all()); + + $disclosureMock2 = $this->createMock(Disclosure::class); + $disclosureMock2->method('getSalt')->willReturn('salt2'); + $sut->add($disclosureMock2); + + $this->assertCount(2, $sut->all()); + } + + + public function testCanGetSalts(): void + { + $sut = $this->sut($this->disclosureMock); + $this->assertSame( + ['salt'], + $sut->salts(), + ); + } + + + public function testAddThrowsForDuplicateSalt(): void + { + $sut = $this->sut($this->disclosureMock); + $this->expectException(SdJwtException::class); + $this->expectExceptionMessage('salt'); + $sut->add($this->disclosureMock); + } +} diff --git a/tests/src/SdJwt/DisclosureTest.php b/tests/src/SdJwt/DisclosureTest.php index c1c7e0f..1e8eb46 100644 --- a/tests/src/SdJwt/DisclosureTest.php +++ b/tests/src/SdJwt/DisclosureTest.php @@ -14,6 +14,12 @@ use SimpleSAML\OpenID\SdJwt\Disclosure; #[CoversClass(Disclosure::class)] +#[\PHPUnit\Framework\Attributes\UsesClass(Helpers::class)] +#[\PHPUnit\Framework\Attributes\UsesClass(Helpers\Base64Url::class)] +#[\PHPUnit\Framework\Attributes\UsesClass(Helpers\Json::class)] +#[\PHPUnit\Framework\Attributes\UsesClass(Helpers\Type::class)] +#[\PHPUnit\Framework\Attributes\UsesClass(Helpers\Hash::class)] +#[\PHPUnit\Framework\Attributes\UsesClass(HashAlgorithmsEnum::class)] final class DisclosureTest extends TestCase { protected Helpers $helpers; @@ -132,4 +138,26 @@ public function testHasProperType(): void $this->sut(name: false)->getType(), ); } + + + public function testCanGetEncoded(): void + { + $this->assertNotEmpty($this->sut()->getEncoded()); + } + + + public function testCanGetDigest(): void + { + $this->assertNotEmpty($this->sut()->getDigest()); + } + + + public function testCanGetDigestRepresentation(): void + { + $this->assertNotEmpty($this->sut( + name: false, + )->getDigestRepresentation()); + + $this->assertNotEmpty($this->sut()->getDigestRepresentation()); + } } diff --git a/tests/src/SdJwt/Factories/DisclosureFactoryTest.php b/tests/src/SdJwt/Factories/DisclosureFactoryTest.php new file mode 100644 index 0000000..c31eba1 --- /dev/null +++ b/tests/src/SdJwt/Factories/DisclosureFactoryTest.php @@ -0,0 +1,56 @@ +helpersMock = $this->createMock(Helpers::class); + $radomHelperMock = $this->createMock(Helpers\Random::class); + $this->helpersMock->method('random')->willReturn($radomHelperMock); + } + + + protected function sut(): DisclosureFactory + { + return new DisclosureFactory($this->helpersMock); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(DisclosureFactory::class, $this->sut()); + } + + + public function testCanBuild(): void + { + $this->assertInstanceOf( + \SimpleSAML\OpenID\SdJwt\Disclosure::class, + $this->sut()->build('value', 'name'), + ); + } + + + public function testCanBuildDecoy(): void + { + $this->assertInstanceOf( + \SimpleSAML\OpenID\SdJwt\Disclosure::class, + $this->sut()->buildDecoy(SdJwtDisclosureType::ArrayElement, []), + ); + } +} diff --git a/tests/src/SdJwt/SdJwtTest.php b/tests/src/SdJwt/SdJwtTest.php new file mode 100644 index 0000000..61889d0 --- /dev/null +++ b/tests/src/SdJwt/SdJwtTest.php @@ -0,0 +1,261 @@ + [ + "CrQe7S5kqBAHt-nMYXgc6bdt2SH5aTY1sU_M-PgkjPI", + "JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE", + "PorFbpKuVu6xymJagvkFsFXAbRoc2JGlAUA2BA4o7cI", + "TGf4oLbgwd5JQaHyKVQZU9UdGE0w5rtDsrZzfUaomLo", + "XQ_3kPKt1XyX7KANkqVR6yZ2Va5NrPIvPYbyMvRKBMM", + "XzFrzwscM6Gn6CJDc6vVK8BkMnfG8vOSKfpPIZdAfdE", + "gbOsI4Edq2x2Kw-w5wPEzakob9hV1cRD0ATN3oQL9JM", + "jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4", + ], + "iss" => "https://issuer.example.com", + "iat" => 1683000000, + "exp" => 1883000000, + "sub" => "user_42", + "nationalities" => [ + [ + "..." => "pFndjkZ_VCzmyTa6UjlZo3dh-ko8aIKQc9DlGzhaVYo", + ], + [ + "..." => "7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0", + ], + ], + "_sd_alg" => "sha-256", + "cnf" => [ + "jwk" => [ + "kty" => "EC", + "crv" => "P-256", + "x" => "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y" => "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ", + ], + ], + ]; + + protected array $sampleHeader = [ + 'alg' => 'RS256', + 'typ' => 'example+sd-jwt', + 'kid' => 'fsQ45F0D916RdKEeTjta8DYWiodjthouHrVWgOXBrkk', + ]; + + protected array $validPayload; + + protected MockObject $disclosureBagMock; + + protected MockObject $kbJwtMock; + + + protected function setUp(): void + { + $this->signatureMock = $this->createMock(Signature::class); + + $jwsMock = $this->createMock(JWS::class); + $jwsMock->method('getPayload') + ->willReturn('json-payload-string'); // Just so we have non-empty value. + $jwsMock->method('getSignature')->willReturn($this->signatureMock); + + $this->jwsDecoratorMock = $this->createMock(JwsDecorator::class); + $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); + + $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); + $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); + $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); + + $this->helpersMock = $this->createMock(Helpers::class); + $this->jsonHelperMock = $this->createMock(Helpers\Json::class); + $this->helpersMock->method('json')->willReturn($this->jsonHelperMock); + $typeHelperMock = $this->createMock(Helpers\Type::class); + $this->helpersMock->method('type')->willReturn($typeHelperMock); + + $typeHelperMock->method('ensureNonEmptyString')->willReturnArgument(0); + $typeHelperMock->method('ensureInt')->willReturnArgument(0); + $typeHelperMock->method('ensureArray')->willReturnArgument(0); + + $this->claimFactoryMock = $this->createMock(ClaimFactory::class); + + $this->validPayload = $this->expiredPayload; + $this->validPayload['exp'] = time() + 3600; + + $this->disclosureBagMock = $this->createMock(DisclosureBag::class); + $this->kbJwtMock = $this->createMock(KbJwt::class); + } + + + protected function sut( + ?JwsDecorator $jwsDecorator = null, + ?JwsVerifierDecorator $jwsVerifierDecorator = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, + ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, + ?DateIntervalDecorator $dateIntervalDecorator = null, + ?Helpers $helpers = null, + ?ClaimFactory $claimFactory = null, + ?DisclosureBag $disclosureBag = null, + ?KbJwt $kbJwt = null, + ): SdJwt { + $jwsDecorator ??= $this->jwsDecoratorMock; + $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; + $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; + $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; + $helpers ??= $this->helpersMock; + $claimFactory ??= $this->claimFactoryMock; + $disclosureBag ??= $this->disclosureBagMock; + $kbJwt ??= $this->kbJwtMock; + + return new SdJwt( + $jwsDecorator, + $jwsVerifierDecorator, + $jwksDecoratorFactory, + $jwsSerializerManagerDecorator, + $dateIntervalDecorator, + $helpers, + $claimFactory, + $disclosureBag, + $kbJwt, + ); + } + + + public function testCanCreateInstance(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $this->assertInstanceOf( + SdJwt::class, + $this->sut(), + ); + } + + + public function testCanGetCommonProperties(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $sut = $this->sut(); + + $this->assertInstanceOf( + HashAlgorithmsEnum::class, + $sut->getSelectiveDisclosureAlgorithm(), + ); + $this->assertSame( + $this->validPayload['cnf'], + $sut->getConfirmation(), + ); + $this->assertInstanceOf( + DisclosureBag::class, + $sut->getDisclosureBag(), + ); + $this->assertInstanceOf( + KbJwt::class, + $sut->getKbJwt(), + ); + } + + + public function testCanGetNullableCommonProperties(): void + { + $payload = $this->validPayload; + unset($payload['_sd_alg']); + unset($payload['cnf']); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $sut = $this->sut(); + + $this->assertNotInstanceOf(HashAlgorithmsEnum::class, $sut->getSelectiveDisclosureAlgorithm()); + $this->assertNull($sut->getConfirmation()); + } + + + public function testCanGetToken(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $disclosureMock = $this->createMock(Disclosure::class); + $disclosureMock->method('getSalt')->willReturn('salt'); + $this->disclosureBagMock->method('all')->willReturn(['salt' => $disclosureMock]); + + // Since we don't do full token generation in tests, just make sure we have the expected number of tildes. + $this->assertSame("~~", $this->sut()->getToken()); + } + + + public function testCanGetTokenWithFilteredDisclosures(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $disclosureMock = $this->createMock(Disclosure::class); + $disclosureMock->method('getSalt')->willReturn('salt'); + $disclosureMock2 = $this->createMock(Disclosure::class); + $disclosureMock2->method('getSalt')->willReturn('salt2'); + $this->disclosureBagMock->method('all')->willReturn( + ['salt' => $disclosureMock, 'salt2' => $disclosureMock2], + ); + $disclosureBagMock2 = $this->createMock(DisclosureBag::class); + $disclosureBagMock2->method('all')->willReturn(['salt2' => $disclosureMock2]); + + // Since we don't do full token generation in tests, just make sure we have the expected number of tildes. + $this->assertSame("~~~", $this->sut()->getToken()); + $this->assertSame("~~", $this->sut()->getToken(disclosureBag: $disclosureBagMock2)); + } + + + public function testCanGetUndisclosedToken(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + // We don't do full token generation in tests, so we expect an empty string here. + $this->assertEmpty($this->sut()->getUndisclosedToken()); + } +} diff --git a/tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsBagTest.php b/tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsBagTest.php new file mode 100644 index 0000000..864aacc --- /dev/null +++ b/tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsBagTest.php @@ -0,0 +1,43 @@ +credentialOfferGrantsValue = $this->createMock(CredentialOfferGrantsValue::class); + } + + + protected function sut( + CredentialOfferGrantsValue ...$credentialOfferGrantsValues, + ): CredentialOfferGrantsBag { + return new CredentialOfferGrantsBag(...$credentialOfferGrantsValues); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(CredentialOfferGrantsBag::class, $this->sut($this->credentialOfferGrantsValue)); + } + + + public function testCanJsonSerialize(): void + { + $this->credentialOfferGrantsValue->expects($this->once())->method('jsonSerialize'); + + $this->sut($this->credentialOfferGrantsValue)->jsonSerialize(); + } +} diff --git a/tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValueTest.php b/tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValueTest.php new file mode 100644 index 0000000..00b957c --- /dev/null +++ b/tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferGrantsValueTest.php @@ -0,0 +1,154 @@ + "oaKazRN8I0IbtZ0C7JuMn5", + "tx_code" => [ + "length" => 4, + "input_mode" => "numeric", + "description" => "Please provide the one-time code that was sent via e-mail", + ], + ]; + + protected array $sampleAuthCode = [ + "issuer_state" => "eyJhbGciOiJSU0Et...FYUaBy", + "authorization_server" => "https://example.com/oidc/auth", + ]; + + protected string $grantType; + + + protected function setUp(): void + { + $this->grantType = GrantTypesEnum::PreAuthorizedCode->value; + } + + + protected function sut( + ?string $grantType = null, + ?array $claims = null, + ): CredentialOfferGrantsValue { + $grantType ??= $this->grantType; + $claims ??= $this->samplePreAuthCode; + + return new CredentialOfferGrantsValue($grantType, $claims); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf( + CredentialOfferGrantsValue::class, + $this->sut(), + ); + } + + + public function testThrowsOnInvalidAuthCodeIssuerState(): void + { + $this->expectException(CredentialOfferException::class); + $this->expectExceptionMessage("Issuer State"); + + $claims = $this->sampleAuthCode; + $claims['issuer_state'] = 123; + $this->sut(GrantTypesEnum::AuthorizationCode->value, $claims); + } + + + public function testThrowsOnInvalidAuthCodeAuthorizationServer(): void + { + $this->expectException(CredentialOfferException::class); + $this->expectExceptionMessage("Authorization Server"); + + $claims = $this->sampleAuthCode; + $claims['authorization_server'] = 123; + $this->sut(GrantTypesEnum::AuthorizationCode->value, $claims); + } + + + public function testThrowsOnInvalidPreAuthCodeValue(): void + { + $this->expectException(CredentialOfferException::class); + $this->expectExceptionMessage("Pre-authorized Code"); + + $claims = $this->samplePreAuthCode; + $claims['pre-authorized_code'] = 123; + $this->sut(GrantTypesEnum::PreAuthorizedCode->value, $claims); + } + + + public function testThrowsOnInvalidPreAuthCodeTxCodeValue(): void + { + $this->expectException(CredentialOfferException::class); + $this->expectExceptionMessage("TxCode"); + + $claims = $this->samplePreAuthCode; + $claims['tx_code'] = 123; + $this->sut(GrantTypesEnum::PreAuthorizedCode->value, $claims); + } + + + public function testThrowsOnInvalidPreAuthCodeTxCodeInputModeValue(): void + { + $this->expectException(CredentialOfferException::class); + $this->expectExceptionMessage("Input Mode"); + + $claims = $this->samplePreAuthCode; + $claims['tx_code']['input_mode'] = 123; + $this->sut(GrantTypesEnum::PreAuthorizedCode->value, $claims); + } + + + public function testThrowsOnInvalidPreAuthCodeTxCodeLengthValue(): void + { + $this->expectException(CredentialOfferException::class); + $this->expectExceptionMessage("Length"); + + $claims = $this->samplePreAuthCode; + $claims['tx_code']['length'] = "123"; + $this->sut(GrantTypesEnum::PreAuthorizedCode->value, $claims); + } + + + public function testThrowsOnInvalidPreAuthCodeTxCodeDescriptionValue(): void + { + $this->expectException(CredentialOfferException::class); + $this->expectExceptionMessage("Description"); + + $claims = $this->samplePreAuthCode; + $claims['tx_code']['description'] = 123; + $this->sut(GrantTypesEnum::PreAuthorizedCode->value, $claims); + } + + + public function testThrowsOnInvalidPreAuthCodeAuthorizationServerValue(): void + { + $this->expectException(CredentialOfferException::class); + $this->expectExceptionMessage("Authorization Server"); + + $claims = $this->samplePreAuthCode; + $claims['authorization_server'] = 123; + $this->sut(GrantTypesEnum::PreAuthorizedCode->value, $claims); + } + + + public function testJsonSerialize(): void + { + $sut = $this->sut(GrantTypesEnum::PreAuthorizedCode->value, $this->samplePreAuthCode); + $this->assertSame( + [GrantTypesEnum::PreAuthorizedCode->value => $this->samplePreAuthCode], + $sut->jsonSerialize(), + ); + } +} From b78e83b942d9df104eaeb1a7e98ec04eddf3f269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 17 Nov 2025 12:14:15 +0100 Subject: [PATCH 57/66] WIP coverage --- .../Factories/CredentialOfferFactory.php | 13 +- .../CredentialOfferParametersTest.php | 59 +++++++ .../Factories/CredentialOfferFactoryTest.php | 99 ++++++++++++ .../Factories/OpenId4VciProofFactoryTest.php | 153 ++++++++++++++++++ 4 files changed, 314 insertions(+), 10 deletions(-) create mode 100644 tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferParametersTest.php create mode 100644 tests/src/VerifiableCredentials/Factories/CredentialOfferFactoryTest.php create mode 100644 tests/src/VerifiableCredentials/Factories/OpenId4VciProofFactoryTest.php diff --git a/src/VerifiableCredentials/Factories/CredentialOfferFactory.php b/src/VerifiableCredentials/Factories/CredentialOfferFactory.php index 252f44b..966176e 100644 --- a/src/VerifiableCredentials/Factories/CredentialOfferFactory.php +++ b/src/VerifiableCredentials/Factories/CredentialOfferFactory.php @@ -29,14 +29,7 @@ public function from( ?array $parameters = null, ?string $uri = null, ): CredentialOffer { - if ( - ($parameters !== null && $uri !== null) || - ($parameters === null && $uri === null) - ) { - throw new CredentialOfferException('Only one of parameters or uri must be provided.'); - } - - if ($parameters !== null) { + if ($parameters !== null && $uri === null) { $credentialIssuer = $parameters[ClaimsEnum::CredentialIssuer->value] ?? null; $credentialIssuer = $this->helpers->type()->ensureNonEmptyString( $credentialIssuer, @@ -75,12 +68,12 @@ public function from( ); } - if ($uri !== null) { + if ($uri !== null && $parameters === null) { return new CredentialOffer( uri: $uri, ); } - throw new CredentialOfferException('Invalid parameters or uri.'); + throw new CredentialOfferException('Only one of parameters or uri must be provided.'); } } diff --git a/tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferParametersTest.php b/tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferParametersTest.php new file mode 100644 index 0000000..bf89a98 --- /dev/null +++ b/tests/src/VerifiableCredentials/CredentialOffer/CredentialOfferParametersTest.php @@ -0,0 +1,59 @@ +credentialOfferGrantsBag = $this->createMock(CredentialOfferGrantsBag::class); + } + + + protected function sut( + ?string $credentialIssuer = null, + ?array $credentialConfigurationIds = null, + false|null|CredentialOfferGrantsBag $credentialOfferGrantsBag = null, + ): CredentialOfferParameters { + $credentialIssuer ??= $this->credentialIssuer; + $credentialConfigurationIds ??= $this->credentialConfigurationIds; + $credentialOfferGrantsBag = $credentialOfferGrantsBag === false ? + null : + $credentialOfferGrantsBag ?? $this->credentialOfferGrantsBag; + + return new CredentialOfferParameters( + $credentialIssuer, + $credentialConfigurationIds, + $credentialOfferGrantsBag, + ); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(CredentialOfferParameters::class, $this->sut()); + } + + + public function testCanJsonSerialize(): void + { + $this->credentialOfferGrantsBag->expects($this->once())->method('jsonSerialize'); + + $this->assertNotEmpty($this->sut()->jsonSerialize()); + } +} diff --git a/tests/src/VerifiableCredentials/Factories/CredentialOfferFactoryTest.php b/tests/src/VerifiableCredentials/Factories/CredentialOfferFactoryTest.php new file mode 100644 index 0000000..40ac4ba --- /dev/null +++ b/tests/src/VerifiableCredentials/Factories/CredentialOfferFactoryTest.php @@ -0,0 +1,99 @@ + 'https://example.com/issuer', + 'credential_configuration_ids' => ['credential-1', 'credential-2'], + 'grants' => [ + 'urn:ietf:params:oauth:grant-type:pre-authorized_code' => [ + "pre-authorized_code" => "oaKazRN8I0IbtZ0C7JuMn5", + "tx_code" => [ + "length" => 4, + "input_mode" => "numeric", + "description" => "Please provide the one-time code that was sent via e-mail", + ], + ], + ], + ]; + + + protected function setUp(): void + { + $this->helpersMock = $this->createMock(Helpers::class); + } + + + protected function sut( + ?Helpers $helpers = null, + ): CredentialOfferFactory { + $helpers ??= $this->helpersMock; + + return new CredentialOfferFactory($helpers); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(CredentialOfferFactory::class, $this->sut()); + } + + + public function testCanBuildFromParameters(): void + { + $this->assertInstanceOf( + CredentialOffer::class, + $this->sut()->from($this->sampleParameters), + ); + } + + + public function testFromThrowsForInvalidGrantData(): void + { + $this->expectException(CredentialOfferException::class); + $this->expectExceptionMessage('Grants'); + + $parameters = $this->sampleParameters; + $parameters['grants']['urn:ietf:params:oauth:grant-type:pre-authorized_code'] = 123; + $this->sut()->from($parameters); + } + + + public function testFromUri(): void + { + $this->assertInstanceOf( + CredentialOffer::class, + $this->sut()->from(uri: 'https://example.com/offer'), + ); + } + + + public function testFromThrowsIfBothParametersAndUriIsProvided(): void + { + $this->expectException(CredentialOfferException::class); + $this->expectExceptionMessage('Only one'); + + $this->sut()->from($this->sampleParameters, 'https://example.com/offer'); + } +} diff --git a/tests/src/VerifiableCredentials/Factories/OpenId4VciProofFactoryTest.php b/tests/src/VerifiableCredentials/Factories/OpenId4VciProofFactoryTest.php new file mode 100644 index 0000000..2fb1a44 --- /dev/null +++ b/tests/src/VerifiableCredentials/Factories/OpenId4VciProofFactoryTest.php @@ -0,0 +1,153 @@ + "openid4vci-proof+jwt", + "alg" => "ES256", + "jwk" => [ + "kty" => "EC", + "crv" => "P-256", + "x" => "nUWAoAv3XZith8E7i19OdaxOLYFOwM-Z2EuM02TirT4", + "y" => "HskHU8BjUi1U9Xqi7Swmj8gwAK_0xkcDjEW_71SosEY", + ], + ]; + + protected array $expiredPayload = [ + 'iat' => 1734009487, + 'nbf' => 1734009487, + 'exp' => 1734009487, + "iss" => "s6BhdRkqt3", + "aud" => "https://server.example.com", + "nonce" => "tZignsnFbp", + ]; + + protected array $validPayload; + + + protected function setUp(): void + { + $this->signatureMock = $this->createMock(Signature::class); + + $jwsMock = $this->createMock(JWS::class); + $jwsMock->method('getPayload') + ->willReturn('json-payload-string'); // Just so we have non-empty value. + $jwsMock->method('getSignature')->willReturn($this->signatureMock); + + $jwsDecoratorMock = $this->createMock(JwsDecorator::class); + $jwsDecoratorMock->method('jws')->willReturn($jwsMock); + + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock->method('fromData')->willReturn($jwsDecoratorMock); + + $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); + $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); + $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); + + $this->helpersMock = $this->createMock(Helpers::class); + $this->jsonHelperMock = $this->createMock(Helpers\Json::class); + $this->helpersMock->method('json')->willReturn($this->jsonHelperMock); + $typeHelperMock = $this->createMock(Helpers\Type::class); + $this->helpersMock->method('type')->willReturn($typeHelperMock); + + $typeHelperMock->method('ensureNonEmptyString')->willReturnArgument(0); + $typeHelperMock->method('ensureInt')->willReturnArgument(0); + + $this->claimFactoryMock = $this->createMock(ClaimFactory::class); + + $this->validPayload = $this->expiredPayload; + $this->validPayload['exp'] = time() + 3600; + } + + + protected function sut( + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, + ?JwsVerifierDecorator $jwsVerifierDecorator = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, + ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, + ?DateIntervalDecorator $dateIntervalDecorator = null, + ?Helpers $helpers = null, + ?ClaimFactory $claimFactory = null, + ): OpenId4VciProofFactory { + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; + $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; + $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; + $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; + $helpers ??= $this->helpersMock; + $claimFactory ??= $this->claimFactoryMock; + + return new OpenId4VciProofFactory( + $jwsDecoratorBuilder, + $jwsVerifierDecorator, + $jwksDecoratorFactory, + $jwsSerializerManagerDecorator, + $dateIntervalDecorator, + $helpers, + $claimFactory, + ); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(OpenId4VciProofFactory::class, $this->sut()); + } + + + public function testCanBuildFromToken(): void + { + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + + $this->assertInstanceOf( + OpenId4VciProof::class, + $this->sut()->fromToken('token'), + ); + } +} From a80aad30622f231b7290ce839fd27d1c0ad7b559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 17 Nov 2025 14:59:28 +0100 Subject: [PATCH 58/66] WIP coverage --- src/VerifiableCredentials/SdJwtVc/SdJwtVc.php | 3 +- .../Factories/OpenId4VciProofFactoryTest.php | 22 ++ .../Factories/TxCodeFactoryTest.php | 33 +++ .../SdJwtVc/Factories/SdJwtVcFactoryTest.php | 168 +++++++++++++ .../SdJwtVc/SdJwtVcTest.php | 225 ++++++++++++++++++ 5 files changed, 449 insertions(+), 2 deletions(-) create mode 100644 tests/src/VerifiableCredentials/Factories/TxCodeFactoryTest.php create mode 100644 tests/src/VerifiableCredentials/SdJwtVc/Factories/SdJwtVcFactoryTest.php create mode 100644 tests/src/VerifiableCredentials/SdJwtVc/SdJwtVcTest.php diff --git a/src/VerifiableCredentials/SdJwtVc/SdJwtVc.php b/src/VerifiableCredentials/SdJwtVc/SdJwtVc.php index 1427d3a..0e66b42 100644 --- a/src/VerifiableCredentials/SdJwtVc/SdJwtVc.php +++ b/src/VerifiableCredentials/SdJwtVc/SdJwtVc.php @@ -6,7 +6,6 @@ use SimpleSAML\OpenID\Codebooks\ClaimsEnum; use SimpleSAML\OpenID\Codebooks\JwtTypesEnum; -use SimpleSAML\OpenID\Exceptions\EntityStatementException; use SimpleSAML\OpenID\Exceptions\SdJwtVcException; use SimpleSAML\OpenID\SdJwt\Disclosure; use SimpleSAML\OpenID\SdJwt\DisclosureBag; @@ -33,7 +32,7 @@ public function getType(): string $typ = parent::getType() ?? throw new SdJwtVcException('No Type header claim found.'); if (!in_array($typ, [JwtTypesEnum::DcSdJwt->value, JwtTypesEnum::VcSdJwt->value], true)) { - throw new EntityStatementException('Invalid Type header claim.'); + throw new SdJwtVcException('Invalid Type header claim.'); } return $typ; diff --git a/tests/src/VerifiableCredentials/Factories/OpenId4VciProofFactoryTest.php b/tests/src/VerifiableCredentials/Factories/OpenId4VciProofFactoryTest.php index 2fb1a44..7c9b896 100644 --- a/tests/src/VerifiableCredentials/Factories/OpenId4VciProofFactoryTest.php +++ b/tests/src/VerifiableCredentials/Factories/OpenId4VciProofFactoryTest.php @@ -12,6 +12,7 @@ use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; @@ -67,6 +68,8 @@ final class OpenId4VciProofFactoryTest extends TestCase protected array $validPayload; + protected MockObject $jwkDecoratorMock; + protected function setUp(): void { @@ -102,6 +105,8 @@ protected function setUp(): void $this->validPayload = $this->expiredPayload; $this->validPayload['exp'] = time() + 3600; + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); } @@ -150,4 +155,21 @@ public function testCanBuildFromToken(): void $this->sut()->fromToken('token'), ); } + + + public function testCanBuildFromData(): void + { + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + + $this->assertInstanceOf( + OpenId4VciProof::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::ES256, + $this->validPayload, + $this->sampleHeader, + ), + ); + } } diff --git a/tests/src/VerifiableCredentials/Factories/TxCodeFactoryTest.php b/tests/src/VerifiableCredentials/Factories/TxCodeFactoryTest.php new file mode 100644 index 0000000..2a5aebd --- /dev/null +++ b/tests/src/VerifiableCredentials/Factories/TxCodeFactoryTest.php @@ -0,0 +1,33 @@ +assertInstanceOf( + TxCode::class, + $this->sut()->build($this->txCode, $this->description), + ); + } +} diff --git a/tests/src/VerifiableCredentials/SdJwtVc/Factories/SdJwtVcFactoryTest.php b/tests/src/VerifiableCredentials/SdJwtVc/Factories/SdJwtVcFactoryTest.php new file mode 100644 index 0000000..e65c6ce --- /dev/null +++ b/tests/src/VerifiableCredentials/SdJwtVc/Factories/SdJwtVcFactoryTest.php @@ -0,0 +1,168 @@ + 'ES256', + 'typ' => 'dc+sd-jwt', + 'kid' => 'F4VFObNusj3PHmrHxpqh4GNiuFHlfh-2s6xMJ95fLYA', + ]; + + protected array $expiredPayload = [ + 'iat' => 1734009487, + 'nbf' => 1734009487, + 'exp' => 1734009487, + 'vct' => 'https://betelgeuse.example.com/education_credential', + 'iss' => 'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ALeaf/', + ]; + + protected array $validPayload; + + + protected function setUp(): void + { + $this->signatureMock = $this->createMock(Signature::class); + + $jwsMock = $this->createMock(JWS::class); + $jwsMock->method('getPayload') + ->willReturn('json-payload-string'); // Just so we have non-empty value. + $jwsMock->method('getSignature')->willReturn($this->signatureMock); + + $jwsDecoratorMock = $this->createMock(JwsDecorator::class); + $jwsDecoratorMock->method('jws')->willReturn($jwsMock); + + $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); + $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock->method('fromData')->willReturn($jwsDecoratorMock); + + $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); + $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); + $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); + + $this->helpersMock = $this->createMock(Helpers::class); + $this->jsonHelperMock = $this->createMock(Helpers\Json::class); + $this->helpersMock->method('json')->willReturn($this->jsonHelperMock); + $typeHelperMock = $this->createMock(Helpers\Type::class); + $this->helpersMock->method('type')->willReturn($typeHelperMock); + + $typeHelperMock->method('ensureNonEmptyString')->willReturnArgument(0); + $typeHelperMock->method('ensureInt')->willReturnArgument(0); + + $this->claimFactoryMock = $this->createMock(ClaimFactory::class); + + $this->validPayload = $this->expiredPayload; + $this->validPayload['exp'] = time() + 3600; + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); + $this->disclosureBagMock = $this->createMock(DisclosureBag::class); + + $this->disclosureFactoryMock = $this->createMock(DisclosureFactory::class); + } + + + protected function sut( + ?JwsDecoratorBuilder $jwsDecoratorBuilder = null, + ?JwsVerifierDecorator $jwsVerifierDecorator = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, + ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, + ?DateIntervalDecorator $dateIntervalDecorator = null, + ?Helpers $helpers = null, + ?ClaimFactory $claimFactory = null, + ?DisclosureFactory $disclosureFactory = null, + ): SdJwtVcFactory { + $jwsDecoratorBuilder ??= $this->jwsDecoratorBuilderMock; + $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; + $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; + $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; + $helpers ??= $this->helpersMock; + $claimFactory ??= $this->claimFactoryMock; + $disclosureFactory ??= $this->disclosureFactoryMock; + + return new SdJwtVcFactory( + $jwsDecoratorBuilder, + $jwsVerifierDecorator, + $jwksDecoratorFactory, + $jwsSerializerManagerDecorator, + $dateIntervalDecorator, + $helpers, + $claimFactory, + $disclosureFactory, + ); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(SdJwtFactory::class, $this->sut()); + } + + + public function testCanBuildFromData(): void + { + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + + $this->assertInstanceOf( + SdJwtVc::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::ES256, + $this->validPayload, + $this->sampleHeader, + $this->disclosureBagMock, + ), + ); + } +} diff --git a/tests/src/VerifiableCredentials/SdJwtVc/SdJwtVcTest.php b/tests/src/VerifiableCredentials/SdJwtVc/SdJwtVcTest.php new file mode 100644 index 0000000..3c291fc --- /dev/null +++ b/tests/src/VerifiableCredentials/SdJwtVc/SdJwtVcTest.php @@ -0,0 +1,225 @@ + [ + "09vKrJMOlyTWM0sjpu_pdOBVBQ2M1y3KhpH515nXkpY", + "2rsjGbaC0ky8mT0pJrPioWTq0_daw1sX76poUlgCwbI", + "EkO8dhW0dHEJbvUHlE_VCeuC9uRELOieLZhh7XbUTtA", + "IlDzIKeiZdDwpqpK6ZfbyphFvz5FgnWa-sN6wqQXCiw", + "JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE", + "PorFbpKuVu6xymJagvkFsFXAbRoc2JGlAUA2BA4o7cI", + "TGf4oLbgwd5JQaHyKVQZU9UdGE0w5rtDsrZzfUaomLo", + "jdrTE8YcbY4EifugihiAe_BPekxJQZICeiUQwY9QqxI", + "jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4", + ], + "iss" => "https://example.com/issuer", + "iat" => 1683000000, + "exp" => 1883000000, + "vct" => "https://credentials.example.com/identity_credential", + "_sd_alg" => "sha-256", + "cnf" => [ + "jwk" => [ + "kty" => "EC", + "crv" => "P-256", + "x" => "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y" => "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ", + ], + ], + ]; + + protected array $sampleHeader = [ + 'alg' => 'ES256', + 'typ' => 'dc+sd-jwt', + 'kid' => 'F4VFObNusj3PHmrHxpqh4GNiuFHlfh-2s6xMJ95fLYA', + ]; + + protected array $validPayload; + + protected MockObject $disclosureBagMock; + + protected MockObject $kbJwtMock; + + + protected function setUp(): void + { + $this->signatureMock = $this->createMock(Signature::class); + + $jwsMock = $this->createMock(JWS::class); + $jwsMock->method('getPayload') + ->willReturn('json-payload-string'); // Just so we have non-empty value. + $jwsMock->method('getSignature')->willReturn($this->signatureMock); + + $this->jwsDecoratorMock = $this->createMock(JwsDecorator::class); + $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); + + $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); + $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); + $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); + + $this->helpersMock = $this->createMock(Helpers::class); + $this->jsonHelperMock = $this->createMock(Helpers\Json::class); + $this->helpersMock->method('json')->willReturn($this->jsonHelperMock); + $typeHelperMock = $this->createMock(Helpers\Type::class); + $this->helpersMock->method('type')->willReturn($typeHelperMock); + $this->arrHelperMock = $this->createMock(Helpers\Arr::class); + $this->helpersMock->method('arr')->willReturn($this->arrHelperMock); + + $typeHelperMock->method('ensureNonEmptyString')->willReturnArgument(0); + $typeHelperMock->method('ensureInt')->willReturnArgument(0); + $typeHelperMock->method('ensureArray')->willReturnArgument(0); + + $this->claimFactoryMock = $this->createMock(ClaimFactory::class); + + $this->validPayload = $this->expiredPayload; + $this->validPayload['exp'] = time() + 3600; + + $this->disclosureBagMock = $this->createMock(DisclosureBag::class); + $this->kbJwtMock = $this->createMock(KbJwt::class); + } + + + protected function sut( + ?JwsDecorator $jwsDecorator = null, + ?JwsVerifierDecorator $jwsVerifierDecorator = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, + ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, + ?DateIntervalDecorator $dateIntervalDecorator = null, + ?Helpers $helpers = null, + ?ClaimFactory $claimFactory = null, + DisclosureBag|false|null $disclosureBag = null, + KbJwt|false|null $kbJwt = null, + ): SdJwtVc { + $jwsDecorator ??= $this->jwsDecoratorMock; + $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; + $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; + $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; + $helpers ??= $this->helpersMock; + $claimFactory ??= $this->claimFactoryMock; + $disclosureBag = $disclosureBag === false ? null : $disclosureBag ?? $this->disclosureBagMock; + $kbJwt = $kbJwt === false ? null : $kbJwt ?? $this->kbJwtMock; + + return new SdJwtVc( + $jwsDecorator, + $jwsVerifierDecorator, + $jwksDecoratorFactory, + $jwsSerializerManagerDecorator, + $dateIntervalDecorator, + $helpers, + $claimFactory, + $disclosureBag, + $kbJwt, + ); + } + + + public function testCanCreateInstance(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $this->assertInstanceOf( + SdJwtVc::class, + $this->sut(), + ); + } + + + public function testCanCreateInstanceWithoutDisclosureBag(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $this->assertInstanceOf( + SdJwtVc::class, + $this->sut(disclosureBag: false), + ); + } + + + public function testThrowsOnInvalidTypeHeader(): void + { + $this->sampleHeader['typ'] = 'invalid-type'; + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Type'); + $this->sut(); + } + + + public function testThrowsOnNonDisclosableClaimCase(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $disclosureMock = $this->createMock(Disclosure::class); + $disclosureMock->method('getName')->willReturn('iss'); + $this->disclosureBagMock->method('all')->willReturn(['salt' => $disclosureMock]); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('not selectively disclosable'); + $this->expectExceptionMessage('iss'); + $this->sut(); + } + + + public function testThrowsOnSdClaimWhenNoDisclosures(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $this->arrHelperMock->method('containsKey')->willReturn(true); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('_Sd'); + $this->sut(); + } +} From 80069e6a5036aab4ba136d8bbe4ff280c79ff125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 19 Nov 2025 11:00:11 +0100 Subject: [PATCH 59/66] WIP coverage --- .../VcDataModel/Claims/VcClaimValue.php | 35 ++++- .../AbstractIdentifiedTypedClaimValueTest.php | 82 ++++++++++ .../Claims/AbstractTypedClaimValueTest.php | 74 +++++++++ .../VcDataModel/Claims/TypeClaimValueTest.php | 49 ++++++ .../Claims/VcAtContextClaimValueTest.php | 55 +++++++ .../VcDataModel/Claims/VcClaimValueTest.php | 142 ++++++++++++++++++ 6 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValueTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValueTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValueTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValueTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValueTest.php diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php index 02632ef..e8d8578 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValue.php @@ -104,6 +104,24 @@ public function getCredentialSchema(): ?VcCredentialSchemaClaimBag } + public function getRefreshService(): ?VcRefreshServiceClaimBag + { + return $this->refreshServiceClaimBag; + } + + + public function getTermsOfUse(): ?VcTermsOfUseClaimBag + { + return $this->termsOfUseClaimBag; + } + + + public function getEvidence(): ?VcEvidenceClaimBag + { + return $this->evidenceClaimBag; + } + + public function getName(): string { return ClaimsEnum::Vc->value; @@ -115,7 +133,20 @@ public function getName(): string */ public function getValue(): array { - // TODO: Implement getValue() method. - return []; + return array_filter([ + ClaimsEnum::AtContext->value => $this->getAtContext()->jsonSerialize(), + ClaimsEnum::Id->value => $this->getId(), + ClaimsEnum::Type->value => $this->getType()->jsonSerialize(), + ClaimsEnum::Issuer->value => $this->getIssuer()->jsonSerialize(), + ClaimsEnum::Issuance_Date->value => $this->getIssuanceDate()->format(\DateTimeInterface::RFC3339), + ClaimsEnum::Credential_Subject->value => $this->getCredentialSubject()->jsonSerialize(), + ClaimsEnum::Proof->value => $this->getProof()?->jsonSerialize(), + ClaimsEnum::Expiration_Date->value => $this->getExpirationDate()?->format(\DateTimeInterface::RFC3339), + ClaimsEnum::Credential_Status->value => $this->getCredentialStatus()?->jsonSerialize(), + ClaimsEnum::Credential_Schema->value => $this->getCredentialSchema()?->jsonSerialize(), + ClaimsEnum::Refresh_Service->value => $this->getRefreshService()?->jsonSerialize(), + ClaimsEnum::Terms_Of_Use->value => $this->getTermsOfUse()?->jsonSerialize(), + ClaimsEnum::Evidence->value => $this->getEvidence()?->jsonSerialize(), + ]); } } diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValueTest.php new file mode 100644 index 0000000..6e095d1 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/AbstractIdentifiedTypedClaimValueTest.php @@ -0,0 +1,82 @@ +typeClaimValueMock = $this->createMock(TypeClaimValue::class); + } + + + protected function sut( + ?string $id = null, + ?TypeClaimValue $typeClaimValue = null, + ?array $otherClaims = null, + ?string $name = null, + ): AbstractIdentifiedTypedClaimValue { + $id ??= $this->id; + $typeClaimValue ??= $this->typeClaimValueMock; + $otherClaims ??= $this->otherClaims; + $name ??= $this->name; + + return new class ( + $id, + $typeClaimValue, + $otherClaims, + $name, + ) extends AbstractIdentifiedTypedClaimValue { + public function __construct( + string $id, + TypeClaimValue $typeClaimValue, + array $otherClaims, + protected readonly string $name, + ) { + parent::__construct($id, $typeClaimValue, $otherClaims); + } + + + public function getName(): string + { + return $this->name; + } + }; + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(AbstractIdentifiedTypedClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame($this->id, $sut->getId()); + $this->assertSame($this->typeClaimValueMock, $sut->getType()); + $this->assertSame($this->id, $sut->getKey('id')); + $this->assertSame($this->name, $sut->getName()); + $this->assertArrayHasKey('id', $sut->getValue()); + $this->assertArrayHasKey('id', $sut->jsonSerialize()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValueTest.php new file mode 100644 index 0000000..d76fd16 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/AbstractTypedClaimValueTest.php @@ -0,0 +1,74 @@ +typeClaimValueMock = $this->createMock(TypeClaimValue::class); + } + + + protected function sut( + ?TypeClaimValue $typeClaimValue = null, + ?array $otherClaims = null, + ?string $name = null, + ): AbstractTypedClaimValue { + $typeClaimValue ??= $this->typeClaimValueMock; + $otherClaims ??= $this->otherClaims; + $name ??= $this->name; + + return new class ($typeClaimValue, $otherClaims, $name) extends AbstractTypedClaimValue { + public function __construct( + TypeClaimValue $typeClaimValue, + array $otherClaims, + protected readonly string $name, + ) { + parent::__construct($typeClaimValue, $otherClaims); + } + + + public function getName(): string + { + return $this->name; + } + }; + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(AbstractTypedClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $this->typeClaimValueMock->expects($this->once()) + ->method('jsonSerialize') + ->willReturn('type'); + + $sut = $this->sut(); + $this->assertSame($this->typeClaimValueMock, $sut->getType()); + $this->assertSame('type', $sut->getKey('type')); + $this->assertSame($this->name, $sut->getName()); + $this->assertArrayHasKey('type', $sut->getValue()); + $this->assertArrayHasKey('type', $sut->jsonSerialize()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValueTest.php new file mode 100644 index 0000000..2e61a0f --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/TypeClaimValueTest.php @@ -0,0 +1,49 @@ +types; + + return new TypeClaimValue($types); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(TypeClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame('type', $sut->getName()); + $this->assertSame($this->types, $sut->getValue()); + $this->assertSame($this->types, $sut->jsonSerialize()); + $this->assertTrue($sut->has('type')); + } + + + public function testJsonSerializeCanReturnStringOrArray(): void + { + $sut = $this->sut(); + $this->assertSame($this->types, $sut->jsonSerialize()); + + $sut = $this->sut(['type']); + $this->assertSame('type', $sut->jsonSerialize()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValueTest.php new file mode 100644 index 0000000..4d90361 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValueTest.php @@ -0,0 +1,55 @@ +value; + + protected array $otherContexts = []; + + + protected function sut( + ?string $baseContext = null, + ?array $otherContexts = null, + ): VcAtContextClaimValue { + $baseContext ??= $this->baseContext; + $otherContexts ??= $this->otherContexts; + + return new VcAtContextClaimValue($baseContext, $otherContexts); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcAtContextClaimValue::class, $this->sut()); + } + + + public function testThrowsOnInvalidBaseContext(): void + { + $this->expectException(VcDataModelException::class); + $this->expectExceptionMessage('context'); + + $this->sut('invalid'); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertContains($this->baseContext, $sut->jsonSerialize()); + $this->assertSame($this->baseContext, $sut->getBaseContext()); + $this->assertSame($this->otherContexts, $sut->getOtherContexts()); + $this->assertSame('AtContext', $sut->getName()); + $this->assertContains($this->baseContext, $sut->getValue()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValueTest.php new file mode 100644 index 0000000..e3be149 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcClaimValueTest.php @@ -0,0 +1,142 @@ +vcAtContextClaimValueMock = $this->createMock(VcAtContextClaimValue::class); + $this->typeClaimValueMock = $this->createMock(TypeClaimValue::class); + $this->vcCredentialSubjectClaimBagMock = $this->createMock(VcCredentialSubjectClaimBag::class); + $this->issuerClaimValueMock = $this->createMock(VcIssuerClaimValue::class); + $this->issuanceDateMock = $this->createMock(\DateTimeImmutable::class); + $this->proofClaimValueMock = $this->createMock(VcProofClaimValue::class); + $this->expirationDateMock = $this->createMock(\DateTimeImmutable::class); + $this->credentialStatusClaimValueMock = $this->createMock(VcCredentialStatusClaimValue::class); + $this->credentialSchemaClaimBagMock = $this->createMock(VcCredentialSchemaClaimBag::class); + $this->refreshServiceClaimBagMock = $this->createMock(VcRefreshServiceClaimBag::class); + $this->termsOfUserClaimBagMock = $this->createMock(VcTermsOfUseClaimBag::class); + $this->evidenceClaimBagMock = $this->createMock(VcEvidenceClaimBag::class); + } + + + protected function sut( + ?VcAtContextClaimValue $vcAtContextClaimValue = null, + ?string $id = null, + ?TypeClaimValue $typeClaimValue = null, + ?VcCredentialSubjectClaimBag $vcCredentialSubjectClaimBag = null, + ?VcIssuerClaimValue $vcIssuerClaimValue = null, + ?\DateTimeImmutable $issuanceDate = null, + ?VcProofClaimValue $proofClaimValue = null, + ?\DateTimeImmutable $expirationDate = null, + ?VcCredentialStatusClaimValue $credentialStatusClaimValue = null, + ?VcCredentialSchemaClaimBag $credentialSchemaClaimBag = null, + ?VcRefreshServiceClaimBag $refreshServiceClaimBag = null, + ?VcTermsOfUseClaimBag $termsOfUserClaimBag = null, + ?VcEvidenceClaimBag $evidenceClaimBag = null, + ): VcClaimValue { + $vcAtContextClaimValue ??= $this->vcAtContextClaimValueMock; + $id ??= $this->id; + $typeClaimValue ??= $this->typeClaimValueMock; + $vcCredentialSubjectClaimBag ??= $this->vcCredentialSubjectClaimBagMock; + $vcIssuerClaimValue ??= $this->issuerClaimValueMock; + $issuanceDate ??= $this->issuanceDateMock; + $proofClaimValue ??= $this->proofClaimValueMock; + $expirationDate ??= $this->expirationDateMock; + $credentialStatusClaimValue ??= $this->credentialStatusClaimValueMock; + $credentialSchemaClaimBag ??= $this->credentialSchemaClaimBagMock; + $refreshServiceClaimBag ??= $this->refreshServiceClaimBagMock; + $termsOfUserClaimBag ??= $this->termsOfUserClaimBagMock; + $evidenceClaimBag ??= $this->evidenceClaimBagMock; + + return new VcClaimValue( + $vcAtContextClaimValue, + $id, + $typeClaimValue, + $vcCredentialSubjectClaimBag, + $vcIssuerClaimValue, + $issuanceDate, + $proofClaimValue, + $expirationDate, + $credentialStatusClaimValue, + $credentialSchemaClaimBag, + $refreshServiceClaimBag, + $termsOfUserClaimBag, + $evidenceClaimBag, + ); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame($this->vcAtContextClaimValueMock, $sut->getAtContext()); + $this->assertSame($this->id, $sut->getId()); + $this->assertSame($this->typeClaimValueMock, $sut->getType()); + $this->assertSame($this->vcCredentialSubjectClaimBagMock, $sut->getCredentialSubject()); + $this->assertSame($this->issuerClaimValueMock, $sut->getIssuer()); + $this->assertSame($this->issuanceDateMock, $sut->getIssuanceDate()); + $this->assertSame($this->proofClaimValueMock, $sut->getProof()); + $this->assertSame($this->expirationDateMock, $sut->getExpirationDate()); + $this->assertSame($this->credentialStatusClaimValueMock, $sut->getCredentialStatus()); + $this->assertSame($this->credentialSchemaClaimBagMock, $sut->getCredentialSchema()); + $this->assertSame($this->refreshServiceClaimBagMock, $sut->getRefreshService()); + $this->assertSame($this->termsOfUserClaimBagMock, $sut->getTermsOfUse()); + $this->assertSame($this->evidenceClaimBagMock, $sut->getEvidence()); + $this->assertSame('vc', $sut->getName()); + + $this->assertArrayHasKey('id', $sut->getValue()); + $this->assertArrayHasKey('id', $sut->jsonSerialize()); + } +} From 81314c48f11e3f53e75b02cc19d22be6849a45b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 19 Nov 2025 12:00:55 +0100 Subject: [PATCH 60/66] WIP coverage --- .../Claims/VcAtContextClaimValue.php | 2 +- .../Claims/VcAtContextClaimValueTest.php | 2 +- .../Claims/VcCredentialSchemaClaimBagTest.php | 53 ++++++++++++++++++ .../VcCredentialSchemaClaimValueTest.php | 53 ++++++++++++++++++ .../VcCredentialStatusClaimValueTest.php | 50 +++++++++++++++++ .../VcCredentialSubjectClaimBagTest.php | 53 ++++++++++++++++++ .../VcCredentialSubjectClaimValueTest.php | 39 ++++++++++++++ .../Claims/VcEvidenceClaimBagTest.php | 51 ++++++++++++++++++ .../Claims/VcEvidenceClaimValueTest.php | 45 ++++++++++++++++ .../Claims/VcIssuerClaimValueTest.php | 45 ++++++++++++++++ .../Claims/VcProofClaimValueTest.php | 49 +++++++++++++++++ .../Claims/VcRefreshServiceClaimBagTest.php | 52 ++++++++++++++++++ .../Claims/VcRefreshServiceClaimValueTest.php | 54 +++++++++++++++++++ .../Claims/VcTermsOfUseClaimBagTest.php | 51 ++++++++++++++++++ .../Claims/VcTermsOfUseClaimValueTest.php | 49 +++++++++++++++++ 15 files changed, 646 insertions(+), 2 deletions(-) create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBagTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimValueTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValueTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBagTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValueTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBagTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimValueTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValueTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValueTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBagTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimValueTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBagTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimValueTest.php diff --git a/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php b/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php index 6acda38..195d08a 100644 --- a/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php +++ b/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValue.php @@ -55,7 +55,7 @@ public function getOtherContexts(): array public function getName(): string { - return ClaimsEnum::AtContext->name; + return ClaimsEnum::AtContext->value; } diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValueTest.php index 4d90361..640fe7c 100644 --- a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValueTest.php +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcAtContextClaimValueTest.php @@ -49,7 +49,7 @@ public function testCanGetProperties(): void $this->assertContains($this->baseContext, $sut->jsonSerialize()); $this->assertSame($this->baseContext, $sut->getBaseContext()); $this->assertSame($this->otherContexts, $sut->getOtherContexts()); - $this->assertSame('AtContext', $sut->getName()); + $this->assertSame('@context', $sut->getName()); $this->assertContains($this->baseContext, $sut->getValue()); } } diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBagTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBagTest.php new file mode 100644 index 0000000..ffb067c --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimBagTest.php @@ -0,0 +1,53 @@ +vcCredentialSchemaClaimValueMock = $this->createMock(VcCredentialSchemaClaimValue::class); + } + + + protected function sut( + ?VcCredentialSchemaClaimValue $vcCredentialStatusClaimValue = null, + VcCredentialStatusClaimValue ...$vcCredentialStatusClaimValues, + ): VcCredentialSchemaClaimBag { + $vcCredentialStatusClaimValue ??= $this->vcCredentialSchemaClaimValueMock; + + return new VcCredentialSchemaClaimBag($vcCredentialStatusClaimValue, ...$vcCredentialStatusClaimValues); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcCredentialSchemaClaimBag::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $this->vcCredentialSchemaClaimValueMock->expects($this->once()) + ->method('jsonSerialize') + ->willReturn(['schema']); + + $sut = $this->sut(); + $this->assertSame('credentialSchema', $sut->getName()); + $this->assertSame([$this->vcCredentialSchemaClaimValueMock], $sut->getValue()); + $this->assertSame([['schema']], $sut->jsonSerialize()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimValueTest.php new file mode 100644 index 0000000..029b104 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSchemaClaimValueTest.php @@ -0,0 +1,53 @@ +typeClaimValueMock = $this->createMock(TypeClaimValue::class); + } + + + protected function sut( + ?string $id = null, + ?TypeClaimValue $typeClaimValue = null, + ): VcCredentialSchemaClaimValue { + $id ??= $this->id; + $typeClaimValue ??= $this->typeClaimValueMock; + + return new VcCredentialSchemaClaimValue( + $id, + $typeClaimValue, + ); + } + + + public function testCanCrateInstance(): void + { + $this->assertInstanceOf(VcCredentialSchemaClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame($this->id, $sut->getId()); + $this->assertSame($this->typeClaimValueMock, $sut->getType()); + $this->assertSame('credentialSchema', $sut->getName()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValueTest.php new file mode 100644 index 0000000..a5bc483 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialStatusClaimValueTest.php @@ -0,0 +1,50 @@ +typeClaimValueMock = $this->createMock(TypeClaimValue::class); + } + + + protected function sut( + ?string $id = null, + ?TypeClaimValue $typeClaimValue = null, + ): VcCredentialStatusClaimValue { + $id ??= $this->id; + $typeClaimValue ??= $this->typeClaimValueMock; + + return new VcCredentialStatusClaimValue($id, $typeClaimValue); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcCredentialStatusClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame($this->id, $sut->getId()); + $this->assertSame($this->typeClaimValueMock, $sut->getType()); + $this->assertSame('credentialStatus', $sut->getName()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBagTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBagTest.php new file mode 100644 index 0000000..173ee42 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimBagTest.php @@ -0,0 +1,53 @@ +vcCredentialSubjectClaimValueMock = $this->createMock(VcCredentialSubjectClaimValue::class); + } + + + protected function sut( + ?VcCredentialSubjectClaimValue $vcCredentialSubjectClaimValue = null, + VcCredentialSubjectClaimValue ...$vcCredentialSubjectClaimValues, + ): VcCredentialSubjectClaimBag { + $vcCredentialSubjectClaimValue ??= $this->vcCredentialSubjectClaimValueMock; + + return new VcCredentialSubjectClaimBag($vcCredentialSubjectClaimValue, ...$vcCredentialSubjectClaimValues); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcCredentialSubjectClaimBag::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $this->vcCredentialSubjectClaimValueMock->expects($this->once()) + ->method('jsonSerialize') + ->willReturn(['credentialSubject']); + + $sut = $this->sut(); + + + $this->assertSame('credentialSubject', $sut->getName()); + $this->assertSame([$this->vcCredentialSubjectClaimValueMock], $sut->getValue()); + $this->assertSame([['credentialSubject']], $sut->jsonSerialize()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValueTest.php new file mode 100644 index 0000000..4571e16 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcCredentialSubjectClaimValueTest.php @@ -0,0 +1,39 @@ + 'subject']; + + + protected function sut( + ?array $data = null, + ): VcCredentialSubjectClaimValue { + $data ??= $this->data; + + return new VcCredentialSubjectClaimValue($data); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcCredentialSubjectClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame($this->data, $sut->jsonSerialize()); + $this->assertSame($this->data, $sut->getValue()); + $this->assertSame('subject', $sut->get('id')); + $this->assertSame('credentialSubject', $sut->getName()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBagTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBagTest.php new file mode 100644 index 0000000..d1e7208 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimBagTest.php @@ -0,0 +1,51 @@ +vcEvidenceClaimValueMock = $this->createMock(VcEvidenceClaimValue::class); + } + + + protected function sut( + ?VcEvidenceClaimValue $vcEvidenceClaimValue = null, + VcEvidenceClaimValue ...$vcEvidenceClaimValues, + ): VcEvidenceClaimBag { + $vcEvidenceClaimValue ??= $this->vcEvidenceClaimValueMock; + + return new VcEvidenceClaimBag($vcEvidenceClaimValue, ...$vcEvidenceClaimValues); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcEvidenceClaimBag::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $this->vcEvidenceClaimValueMock->expects($this->once()) + ->method('jsonSerialize') + ->willReturn(['evidence']); + + $sut = $this->sut(); + $this->assertSame('evidence', $sut->getName()); + $this->assertSame([$this->vcEvidenceClaimValueMock], $sut->getValue()); + $this->assertSame([['evidence']], $sut->jsonSerialize()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimValueTest.php new file mode 100644 index 0000000..a274add --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcEvidenceClaimValueTest.php @@ -0,0 +1,45 @@ +typeClaimValue = $this->createMock(TypeClaimValue::class); + } + + + protected function sut( + ?TypeClaimValue $typeClaimValue = null, + ): VcEvidenceClaimValue { + $typeClaimValue ??= $this->typeClaimValue; + + return new VcEvidenceClaimValue($typeClaimValue); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcEvidenceClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame($this->typeClaimValue, $sut->getType()); + $this->assertSame('evidence', $sut->getName()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValueTest.php new file mode 100644 index 0000000..21a478d --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcIssuerClaimValueTest.php @@ -0,0 +1,45 @@ +id; + $otherClaims ??= $this->otherClaims; + + return new VcIssuerClaimValue($id, $otherClaims); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcIssuerClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + + $this->assertSame($this->id, $sut->getId()); + $this->assertSame('id', $sut->getKey('id')); + $this->assertSame('issuer', $sut->getName()); + $this->assertSame(['id' => 'id'], $sut->getValue()); + $this->assertSame(['id' => 'id'], $sut->jsonSerialize()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValueTest.php new file mode 100644 index 0000000..bf5da95 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcProofClaimValueTest.php @@ -0,0 +1,49 @@ +typeClaimValueMock = $this->createMock(TypeClaimValue::class); + } + + + protected function sut( + ?TypeClaimValue $typeClaimValue = null, + ?array $otherClaims = null, + ): VcProofClaimValue { + $typeClaimValue ??= $this->typeClaimValueMock; + $otherClaims ??= $this->otherClaims; + + return new VcProofClaimValue($typeClaimValue, $otherClaims); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcProofClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame($this->typeClaimValueMock, $sut->getType()); + $this->assertSame('proof', $sut->getName()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBagTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBagTest.php new file mode 100644 index 0000000..17e649f --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimBagTest.php @@ -0,0 +1,52 @@ +vcRefreshServiceClaimValueMock = $this->createMock(VcRefreshServiceClaimValue::class); + } + + + protected function sut( + ?VcRefreshServiceClaimValue $vcRefreshServiceClaimValue = null, + VcRefreshServiceClaimValue ...$vcRefreshServiceClaimValues, + ): \SimpleSAML\OpenID\VerifiableCredentials\VcDataModel\Claims\VcRefreshServiceClaimBag { + $vcRefreshServiceClaimValue ??= $this->vcRefreshServiceClaimValueMock; + + return new VcRefreshServiceClaimBag($vcRefreshServiceClaimValue, ...$vcRefreshServiceClaimValues); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcRefreshServiceClaimBag::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $this->vcRefreshServiceClaimValueMock->expects($this->once()) + ->method('jsonSerialize') + ->willReturn(['refreshService']); + + $sut = $this->sut(); + $this->assertSame([['refreshService']], $sut->jsonSerialize()); + $this->assertSame([$this->vcRefreshServiceClaimValueMock], $sut->getValue()); + $this->assertSame('refreshService', $sut->getName()); + ; + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimValueTest.php new file mode 100644 index 0000000..cf5c8ae --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcRefreshServiceClaimValueTest.php @@ -0,0 +1,54 @@ +typeClaimValue = $this->createMock(TypeClaimValue::class); + } + + + protected function sut( + ?string $id = null, + ?TypeClaimValue $typeClaimValue = null, + ?array $otherClaims = null, + ): VcRefreshServiceClaimValue { + $id ??= $this->id; + $typeClaimValue ??= $this->typeClaimValue; + $otherClaims ??= $this->otherClaims; + + return new VcRefreshServiceClaimValue($id, $typeClaimValue, $otherClaims); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcRefreshServiceClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame($this->id, $sut->getId()); + $this->assertSame($this->typeClaimValue, $sut->getType()); + $this->assertSame('refreshService', $sut->getName()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBagTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBagTest.php new file mode 100644 index 0000000..54d3062 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimBagTest.php @@ -0,0 +1,51 @@ +vcTermsOfUseClaimValueMock = $this->createMock(VcTermsOfUseClaimValue::class); + } + + + protected function sut( + ?VcTermsOfUseClaimValue $vcTermsOfUseClaimValue = null, + VcTermsOfUseClaimValue ...$vcTermsOfUseClaimValues, + ): VcTermsOfUseClaimBag { + $vcTermsOfUseClaimValue ??= $this->vcTermsOfUseClaimValueMock; + + return new VcTermsOfUseClaimBag($vcTermsOfUseClaimValue, ...$vcTermsOfUseClaimValues); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcTermsOfUseClaimBag::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $this->vcTermsOfUseClaimValueMock->expects($this->once()) + ->method('jsonSerialize') + ->willReturn(['termsOfUse']); + + $sut = $this->sut(); + $this->assertSame('termsOfUse', $sut->getName()); + $this->assertSame([$this->vcTermsOfUseClaimValueMock], $sut->getValue()); + $this->assertSame([['termsOfUse']], $sut->jsonSerialize()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimValueTest.php b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimValueTest.php new file mode 100644 index 0000000..e794c09 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Claims/VcTermsOfUseClaimValueTest.php @@ -0,0 +1,49 @@ +typeClaimValueMock = $this->createMock(TypeClaimValue::class); + } + + + protected function sut( + ?TypeClaimValue $typeClaimValue = null, + ?array $otherClaims = null, + ): VcTermsOfUseClaimValue { + $typeClaimValue ??= $this->typeClaimValueMock; + $otherClaims ??= $this->otherClaims; + + return new VcTermsOfUseClaimValue($typeClaimValue, $otherClaims); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcTermsOfUseClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame($this->typeClaimValueMock, $sut->getType()); + $this->assertSame('termsOfUse', $sut->getName()); + } +} From 7c4256033c09e742a7407b0c215439a9ddeffac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 19 Nov 2025 16:57:18 +0100 Subject: [PATCH 61/66] Initial VCI coverage --- src/VerifiableCredentials/OpenId4VciProof.php | 1 + .../Factories/VcDataModelClaimFactory.php | 10 +- tests/src/DidTest.php | 41 ++ tests/src/HelpersTest.php | 8 + tests/src/JwkTest.php | 33 + .../CredentialOfferTest.php | 63 ++ .../OpenId4VciProofTest.php | 135 ++++ .../src/VerifiableCredentials/TxCodeTest.php | 53 ++ .../Factories/JwtVcJsonFactoryTest.php | 24 + .../Factories/VcDataModelClaimFactoryTest.php | 265 ++++++++ .../VcDataModel/JwtVcJsonTest.php | 604 ++++++++++++++++++ tests/src/VerifiableCredentialsTest.php | 224 +++++++ 12 files changed, 1453 insertions(+), 8 deletions(-) create mode 100644 tests/src/DidTest.php create mode 100644 tests/src/JwkTest.php create mode 100644 tests/src/VerifiableCredentials/CredentialOfferTest.php create mode 100644 tests/src/VerifiableCredentials/OpenId4VciProofTest.php create mode 100644 tests/src/VerifiableCredentials/TxCodeTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactoryTest.php create mode 100644 tests/src/VerifiableCredentials/VcDataModel/JwtVcJsonTest.php create mode 100644 tests/src/VerifiableCredentialsTest.php diff --git a/src/VerifiableCredentials/OpenId4VciProof.php b/src/VerifiableCredentials/OpenId4VciProof.php index 2d9ff40..5a7d08c 100644 --- a/src/VerifiableCredentials/OpenId4VciProof.php +++ b/src/VerifiableCredentials/OpenId4VciProof.php @@ -135,6 +135,7 @@ protected function validate(): void $this->getAudience(...), $this->getIssuedAt(...), $this->getExpirationTime(...), + $this->getNonce(...), ); } } diff --git a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php index c7c3f49..372635e 100644 --- a/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php +++ b/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactory.php @@ -137,14 +137,8 @@ public function buildVcCredentialSubjectClaimBag( // If we have the 'sub' claim in JWT, we must use it as the credentialSubject ID value. However, we can't do // that if we have more than one credentialSubject. - if (is_string($subClaimValue)) { - if ($data === []) { - $vcCredentialSubjectClaimValueData[ClaimsEnum::Id->value] = $subClaimValue; - } else { - throw new VcDataModelException( - 'Refusing to set credentialSubject ID claim value for multiple subjects.', - ); - } + if (is_string($subClaimValue) && $data === []) { + $vcCredentialSubjectClaimValueData[ClaimsEnum::Id->value] = $subClaimValue; } $vcCredentialSubjectClaimValue = $this->buildVcCredentialSubjectClaimValue($vcCredentialSubjectClaimValueData); diff --git a/tests/src/DidTest.php b/tests/src/DidTest.php new file mode 100644 index 0000000..802fa97 --- /dev/null +++ b/tests/src/DidTest.php @@ -0,0 +1,41 @@ +assertInstanceOf(Did::class, $this->sut()); + } + + + public function testCanBuildTools(): void + { + $this->assertInstanceOf( + DidKeyJwkResolver::class, + $this->sut()->didKeyResolver(), + ); + + $this->assertInstanceOf( + Helpers::class, + $this->sut()->helpers(), + ); + } +} diff --git a/tests/src/HelpersTest.php b/tests/src/HelpersTest.php index 38cad5d..64bc9c1 100644 --- a/tests/src/HelpersTest.php +++ b/tests/src/HelpersTest.php @@ -9,7 +9,11 @@ use PHPUnit\Framework\TestCase; use SimpleSAML\OpenID\Helpers; use SimpleSAML\OpenID\Helpers\Arr; +use SimpleSAML\OpenID\Helpers\Base64Url; +use SimpleSAML\OpenID\Helpers\DateTime; +use SimpleSAML\OpenID\Helpers\Hash; use SimpleSAML\OpenID\Helpers\Json; +use SimpleSAML\OpenID\Helpers\Random; use SimpleSAML\OpenID\Helpers\Type; use SimpleSAML\OpenID\Helpers\Url; @@ -40,5 +44,9 @@ public function testCanBuildTools(): void $this->assertInstanceOf(Json::class, $sut->json()); $this->assertInstanceOf(Arr::class, $sut->arr()); $this->assertInstanceOf(Type::class, $sut->type()); + $this->assertInstanceOf(DateTime::class, $sut->dateTime()); + $this->assertInstanceOf(Base64Url::class, $sut->base64Url()); + $this->assertInstanceOf(Hash::class, $sut->hash()); + $this->assertInstanceOf(Random::class, $sut->random()); } } diff --git a/tests/src/JwkTest.php b/tests/src/JwkTest.php new file mode 100644 index 0000000..82a2d1a --- /dev/null +++ b/tests/src/JwkTest.php @@ -0,0 +1,33 @@ +assertInstanceOf(Jwk::class, $this->sut()); + } + + + public function testCanBuildTools(): void + { + $this->assertInstanceOf( + JwkDecoratorFactory::class, + $this->sut()->jwkDecoratorFactory(), + ); + } +} diff --git a/tests/src/VerifiableCredentials/CredentialOfferTest.php b/tests/src/VerifiableCredentials/CredentialOfferTest.php new file mode 100644 index 0000000..0612ea5 --- /dev/null +++ b/tests/src/VerifiableCredentials/CredentialOfferTest.php @@ -0,0 +1,63 @@ +credentialOfferParametersMock = $this->createMock(CredentialOfferParameters::class); + } + + + protected function sut( + CredentialOfferParameters|false|null $credentialOfferParameters = null, + string|false|null $uri = null, + ): CredentialOffer { + $credentialOfferParameters = $credentialOfferParameters === false ? + null : + $credentialOfferParameters ?? $this->credentialOfferParametersMock; + + $uri = $credentialOfferParameters === null ? + ($uri === false ? null : $uri ?? $this->uri) : + null; + + return new CredentialOffer($credentialOfferParameters, $uri); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(CredentialOffer::class, $this->sut()); + } + + + public function testCanJsonSerialize(): void + { + $this->assertIsArray($this->sut($this->credentialOfferParametersMock)->jsonSerialize()); + $this->assertIsString($this->sut(false, uri: $this->uri)->jsonSerialize()); + } + + + public function testThrowsIfNoParametersAndNoUri(): void + { + $this->expectException(CredentialOfferException::class); + $this->expectExceptionMessage('Invalid'); + + $this->sut(false, false)->jsonSerialize(); + } +} diff --git a/tests/src/VerifiableCredentials/OpenId4VciProofTest.php b/tests/src/VerifiableCredentials/OpenId4VciProofTest.php new file mode 100644 index 0000000..e6c3d55 --- /dev/null +++ b/tests/src/VerifiableCredentials/OpenId4VciProofTest.php @@ -0,0 +1,135 @@ + "s6BhdRkqt3", + "aud" => "https://credential-issuer.example.com", + "iat" => 1701960444, + "nonce" => "LarRGSbmUPYtRYO6BQ4yn8", + ]; + + protected array $sampleHeader = [ + 'alg' => 'ES256', + 'typ' => 'openid4vci-proof+jwt', + 'kid' => 'F4VFObNusj3PHmrHxpqh4GNiuFHlfh-2s6xMJ95fLYA', + ]; + + protected array $validPayload; + + + protected function setUp(): void + { + $this->signatureMock = $this->createMock(Signature::class); + + $jwsMock = $this->createMock(JWS::class); + $jwsMock->method('getPayload') + ->willReturn('json-payload-string'); // Just so we have non-empty value. + $jwsMock->method('getSignature')->willReturn($this->signatureMock); + + $this->jwsDecoratorMock = $this->createMock(JwsDecorator::class); + $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); + + $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); + $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); + $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); + + $this->helpersMock = $this->createMock(Helpers::class); + $this->jsonHelperMock = $this->createMock(Helpers\Json::class); + $this->helpersMock->method('json')->willReturn($this->jsonHelperMock); + $typeHelperMock = $this->createMock(Helpers\Type::class); + $this->helpersMock->method('type')->willReturn($typeHelperMock); + $arrHelperMock = $this->createMock(Helpers\Arr::class); + $this->helpersMock->method('arr')->willReturn($arrHelperMock); + + $typeHelperMock->method('ensureNonEmptyString')->willReturnArgument(0); + $typeHelperMock->method('ensureInt')->willReturnArgument(0); + $typeHelperMock->method('ensureArray')->willReturnArgument(0); + + $this->claimFactoryMock = $this->createMock(ClaimFactory::class); + + $this->validPayload = $this->expiredPayload; + $this->validPayload['exp'] = time() + 3600; + } + + + protected function sut( + ?JwsDecorator $jwsDecorator = null, + ?JwsVerifierDecorator $jwsVerifierDecorator = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, + ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, + ?DateIntervalDecorator $dateIntervalDecorator = null, + ?Helpers $helpers = null, + ?ClaimFactory $claimFactory = null, + ): OpenId4VciProof { + $jwsDecorator ??= $this->jwsDecoratorMock; + $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; + $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; + $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; + $helpers ??= $this->helpersMock; + $claimFactory ??= $this->claimFactoryMock; + + return new OpenId4VciProof( + $jwsDecorator, + $jwsVerifierDecorator, + $jwksDecoratorFactory, + $jwsSerializerManagerDecorator, + $dateIntervalDecorator, + $helpers, + $claimFactory, + ); + } + + + public function testCanCreateInstance(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $this->assertInstanceOf( + OpenId4VciProof::class, + $this->sut(), + ); + } +} diff --git a/tests/src/VerifiableCredentials/TxCodeTest.php b/tests/src/VerifiableCredentials/TxCodeTest.php new file mode 100644 index 0000000..bc207df --- /dev/null +++ b/tests/src/VerifiableCredentials/TxCodeTest.php @@ -0,0 +1,53 @@ +txCode; + $description ??= $this->description; + + return new TxCode($txCode, $description); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(TxCode::class, $this->sut()); + } + + + public function testThrowsForNegativeCode(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->sut(-1); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame($this->txCode, $sut->getCode()); + $this->assertSame($this->txCode, $sut->getCodeAsString()); + $this->assertSame($this->description, $sut->getDescription()); + $this->assertSame(TxCodeInputModeEnum::Text, $sut->getInputMode()); + $this->assertSame(strlen((string) $this->txCode), $sut->getLength()); + $this->assertArrayHasKey('input_mode', $sut->jsonSerialize()); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php b/tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php index 306d4a1..3399a99 100644 --- a/tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php +++ b/tests/src/VerifiableCredentials/VcDataModel/Factories/JwtVcJsonFactoryTest.php @@ -10,10 +10,12 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Factories\ClaimFactory; use SimpleSAML\OpenID\Helpers; use SimpleSAML\OpenID\Helpers\Arr; +use SimpleSAML\OpenID\Jwk\JwkDecorator; use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory; use SimpleSAML\OpenID\Jws\JwsDecorator; use SimpleSAML\OpenID\Jws\JwsDecoratorBuilder; @@ -80,6 +82,8 @@ final class JwtVcJsonFactoryTest extends TestCase protected array $validPayload; + protected MockObject $jwkDecoratorMock; + protected function setUp(): void { @@ -95,6 +99,7 @@ protected function setUp(): void $this->jwsDecoratorBuilderMock = $this->createMock(JwsDecoratorBuilder::class); $this->jwsDecoratorBuilderMock->method('fromToken')->willReturn($jwsDecoratorMock); + $this->jwsDecoratorBuilderMock->method('fromData')->willReturn($jwsDecoratorMock); $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); @@ -116,6 +121,8 @@ protected function setUp(): void $this->validPayload = $this->expiredPayload; $this->validPayload['exp'] = time() + 3600; + + $this->jwkDecoratorMock = $this->createMock(JwkDecorator::class); } @@ -164,4 +171,21 @@ public function testCanBuildFromToken(): void $this->sut()->fromToken('token'), ); } + + + public function testCanBuildFromData(): void + { + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + + $this->assertInstanceOf( + JwtVcJson::class, + $this->sut()->fromData( + $this->jwkDecoratorMock, + SignatureAlgorithmEnum::ES256, + $this->validPayload, + $this->sampleHeader, + ), + ); + } } diff --git a/tests/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactoryTest.php b/tests/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactoryTest.php new file mode 100644 index 0000000..2e1e436 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/Factories/VcDataModelClaimFactoryTest.php @@ -0,0 +1,265 @@ +helpers = new Helpers(); + $this->claimFactoryMock = $this->createMock(ClaimFactory::class); + } + + + public function sut( + ?Helpers $helpers = null, + ?VcDataModelClaimFactory $claimFactory = null, + ): VcDataModelClaimFactory { + $helpers ??= $this->helpers; + $claimFactory ??= $this->claimFactoryMock; + + return new VcDataModelClaimFactory($helpers, $claimFactory); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VcDataModelClaimFactory::class, $this->sut()); + } + + + public function testCanBuildVcClaimValue(): void + { + $this->assertInstanceOf( + VcClaimValue::class, + $this->sut()->buildVcClaimValue( + $this->createMock(VcAtContextClaimValue::class), + 'id', + $this->createMock(TypeClaimValue::class), + $this->createMock(VcCredentialSubjectClaimBag::class), + $this->createMock(VcIssuerClaimValue::class), + $this->createMock(DateTimeImmutable::class), + null, + null, + null, + null, + null, + null, + null, + ), + ); + } + + + public function testCanBuildVcAtContextClaimValue(): void + { + $this->assertInstanceOf( + VcAtContextClaimValue::class, + $this->sut()->buildVcAtContextClaimValue(AtContextsEnum::W3Org2018CredentialsV1->value, []), + ); + } + + + public function testCanBuildTypeClaimValue(): void + { + $this->assertInstanceOf(TypeClaimValue::class, $this->sut()->buildTypeClaimValue('type')); + } + + + public function testBuildTypeClaimValueThrowsIfDataNotArray(): void + { + $this->expectException(\SimpleSAML\OpenID\Exceptions\VcDataModelException::class); + $this->expectExceptionMessage('Type'); + + $this->sut()->buildTypeClaimValue(1); + } + + + public function testBuildVcCredentialSubjectClaimValue(): void + { + $this->assertInstanceOf( + VcCredentialSubjectClaimValue::class, + $this->sut()->buildVcCredentialSubjectClaimValue([]), + ); + } + + + public function testBuildVcCredentialSubjectClaimBag(): void + { + $this->assertInstanceOf( + VcCredentialSubjectClaimBag::class, + $this->sut()->buildVcCredentialSubjectClaimBag(['id' => 'subject']), + ); + } + + + public function testBuildVcCredentialSubjectClaimBagThrowsIfSubProvidedForMultipleSubjects(): void + { + $this->expectException(\SimpleSAML\OpenID\Exceptions\VcDataModelException::class); + $this->expectExceptionMessage('multiple subjects'); + + $this->sut()->buildVcCredentialSubjectClaimBag( + [['id' => 'subject'],['id' => 'subject2']], + 'sub', + ); + } + + + public function testBuildVcCredentialSubjectClaimBagSetsSubForSingleSubject(): void + { + $this->assertInstanceOf( + VcCredentialSubjectClaimBag::class, + $this->sut()->buildVcCredentialSubjectClaimBag(['id' => 'subject'], 'sub'), + ); + } + + + public function testBuildVcIssuerClaimValue(): void + { + $this->assertInstanceOf( + VcIssuerClaimValue::class, + $this->sut()->buildVcIssuerClaimValue(['id' => 'urn:example:issuer']), + ); + } + + + public function testBuildVcProofClaimValue(): void + { + $this->assertInstanceOf( + VcProofClaimValue::class, + $this->sut()->buildVcProofClaimValue(['type' => 'type']), + ); + } + + + public function testBuildVcCredentialStatusClaimValue(): void + { + $this->assertInstanceOf( + VcCredentialStatusClaimValue::class, + $this->sut()->buildVcCredentialStatusClaimValue(['id' => 'urn:example:status', 'type' => 'type']), + ); + } + + + public function testBuildVcCredentialSchemaClaimValue(): void + { + $this->assertInstanceOf( + VcCredentialSchemaClaimValue::class, + $this->sut()->buildVcCredentialSchemaClaimValue(['id' => 'urn:example:schema', 'type' => 'type']), + ); + } + + + public function testBuildVcCredentialSchemaClaimBag(): void + { + $this->assertInstanceOf( + VcCredentialSchemaClaimBag::class, + $this->sut()->buildVcCredentialSchemaClaimBag(['id' => 'urn:example:schema', 'type' => 'type']), + ); + } + + + public function testBuildVcRefreshServiceClaimValue(): void + { + $this->assertInstanceOf( + VcRefreshServiceClaimValue::class, + $this->sut()->buildVcRefreshServiceClaimValue(['id' => 'urn:example:refresh', 'type' => 'type']), + ); + } + + + public function testBuildVcRefreshServiceClaimBag(): void + { + $this->assertInstanceOf( + VcRefreshServiceClaimBag::class, + $this->sut()->buildVcRefreshServiceClaimBag(['id' => 'urn:example:refresh', 'type' => 'type']), + ); + } + + + public function testBuildVcTermsOfUseClaimValue(): void + { + $this->assertInstanceOf( + VcTermsOfUseClaimValue::class, + $this->sut()->buildVcTermsOfUseClaimValue(['type' => 'type']), + ); + } + + + public function testBuildVcTermsOfUseClaimBag(): void + { + $this->assertInstanceOf( + VcTermsOfUseClaimBag::class, + $this->sut()->buildVcTermsOfUseClaimBag(['type' => 'type']), + ); + } + + + public function testBuildVcEvidenceClaimValue(): void + { + $this->assertInstanceOf( + VcEvidenceClaimValue::class, + $this->sut()->buildVcEvidenceClaimValue(['type' => 'type']), + ); + } + + + public function testBuildVcEvidenceClaimBag(): void + { + $this->assertInstanceOf( + VcEvidenceClaimBag::class, + $this->sut()->buildVcEvidenceClaimBag(['type' => 'type']), + ); + } +} diff --git a/tests/src/VerifiableCredentials/VcDataModel/JwtVcJsonTest.php b/tests/src/VerifiableCredentials/VcDataModel/JwtVcJsonTest.php new file mode 100644 index 0000000..c9063b3 --- /dev/null +++ b/tests/src/VerifiableCredentials/VcDataModel/JwtVcJsonTest.php @@ -0,0 +1,604 @@ + [ + "@context" => [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "id" => "https://credential-issuer.example.com/credentials/3732", + "type" => [ + "VerifiableCredential", + "UniversityDegreeCredential", + ], + "issuer" => "https://credential-issuer.example.com", + "issuanceDate" => "2025-01-01T00:00:00Z", + "expirationDate" => "2025-01-01T00:00:00Z", + "credentialSubject" => [ + // phpcs:ignore Generic.Files.LineLength.TooLong + "id" => "did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpWYkpPU3ZqeFU2TDhDN0dVTzRkc2hJWVYzemJ2RndrWUI0M1lKNUt0dDhFIiwia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsImFsZyI6IkVTMjU2IiwieCI6Ik1kQy1PS3E0QVFKZlZDWDV6cFFvTDhqNFZFZnZQWDk4dFU5aHhjTlhHcm8iLCJ5IjoibnNXbmZiNk5Xc0szOUJILWhBYVNrQ1NlNEJ5bWVOc2NKRV9zYUQzRDNiTSJ9", + "degree" => [ + "type" => "BachelorDegree", + "name" => "Bachelor of Science and Arts", + ], + ], + ], + "iss" => "https://credential-issuer.example.com", + "nbf" => 1735689600, + "jti" => "https://credential-issuer.example.com/credentials/3732", + // phpcs:ignore Generic.Files.LineLength.TooLong + "sub" => "did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpWYkpPU3ZqeFU2TDhDN0dVTzRkc2hJWVYzemJ2RndrWUI0M1lKNUt0dDhFIiwia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsImFsZyI6IkVTMjU2IiwieCI6Ik1kQy1PS3E0QVFKZlZDWDV6cFFvTDhqNFZFZnZQWDk4dFU5aHhjTlhHcm8iLCJ5IjoibnNXbmZiNk5Xc0szOUJILWhBYVNrQ1NlNEJ5bWVOc2NKRV9zYUQzRDNiTSJ9", + ]; + + protected array $sampleHeader = [ + 'alg' => 'ES256', + 'typ' => 'JWT', + 'kid' => 'F4VFObNusj3PHmrHxpqh4GNiuFHlfh-2s6xMJ95fLYA', + ]; + + protected array $validPayload; + + + protected function setUp(): void + { + $this->signatureMock = $this->createMock(Signature::class); + + $jwsMock = $this->createMock(JWS::class); + $jwsMock->method('getPayload') + ->willReturn('json-payload-string'); // Just so we have non-empty value. + $jwsMock->method('getSignature')->willReturn($this->signatureMock); + + $this->jwsDecoratorMock = $this->createMock(JwsDecorator::class); + $this->jwsDecoratorMock->method('jws')->willReturn($jwsMock); + + $this->jwsVerifierDecoratorMock = $this->createMock(JwsVerifierDecorator::class); + $this->jwksDecoratorFactoryMock = $this->createMock(JwksDecoratorFactory::class); + $this->jwsSerializerManagerDecoratorMock = $this->createMock(JwsSerializerManagerDecorator::class); + $this->dateIntervalDecoratorMock = $this->createMock(DateIntervalDecorator::class); + + $this->helpersMock = $this->createMock(Helpers::class); + $this->jsonHelperMock = $this->createMock(Helpers\Json::class); + $this->helpersMock->method('json')->willReturn($this->jsonHelperMock); + $typeHelperMock = $this->createMock(Helpers\Type::class); + $this->helpersMock->method('type')->willReturn($typeHelperMock); + $arrHelperMock = $this->createMock(Helpers\Arr::class); + $this->helpersMock->method('arr')->willReturn($arrHelperMock); + $this->dateTimeHelperMock = $this->createMock(Helpers\DateTime::class); + $this->helpersMock->method('dateTime')->willReturn($this->dateTimeHelperMock); + + $typeHelperMock->method('ensureNonEmptyString')->willReturnArgument(0); + $typeHelperMock->method('ensureInt')->willReturnArgument(0); + $typeHelperMock->method('ensureArray')->willReturnArgument(0); + + $arrHelperMock->method('getNestedValue') + ->willReturnCallback(fn( + array $array, + string $key, + string $key2, + ): mixed => $array[$key][$key2] ?? null); + + $this->claimFactoryMock = $this->createMock(ClaimFactory::class); + + $this->validPayload = $this->expiredPayload; + $this->validPayload['exp'] = time() + 3600; + $this->validPayload['vc']['expirationDate'] = (new DateTimeImmutable())->modify('+1 hour') + ->format(DateTimeInterface::ATOM); + } + + + protected function sut( + ?JwsDecorator $jwsDecorator = null, + ?JwsVerifierDecorator $jwsVerifierDecorator = null, + ?JwksDecoratorFactory $jwksDecoratorFactory = null, + ?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null, + ?DateIntervalDecorator $dateIntervalDecorator = null, + ?Helpers $helpers = null, + ?ClaimFactory $claimFactory = null, + ): JwtVcJson { + $jwsDecorator ??= $this->jwsDecoratorMock; + $jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock; + $jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock; + $jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock; + $dateIntervalDecorator ??= $this->dateIntervalDecoratorMock; + $helpers ??= $this->helpersMock; + $claimFactory ??= $this->claimFactoryMock; + + return new JwtVcJson( + $jwsDecorator, + $jwsVerifierDecorator, + $jwksDecoratorFactory, + $jwsSerializerManagerDecorator, + $dateIntervalDecorator, + $helpers, + $claimFactory, + ); + } + + + public function testCanCreateInstance(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $this->assertInstanceOf( + JwtVcJson::class, + $this->sut(), + ); + } + + + public function testCanGetProperties(): void + { + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $sut = $this->sut(); + $this->assertSame(CredentialFormatIdentifiersEnum::JwtVcJson, $sut->getCredentialFormatIdentifier()); + $this->assertInstanceOf(VcClaimValue::class, $sut->getVc()); + $this->assertInstanceOf(VcAtContextClaimValue::class, $sut->getVcAtContext()); + $this->assertIsString($sut->getVcId()); + $this->assertInstanceOf(TypeClaimValue::class, $sut->getVcType()); + $this->assertInstanceOf(VcCredentialSubjectClaimBag::class, $sut->getVcCredentialSubject()); + $this->assertInstanceOf(VcIssuerClaimValue::class, $sut->getVcIssuer()); + $this->assertInstanceOf(\DateTimeImmutable::class, $sut->getVcIssuanceDate()); + $this->assertInstanceOf(\DateTimeImmutable::class, $sut->getVcExpirationDate()); + } + + + public function testThrowsIfNoVcClaimInPayload(): void + { + $this->expectException(JwsException::class); + $this->expectExceptionMessage('VC'); + + $payload = $this->validPayload; + unset($payload['vc']); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->sut(); + } + + + public function testThrowsForInvalidBaseContext(): void + { + $this->expectException(JwsException::class); + $this->expectExceptionMessage('context'); + + $payload = $this->validPayload; + $payload['vc']['@context'] = [123]; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->sut(); + } + + + public function testJwtIdCanBeNull(): void + { + $payload = $this->validPayload; + unset($payload['jti']); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $sut = $this->sut(); + $this->assertNull($sut->getJwtId()); + $this->assertIsString($sut->getVcId()); + } + + + public function testVcIdCanBeNull(): void + { + $payload = $this->validPayload; + unset($payload['jti']); + unset($payload['vc']['id']); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $sut = $this->sut(); + $this->assertNull($sut->getVcId()); + } + + + public function testIssCanBeNull(): void + { + $payload = $this->validPayload; + unset($payload['iss']); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $sut = $this->sut(); + $this->assertNull($sut->getIssuer()); + $this->assertInstanceOf(VcIssuerClaimValue::class, $sut->getVcIssuer()); + } + + + public function testThrowsOnMissingIssuer(): void + { + $payload = $this->validPayload; + unset($payload['iss']); + unset($payload['vc']['issuer']); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Issuer'); + + $this->sut(); + } + + + public function testCanHaveMultipleIssuers(): void + { + $payload = $this->validPayload; + unset($payload['iss']); + $payload['vc']['issuer'] = ['https://issuer1.example.com', 'https://issuer2.example.com']; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->assertInstanceOf(VcIssuerClaimValue::class, $this->sut()->getVcIssuer()); + } + + + public function testThrowsOnMalformedIssuer(): void + { + $payload = $this->validPayload; + unset($payload['iss']); + $payload['vc']['issuer'] = 123; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Issuer'); + + $this->sut(); + } + + + public function testNbfCanBeNull(): void + { + $payload = $this->validPayload; + unset($payload['nbf']); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->assertNull($this->sut()->getNotBefore()); + $this->assertInstanceOf(\DateTimeImmutable::class, $this->sut()->getVcIssuanceDate()); + } + + + public function testThrowsOnInvalidNbf(): void + { + $this->dateTimeHelperMock->method('fromTimestamp') + ->willThrowException(new \Exception('Error')); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Not Before'); + + $this->sut(); + } + + + public function testThrowsOnInvalidIssuanceDate(): void + { + $payload = $this->validPayload; + unset($payload['nbf']); + unset($payload['vc']['issuanceDate']); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Issuance Date'); + + $this->sut(); + } + + + public function testThrowsOnParseIssuanceDateError(): void + { + $payload = $this->validPayload; + unset($payload['nbf']); + + $this->dateTimeHelperMock->method('fromXsDateTime') + ->willThrowException(new \Exception('Error')); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + + $this->sut()->getVcIssuanceDate(); + } + + + public function testCanGetProof(): void + { + $payload = $this->validPayload; + $payload['vc']['proof'] = ['type' => 'type']; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->assertInstanceOf( + VcProofClaimValue::class, + $this->sut()->getVcProof(), + ); + } + + + public function testThrowsOnMalformedProof(): void + { + $payload = $this->validPayload; + $payload['vc']['proof'] = 123; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Proof'); + + $this->sut(); + } + + + public function testExpCanBeNull(): void + { + $payload = $this->validPayload; + unset($payload['exp']); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $sut = $this->sut(); + $this->assertNull($sut->getExpirationTime()); + $this->assertInstanceOf(\DateTimeImmutable::class, $sut->getVcExpirationDate()); + } + + + public function testExpirationDateCanBeNull(): void + { + $payload = $this->validPayload; + unset($payload['exp']); + unset($payload['vc']['expirationDate']); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->assertNotInstanceOf(\DateTimeImmutable::class, $this->sut()->getVcExpirationDate()); + } + + + public function testThrowsOnParseExpirationDateError(): void + { + $payload = $this->validPayload; + unset($payload['exp']); + + + $this->dateTimeHelperMock->method('fromXsDateTime') + ->willThrowException(new \Exception('Error')); + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + + $this->sut()->getVcExpirationDate(); + } + + + public function testThrowsOnMalformedExpirationDate(): void + { + $payload = $this->validPayload; + unset($payload['exp']); + $payload['vc']['expirationDate'] = 123; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + + $this->sut()->getVcExpirationDate(); + } + + + public function testCanGetCredentialStatus(): void + { + $payload = $this->validPayload; + $payload['vc']['credentialStatus'] = ['id' => 'id', 'type' => 'type']; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->assertInstanceOf(VcCredentialStatusClaimValue::class, $this->sut()->getVcCredentialStatus()); + } + + + public function testThrowsOnMalformedCredentialStatus(): void + { + $payload = $this->validPayload; + $payload['vc']['credentialStatus'] = 123; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Credential Status'); + + $this->sut(); + } + + + public function testCanGetCredentialSchema(): void + { + $payload = $this->validPayload; + $payload['vc']['credentialSchema'] = ['id' => 'id', 'type' => 'type']; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->assertInstanceOf(VcCredentialSchemaClaimBag::class, $this->sut()->getVcCredentialSchema()); + } + + + public function testThrowsOnMalformedCredentialSchema(): void + { + $payload = $this->validPayload; + $payload['vc']['credentialSchema'] = 123; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Credential Schema'); + + $this->sut(); + } + + + public function testCanGetRefreshService(): void + { + $payload = $this->validPayload; + $payload['vc']['refreshService'] = ['id' => 'id', 'type' => 'type']; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->assertInstanceOf(VcRefreshServiceClaimBag::class, $this->sut()->getVcRefreshService()); + } + + + public function testThrowsOnMalformedRefreshService(): void + { + $payload = $this->validPayload; + $payload['vc']['refreshService'] = 123; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Refresh Service'); + + $this->sut(); + } + + + public function testCanGetTermsOfUse(): void + { + $payload = $this->validPayload; + $payload['vc']['termsOfUse'] = ['type' => 'type']; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->assertInstanceOf(VcTermsOfUseClaimBag::class, $this->sut()->getVcTermsOfUse()); + } + + + public function testThrowsOnMalformedTermsOfUse(): void + { + $payload = $this->validPayload; + $payload['vc']['termsOfUse'] = 123; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Terms Of Use'); + + $this->sut(); + } + + + public function testCanGetEvidence(): void + { + $payload = $this->validPayload; + $payload['vc']['evidence'] = ['type' => 'type']; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->assertInstanceOf(VcEvidenceClaimBag::class, $this->sut()->getVcEvidence()); + } + + + public function testThrowsOnMalformedEvidence(): void + { + $payload = $this->validPayload; + $payload['vc']['evidence'] = 123; + + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($payload); + + $this->expectException(JwsException::class); + $this->expectExceptionMessage('Evidence'); + + $this->sut()->getVcEvidence(); + } +} diff --git a/tests/src/VerifiableCredentialsTest.php b/tests/src/VerifiableCredentialsTest.php new file mode 100644 index 0000000..157acec --- /dev/null +++ b/tests/src/VerifiableCredentialsTest.php @@ -0,0 +1,224 @@ +supportedSerializersMock = $this->createMock(SupportedSerializers::class); + $this->supportedAlgorithmsMock = $this->createMock(SupportedAlgorithms::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->timestampValidationLeeway = new DateInterval('PT1M'); + } + + + protected function sut( + ?SupportedSerializers $supportedSerializers = null, + ?SupportedAlgorithms $supportedAlgorithms = null, + ?LoggerInterface $logger = null, + ?DateInterval $timestampValidationLeeway = null, + ): VerifiableCredentials { + $supportedSerializers ??= $this->supportedSerializersMock; + $supportedAlgorithms ??= $this->supportedAlgorithmsMock; + $logger ??= $this->loggerMock; + $timestampValidationLeeway ??= $this->timestampValidationLeeway; + + return new VerifiableCredentials( + $supportedSerializers, + $supportedAlgorithms, + $logger, + $timestampValidationLeeway, + ); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(VerifiableCredentials::class, $this->sut()); + } + + + public function testCanBuildTools(): void + { + $sut = $this->sut(); + + $this->assertInstanceOf( + DateIntervalDecoratorFactory::class, + $sut->dateIntervalDecoratorFactory(), + ); + + $this->assertInstanceOf( + Helpers::class, + $sut->helpers(), + ); + + $this->assertInstanceOf( + ClaimsPathPointerResolver::class, + $sut->claimsPathPointerResolver(), + ); + + $this->assertInstanceOf( + JwsDecoratorBuilderFactory::class, + $sut->jwsDecoratorBuilderFactory(), + ); + + $this->assertInstanceOf( + JwsSerializerManagerDecoratorFactory::class, + $sut->jwsSerializerManagerDecoratorFactory(), + ); + + $this->assertInstanceOf( + JwsDecoratorBuilderFactory::class, + $sut->jwsDecoratorBuilderFactory(), + ); + + $this->assertInstanceOf( + JwsSerializerManagerDecoratorFactory::class, + $sut->jwsSerializerManagerDecoratorFactory(), + ); + + $this->assertInstanceOf( + JwsSerializerManagerDecorator::class, + $sut->jwsSerializerManagerDecorator(), + ); + + $this->assertInstanceOf( + AlgorithmManagerDecoratorFactory::class, + $sut->algorithmManagerDecoratorFactory(), + ); + + $this->assertInstanceOf( + AlgorithmManagerDecorator::class, + $sut->algorithmManagerDecorator(), + ); + + $this->assertInstanceOf( + JwsDecoratorBuilder::class, + $sut->jwsDecoratorBuilder(), + ); + + $this->assertInstanceOf( + JwsVerifierDecoratorFactory::class, + $sut->jwsVerifierDecoratorFactory(), + ); + + $this->assertInstanceOf( + JwsVerifierDecorator::class, + $sut->jwsVerifierDecorator(), + ); + + $this->assertInstanceOf( + JwksDecoratorFactory::class, + $sut->jwksDecoratorFactory(), + ); + + $this->assertInstanceOf( + JwtVcJsonFactory::class, + $sut->jwtVcJsonFactory(), + ); + + $this->assertInstanceOf( + CredentialOfferFactory::class, + $sut->credentialOfferFactory(), + ); + + $this->assertInstanceOf( + OpenId4VciProofFactory::class, + $sut->openId4VciProofFactory(), + ); + + $this->assertInstanceOf( + DisclosureFactory::class, + $sut->disclosureFactory(), + ); + + $this->assertInstanceOf( + DisclosureBagFactory::class, + $sut->disclosureBagFactory(), + ); + + $this->assertInstanceOf( + DisclosureFactory::class, + $sut->disclosureFactory(), + ); + + $this->assertInstanceOf( + DisclosureBagFactory::class, + $sut->disclosureBagFactory(), + ); + + $this->assertInstanceOf( + SdJwtVcFactory::class, + $sut->sdJwtVcFactory(), + ); + + $this->assertInstanceOf( + TxCodeFactory::class, + $sut->txCodeFactory(), + ); + } +} From d8bc7bfb35b7aa092c92d20378833f0aad15a6bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 19 Nov 2025 17:07:46 +0100 Subject: [PATCH 62/66] Move to dedicated docs --- README.md | 277 +-------------------------------------- docs/1-openid.md | 0 docs/2-installation.md | 7 + docs/3-federation.md | 287 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 297 insertions(+), 274 deletions(-) create mode 100644 docs/1-openid.md create mode 100644 docs/2-installation.md create mode 100644 docs/3-federation.md diff --git a/README.md b/README.md index fc4d57a..1e41de2 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [![Build Status](https://github.com/simplesamlphp/openid/actions/workflows/php.yml/badge.svg)](https://github.com/simplesamlphp/openid/actions/workflows/php.yml) [![Coverage Status](https://codecov.io/gh/simplesamlphp/openid/branch/master/graph/badge.svg)](https://app.codecov.io/gh/simplesamlphp/openid) -The library is under development, and you can expect braking changes along the way. +The library provides some common tools that you might find useful when working with the OpenID family of specifications. -The library provides some common tools that you might find useful when working with OpenID family of specifications. +> The library is under development, and you can expect braking changes along the way. ``` /* @@ -20,277 +20,6 @@ The library provides some common tools that you might find useful when working w */ ``` -## Installation +To get started, refer to [library documentation](docs/1-openid.md). -Library can be installed by using Composer: -```shell -composer require simplesamlphp/openid -``` - -## OpenID Federation (draft 43) - -The initial functionality of the library revolves around the OpenID Federation specification. To use it, create an -instance of the class `\SimpleSAML\OpenID\Federation` - -```php -cache, // \Psr\SimpleCache\CacheInterface - logger: $this->logger, // \Psr\Log\LoggerInterface - ); - - // Continue with using available tools ... - - return new Response(); - } -} -``` - -### Trust chain resolver - -Once you have a `\SimpleSAML\OpenID\Federation` instantiated, you can continue with using available tools. The first -tool we will take a look at is trust chain resolver. This tool can be used to try and resolve the (shortest) trust chain -for given leaf entity (subject) and trusted anchors: - -```php - -// ... - -try { - /** @var \SimpleSAML\OpenID\Federation $federationTools */ - /** @var \SimpleSAML\OpenID\Federation\TrustChainBag $trustChainBag */ - $trustChainBag = $federationTools->trustChainResolver()->for( - 'https://leaf-entity-id.example.org/', // Trust chain subject (leaf entity). - [ - // List of valid trust anchors. - 'https://trust-achor-id.example.org/', - 'https://other-trust-achor-id.example.org/', - ], - ); -} catch (\Throwable $exception) { - $this->logger->error('Could not resolve trust chain: ' . $exception->getMessage()) - return; -} - -``` - -If the trust chain is successfully resolved, this will return an instance of -`\SimpleSAML\OpenID\Federation\TrustChainBag`. Otherwise, exception will be thrown. -From the TrustChainBag you can get the TrustChain using several methods. - -```php - -// ... - -try { - /** @var \SimpleSAML\OpenID\Federation\TrustChain $trustChain */ - /** @var \SimpleSAML\OpenID\Federation\TrustChainBag $trustChainBag */ - // Simply get the shortest available chain. - $trustChain = $trustChainBag->getShortest(); - // Get the shortest chain, but take into account the Trust Anchor priority. - $trustChain = $trustChainBag->getShortestByTrustAnchorPriority( - 'https://other-trust-achor-id.example.org/', // Get chain for this Trust Anchor even if the chain is longer. - 'https://trust-achor-id.example.org/', - ); -} catch (\Throwable $exception) { - $this->logger->error('Could not resolve trust chain: ' . $exception->getMessage()) - return; -} - -``` - -Once you have the Trust Chain, you can try and get the resolved metadata for particular entity type. Resolved metadata -means that all metadata policies from all intermediates have been successfully applied. Here is one example for trying -to get metadata for OpenID RP, which will return an array (or null if no metadata is available for given entity type): - -```php -// ... - -$entityType = \SimpleSAML\OpenID\Codebooks\EntityTypesEnum::OpenIdRelyingParty; - -try { - /** @var \SimpleSAML\OpenID\Federation\TrustChain $trustChain */ - $metadata = $trustChain->getResolvedMetadata($entityType); -} catch (\Throwable $exception) { - $this->logger->error( - sprintf( - 'Error resolving metadata for entity type %s. Error: %s.', - $entityType->value, - $exception->getMessage(), - ), - ); - return; -} - -if (is_null($metadata)) { - $this->logger->error( - sprintf( - 'No metadata available for entity type %s.', - $entityType->value, - ), - ); - return; -} -``` - -If getting metadata results in an exception, the metadata is considered invalid and is to be discarded. - -### Additional verification of signatures - -The whole trust chain (each entity statement) has been verified using public keys from JWKS claims in configuration / -subordinate statements. As per specification recommendation, you can also validate the signature of the Trust Chain -Configuration Statement by using the Trust Anchor public keys (JWKS) that you have acquired in some secure out-of-band -way (so to not only rely on TLS protection while fetching Trust Anchor Configuration Statement): - -```php - -// ... - -// Get entity statement for the resolved Trust Anchor: -/** @var \SimpleSAML\OpenID\Federation\TrustChain $trustChain */ -$trustAnchorConfigurationStatement = $trustChain->getResolvedTrustAnchor(); -// Get data that you need to prepare appropriate public keys, for example, the entity ID: -$trustAnchorEntityId = $trustAnchorConfigurationStatement->getIssuer(); - -// Prepare JWKS array containing Trust Anchor public keys that you have acquired in secure out-of-band way ... -/** @var array $trustAnchorJwks */ - -try { - $trustAnchorConfigurationStatement->verifyWithKeySet($trustAnchorJwks); -} catch (\Throwable $exception) { - $this->logger->error('Could not verify trust anchor configuration statement signature: ' . - $exception->getMessage()); - return; -} - -``` - -### Fetching Trust Marks - -Federation tools expose Trust Mark Fetcher which you can use to dynamically fetch or refresh (short-living) Trust Marks. - -```php -// ... - -/** @var \SimpleSAML\OpenID\Federation $federationTools */ - -// Trust Mark Type that you want to fetch. -$trustMarkType = 'https://example.com/trust-mark/member'; -// ID of Subject for which to fetch the Trust Mark. -$subjectId = 'https://leaf-entity.org' -// ID of the Trust Mark Issuer from which to fetch the Trust Mark. -$trustMarkIssuerEntityId = 'https://trust-mark-issuer.org' - -try { - // First, fetch the Configuration Statement for Trust Mark Issuer. - $trustMarkIssuerConfigurationStatement = $this->federation - ->entityStatementFetcher() - ->fromCacheOrWellKnownEndpoint($trustMarkIssuerEntityId); - - // Fetch the Trust Mark from Issuer. - $trustMarkEntity = $federationTools->trustMarkFetcher()->fromCacheOrFederationTrustMarkEndpoint( - $trustMarkType, - $subjectId, - $trustMarkIssuerConfigurationStatement - ); - -} catch (\Throwable $exception) { - $this->logger->error('Trust Mark fetch failed. Error was: ' . $exception->getMessage()); - return; -} - -``` - -### Validating Trust Marks - -Federation tools expose Trust Mark Validator with several methods for validating -Trust Marks, with the most common one being the one to validate Trust Mark for -some entity simply based on the Trust Mark Type. - -If cache is used, Trust Mark validation will be cached with cache TTL being the minimum expiration -time of Trust Mark, Leaf Entity Statement or `maxCacheDuration`, whatever is smaller. - -```php -// ... - -/** @var \SimpleSAML\OpenID\Federation $federationTools */ -/** @var \SimpleSAML\OpenID\Federation\TrustChain $trustChain */ - - -// Trust Mark Type that you want to validate. -$trustMarkType = 'https://example.com/trust-mark/member'; -// Leaf for which you want to validate the Trust Mark with ID above. -$leafEntityConfigurationStatement = $trustChain->getResolvedLeaf(); -// Trust Anchor under which you want to validate Trust Mark. -$trustAnchorConfigurationStatement = $trustChain->getResolvedTrustAnchor(); - -try { - // Example which queries cache for previously validated Trust Mark and does formal validation if not cached. - $federationTools->trustMarkValidator()->fromCacheOrDoForTrustMarkType( - $trustMarkType, - $leafEntityConfigurationStatement, - $trustAnchorConfigurationStatement, - $expectedJwtType = \SimpleSAML\OpenID\Codebooks\JwtTypesEnum::TrustMarkJwt, - ); - - // Example which always does formal validation (does not use cache), and requires usage of Trust Mark - // Status Endpoint for non-expiring Trust Marks. - $federationTools->trustMarkValidator()->doForTrustMarkType( - $trustMarkType, - $leafEntityConfigurationStatement, - $trustAnchorConfigurationStatement, - $expectedJwtType = \SimpleSAML\OpenID\Codebooks\JwtTypesEnum::TrustMarkJwt, - \SimpleSAML\OpenID\Codebooks\TrustMarkStatusEndpointUsagePolicyEnum::RequiredForNonExpiringTrustMarksOnly, - ); -} catch (\Throwable $exception) { - $this->logger->error('Trust Mark validation failed. Error was: ' . $exception->getMessage()); - return; -} - -``` diff --git a/docs/1-openid.md b/docs/1-openid.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/2-installation.md b/docs/2-installation.md new file mode 100644 index 0000000..3652bb6 --- /dev/null +++ b/docs/2-installation.md @@ -0,0 +1,7 @@ +## OpenID Tools Library Installation + +Library can be installed by using Composer: + +```shell +composer require simplesamlphp/openid +``` \ No newline at end of file diff --git a/docs/3-federation.md b/docs/3-federation.md new file mode 100644 index 0000000..0d92f29 --- /dev/null +++ b/docs/3-federation.md @@ -0,0 +1,287 @@ +## OpenID Federation Tools (draft 44) + +To use it, create an instance of the class `\SimpleSAML\OpenID\Federation`. + +```php +cache, // \Psr\SimpleCache\CacheInterface + logger: $this->logger, // \Psr\Log\LoggerInterface + ); + + // Continue with using available tools ... + + return new Response(); + } +} +``` + +### Trust chain resolver + +Once you have a `\SimpleSAML\OpenID\Federation` instantiated, you can continue +with using available tools. The first tool we will take a look at is the Trust +Chain Resolver. This tool can be used to try and resolve the (shortest) trust +chain for a given leaf entity (subject) and trusted anchors: + +```php + +// ... + +try { + /** @var \SimpleSAML\OpenID\Federation $federationTools */ + /** @var \SimpleSAML\OpenID\Federation\TrustChainBag $trustChainBag */ + $trustChainBag = $federationTools->trustChainResolver()->for( + 'https://leaf-entity-id.example.org/', // Trust chain subject (leaf). + [ + // List of valid trust anchors. + 'https://trust-achor-id.example.org/', + 'https://other-trust-achor-id.example.org/', + ], + ); +} catch (\Throwable $exception) { + $this->logger->error('Could not resolve trust chain: ' . + $exception->getMessage()) + return; +} + +``` + +If the trust chain is successfully resolved, this will return an instance of +`\SimpleSAML\OpenID\Federation\TrustChainBag`. Otherwise, an exception will + be thrown. From the TrustChainBag you can get the TrustChain using several +methods. + +```php + +// ... + +try { + /** @var \SimpleSAML\OpenID\Federation\TrustChain $trustChain */ + /** @var \SimpleSAML\OpenID\Federation\TrustChainBag $trustChainBag */ + // Simply get the shortest available chain. + $trustChain = $trustChainBag->getShortest(); + // Get the shortest chain, but take into account the Trust Anchor priority. + $trustChain = $trustChainBag->getShortestByTrustAnchorPriority( + // Get a chain for this Trust Anchor even if the chain is longer. + 'https://other-trust-achor-id.example.org/', + 'https://trust-achor-id.example.org/', + ); +} catch (\Throwable $exception) { + $this->logger->error('Could not resolve trust chain: ' . + $exception->getMessage()) + return; +} + +``` + +Once you have the Trust Chain, you can try and get the resolved metadata for +a particular entity type. Resolved metadata means that all metadata policies +from all intermediates have been successfully applied. Here is one example +for trying to get metadata for OpenID RP, which will return an array +(or null if no metadata is available for a given entity type): + +```php +// ... + +$entityType = \SimpleSAML\OpenID\Codebooks\EntityTypesEnum::OpenIdRelyingParty; + +try { + /** @var \SimpleSAML\OpenID\Federation\TrustChain $trustChain */ + $metadata = $trustChain->getResolvedMetadata($entityType); +} catch (\Throwable $exception) { + $this->logger->error( + sprintf( + 'Error resolving metadata for entity type %s. Error: %s.', + $entityType->value, + $exception->getMessage(), + ), + ); + return; +} + +if (is_null($metadata)) { + $this->logger->error( + sprintf( + 'No metadata available for entity type %s.', + $entityType->value, + ), + ); + return; +} +``` + +If getting metadata results in an exception, the metadata is considered invalid +and is to be discarded. + +### Additional verification of signatures + +The whole trust chain (each entity statement) has been verified using public +keys from JWKS claims in configuration / subordinate statements. As per +specification recommendation, you can also validate the signature of the +Trust Chain Configuration Statement by using the Trust Anchor public +keys (JWKS) that you have acquired in some secure out-of-band way +(so to not only rely on TLS protection while fetching Trust Anchor +Configuration Statement): + +```php + +// ... + +// Get entity statement for the resolved Trust Anchor: +/** @var \SimpleSAML\OpenID\Federation\TrustChain $trustChain */ +$trustAnchorConfigurationStatement = $trustChain->getResolvedTrustAnchor(); +// Get data that you need to prepare appropriate public keys, for example, +// the entity ID: +$trustAnchorEntityId = $trustAnchorConfigurationStatement->getIssuer(); + +// Prepare a JWKS array containing Trust Anchor public keys that you have +// acquired in a secure out-of-band way... +/** @var array $trustAnchorJwks */ + +try { + $trustAnchorConfigurationStatement->verifyWithKeySet($trustAnchorJwks); +} catch (\Throwable $exception) { + $this->logger->error( + 'Could not verify trust anchor configuration statement signature: ' . + $exception->getMessage(), + ); + return; +} + +``` + +### Fetching Trust Marks + +Federation tools expose Trust Mark Fetcher, which you can use to dynamically +fetch or refresh (short-living) Trust Marks. + +```php +// ... + +/** @var \SimpleSAML\OpenID\Federation $federationTools */ + +// Trust Mark Type that you want to fetch. +$trustMarkType = 'https://example.com/trust-mark/member'; +// ID of Subject for which to fetch the Trust Mark. +$subjectId = 'https://leaf-entity.org' +// ID of the Trust Mark Issuer from which to fetch the Trust Mark. +$trustMarkIssuerEntityId = 'https://trust-mark-issuer.org' + +try { + // First, fetch the Configuration Statement for Trust Mark Issuer. + $trustMarkIssuerConfigurationStatement = $this->federation + ->entityStatementFetcher() + ->fromCacheOrWellKnownEndpoint($trustMarkIssuerEntityId); + + // Fetch the Trust Mark from Issuer. + $trustMarkEntity = $federationTools->trustMarkFetcher() + ->fromCacheOrFederationTrustMarkEndpoint( + $trustMarkType, + $subjectId, + $trustMarkIssuerConfigurationStatement + ); + +} catch (\Throwable $exception) { + $this->logger->error('Trust Mark fetch failed. Error was: ' . + $exception->getMessage()); + return; +} + +``` + +### Validating Trust Marks + +Federation tools expose Trust Mark Validator with several methods for validating +Trust Marks, with the most common one being the one to validate Trust Mark for +some entity simply based on the Trust Mark Type. + +If cache is used, Trust Mark validation will be cached with cache TTL being the +minimum expiration time of Trust Mark, Leaf Entity Statement or +`maxCacheDuration`, whatever is smaller. + +```php +// ... + +/** @var \SimpleSAML\OpenID\Federation $federationTools */ +/** @var \SimpleSAML\OpenID\Federation\TrustChain $trustChain */ + + +// Trust Mark Type that you want to validate. +$trustMarkType = 'https://example.com/trust-mark/member'; +// Leaf for which you want to validate the Trust Mark with ID above. +$leafEntityConfigurationStatement = $trustChain->getResolvedLeaf(); +// Trust Anchor under which you want to validate Trust Mark. +$trustAnchorConfigurationStatement = $trustChain->getResolvedTrustAnchor(); + +try { + // Example which queries cache for previously validated Trust Mark and does + // formal validation if not cached. + $federationTools->trustMarkValidator()->fromCacheOrDoForTrustMarkType( + $trustMarkType, + $leafEntityConfigurationStatement, + $trustAnchorConfigurationStatement, + $expectedJwtType = \SimpleSAML\OpenID\Codebooks\JwtTypesEnum::TrustMarkJwt, + ); + + // Example which always does formal validation (does not use cache), and + // requires usage of Trust Mark Status Endpoint for non-expiring Trust + // Marks. + $federationTools->trustMarkValidator()->doForTrustMarkType( + $trustMarkType, + $leafEntityConfigurationStatement, + $trustAnchorConfigurationStatement, + $expectedJwtType = \SimpleSAML\OpenID\Codebooks\JwtTypesEnum::TrustMarkJwt, + \SimpleSAML\OpenID\Codebooks\TrustMarkStatusEndpointUsagePolicyEnum::RequiredForNonExpiringTrustMarksOnly, + ); +} catch (\Throwable $exception) { + $this->logger->error('Trust Mark validation failed. Error was: ' . $exception->getMessage()); + return; +} + +``` \ No newline at end of file From e8d68787c05231fec308ada99642450abe1a7f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 19 Nov 2025 17:09:31 +0100 Subject: [PATCH 63/66] Add Build docmuentation workflow --- .github/workflows/documentation.yml | 21 +++++++++++++++++++++ docs/3-federation.md | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 650b3c6..1801c92 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -33,3 +33,24 @@ jobs: path: '**/*.md' check_filenames: true ignore_words_list: tekst + + build: + name: Build documentation + needs: quality + runs-on: [ubuntu-latest] + + steps: + - name: Run docs build + if: github.event_name != 'pull_request' + uses: actions/github-script@v8 + with: + # Token has to be generated on a user account that controls the docs-repository. + # The _only_ scope to select is "Access public repositories", nothing more. + github-token: ${{ secrets.PAT_TOKEN }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'simplesamlphp', + repo: 'docs', + workflow_id: 'mk_docs.yml', + ref: 'main' + }) diff --git a/docs/3-federation.md b/docs/3-federation.md index 0d92f29..2951930 100644 --- a/docs/3-federation.md +++ b/docs/3-federation.md @@ -256,7 +256,7 @@ minimum expiration time of Trust Mark, Leaf Entity Statement or $trustMarkType = 'https://example.com/trust-mark/member'; // Leaf for which you want to validate the Trust Mark with ID above. $leafEntityConfigurationStatement = $trustChain->getResolvedLeaf(); -// Trust Anchor under which you want to validate Trust Mark. +// Trust Anchor, under which you want to validate Trust Mark. $trustAnchorConfigurationStatement = $trustChain->getResolvedTrustAnchor(); try { From cd1ed84fad28ed4ca39aebce0da1986611b9201a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 19 Nov 2025 17:17:52 +0100 Subject: [PATCH 64/66] Reduce phpstan testsuite level --- phpstan-dev.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan-dev.neon b/phpstan-dev.neon index f6d28bc..b7a1e81 100644 --- a/phpstan-dev.neon +++ b/phpstan-dev.neon @@ -1,6 +1,6 @@ parameters: - level: 4 + level: 3 paths: - tests tmpDir: build/phpstan/dev \ No newline at end of file From 6ba472ad225aa20687eb0239270a823a8a7b2073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Fri, 21 Nov 2025 12:13:46 +0100 Subject: [PATCH 65/66] Initial dedicated docs --- docs/1-openid.md | 5 + docs/4-vci.md | 121 +++++++++++++++++++++++ src/Algorithms/SignatureAlgorithmBag.php | 6 +- 3 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 docs/4-vci.md diff --git a/docs/1-openid.md b/docs/1-openid.md index e69de29..b088151 100644 --- a/docs/1-openid.md +++ b/docs/1-openid.md @@ -0,0 +1,5 @@ +# OpenID Tools Library + +1. [Installation](2-installation.md) +2. [OpenID Federation Tools](3-federation.md) +3. [OpenID for Verifiable Credential Issuance (OpenID4VCI) Tools](4-vci.md) diff --git a/docs/4-vci.md b/docs/4-vci.md new file mode 100644 index 0000000..261cd16 --- /dev/null +++ b/docs/4-vci.md @@ -0,0 +1,121 @@ +## OpenID for Verifiable Credential Issuance (OpenID4VCI) Tools + +To use it, create an instance of the `\SimpleSAML\OpenID\VerifiableCredentials` +class. + +```php + +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmBag; +use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; +use SimpleSAML\OpenID\Jwk; +use SimpleSAML\OpenID\Serializers\JwsSerializerBag; +use SimpleSAML\OpenID\Serializers\JwsSerializerEnum; +use SimpleSAML\OpenID\SupportedAlgorithms; +use SimpleSAML\OpenID\VerifiableCredentials; + +// Prepare supported JWS serializers. +$jwsSerializerBag = new JwsSerializerBag( + JwsSerializerEnum::Compact, +); + +// Prepare supported signature algorithms. +$supportedAlgorithms = new SupportedAlgorithms( + new SignatureAlgorithmBag( + SignatureAlgorithmEnum::RS256, + SignatureAlgorithmEnum::RS384, + SignatureAlgorithmEnum::RS512, + SignatureAlgorithmEnum::ES256, + SignatureAlgorithmEnum::ES384, + SignatureAlgorithmEnum::ES512, + SignatureAlgorithmEnum::PS256, + SignatureAlgorithmEnum::PS384, + SignatureAlgorithmEnum::PS512, + ), +); + +// Choose the leeway time used when validate timestamps like `exp`, `iat`, etc. +$timestampValidationLeeway = new DateInterval('PT1M'); + +$verifiableCredentialTools = new VerifiableCredentials( + $jwsSerializerBag, + $supportedAlgorithms, + $timestampValidationLeeway, +); + +// You can also use the JWK Tools to create a JWK decorator from a private key file. +$jwkTools = new Jwk(); +``` + +You can now use the `$verifiableCredentialTools` instance to create and verify +verifiable credentials. + +### Creating SD-JWT VCs + +The following example shows how to create a SD-JWT VC. + +```php + +use SimpleSAML\OpenID\Codebooks\ClaimsEnum; + +/** @var \SimpleSAML\OpenID\VerifiableCredentials $verifiableCredentialTools */ +/** @var \SimpleSAML\OpenID\Jwk $jwkTools */ + +// Use any logic necessary to prepare data to be disclosed. +$disclosedData = [ + 'name' => 'John', + // ... +]; + +// Prepare a disclosure bag. +$disclosureBag = $verifiableCredentialTools->disclosureBagFactory()->build(); + +// Add disclosures to the bag. +foreach ($disclosedData as $key => $value) { + $disclosure = $verifiableCredentialTools->disclosureFactory()->build( + value: $value, + name: $key, + path: [], // Or set a path as ['path', 'to', 'value'] + saltBlacklist: $disclosureBag->salts(), // To prevent salt collisions. + ); + + $disclosureBag->add($disclosure); +} + +$issuedAt = new \DateTimeImmutable(); + +// Use any logic necessary to prepare basic JWT payload data. +$jwtPayload = [ + ClaimsEnum::Iss->value => 'https://example.com/issuer', + ClaimsEnum::Iat->value => $issuedAt->getTimestamp(), + ClaimsEnum::Nbf->value => $issuedAt->getTimestamp(), + ClaimsEnum::Sub->value => 'subject-id', + ClaimsEnum::Jti->value => 'vc-id', + ClaimsEnum::Vct->value => 'https://credentials.example.com/identity_credential', + // ... +]; + +// Use any logic necessary to prepare SD JWT header data. +$jwtHeader = [ + //... +]; + +// Prepare a signing key decorator. Check other methods on `jwkDecoratorFactory` +// for alternative ways to create a key decorator. +$signingKey = $jwkTools->jwkDecoratorFactory()->fromPkcs1Or8KeyFile( + '/path/to/private/key.pem', +); + +// Set the signature algorithm to use. +$signatureAlgorithm = SignatureAlgorithmEnum::ES256; + +$verifiableCredential = $verifiableCredentialTools->sdJwtVcFactory()->fromData( + $signingKey, + $signatureAlgorithm, + $jwtPayload, + $jwtHeader, + $disclosureBag, +); + +// Get the credential token string. +$token = $verifiableCredential->getToken(); +``` diff --git a/src/Algorithms/SignatureAlgorithmBag.php b/src/Algorithms/SignatureAlgorithmBag.php index fe66a42..37cdcb0 100644 --- a/src/Algorithms/SignatureAlgorithmBag.php +++ b/src/Algorithms/SignatureAlgorithmBag.php @@ -14,13 +14,15 @@ class SignatureAlgorithmBag public function __construct(SignatureAlgorithmEnum $algorithm, SignatureAlgorithmEnum ...$algorithms) { - $this->algorithms = [$algorithm, ...$algorithms]; + $this->algorithms = array_unique([$algorithm, ...$algorithms]); } public function add(SignatureAlgorithmEnum $algorithm): void { - $this->algorithms[] = $algorithm; + if (!in_array($algorithm, $this->algorithms, true)) { + $this->algorithms[] = $algorithm; + } } From 55d63cdf9bcedcb151c25f2f1bac1a25d9f44144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Fri, 21 Nov 2025 12:20:01 +0100 Subject: [PATCH 66/66] Compare unique items regularly --- src/Algorithms/SignatureAlgorithmBag.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Algorithms/SignatureAlgorithmBag.php b/src/Algorithms/SignatureAlgorithmBag.php index 37cdcb0..323787d 100644 --- a/src/Algorithms/SignatureAlgorithmBag.php +++ b/src/Algorithms/SignatureAlgorithmBag.php @@ -14,7 +14,7 @@ class SignatureAlgorithmBag public function __construct(SignatureAlgorithmEnum $algorithm, SignatureAlgorithmEnum ...$algorithms) { - $this->algorithms = array_unique([$algorithm, ...$algorithms]); + $this->algorithms = array_unique([$algorithm, ...$algorithms], SORT_REGULAR); }