From f7db44f0da0ed7d4adcf3c159f57af61cab6a7c3 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Tue, 21 Oct 2025 10:50:59 +0200 Subject: [PATCH 01/22] Reflect issuer_state in access_token --- src/Entities/AccessTokenEntity.php | 6 ++++ src/Entities/AuthCodeEntity.php | 15 ++++++++++ .../Entities/AccessTokenEntityFactory.php | 2 ++ .../Entities/AuthCodeEntityFactory.php | 4 +++ src/Factories/RequestRulesManagerFactory.php | 4 +-- src/Repositories/AccessTokenRepository.php | 8 +++-- src/Repositories/AuthCodeRepository.php | 9 ++++-- src/Server/AuthorizationServer.php | 2 ++ src/Server/Grants/AuthCodeGrant.php | 18 ++++++++--- .../Grants/Traits/IssueAccessTokenTrait.php | 2 ++ .../RequestRules/Rules/IssuerStateRule.php | 29 ++---------------- src/Services/DatabaseMigration.php | 30 +++++++++++++++++++ 12 files changed, 90 insertions(+), 39 deletions(-) diff --git a/src/Entities/AccessTokenEntity.php b/src/Entities/AccessTokenEntity.php index 834902a1..e98e1fbb 100644 --- a/src/Entities/AccessTokenEntity.php +++ b/src/Entities/AccessTokenEntity.php @@ -74,6 +74,7 @@ public function __construct( protected readonly ?array $authorizationDetails = null, protected readonly ?string $boundClientId = null, protected readonly ?string $boundRedirectUri = null, + protected ?string $issuerState = null, ) { $this->setIdentifier($id); $this->setClient($clientEntity); @@ -89,6 +90,7 @@ public function __construct( $this->revoke(); } $jwtConfiguration !== null ? $this->jwtConfiguration = $jwtConfiguration : $this->initJwtConfiguration(); + $this->issuerState = $issuerState; } /** @@ -125,6 +127,7 @@ public function getState(): array null, 'bound_client_id' => $this->boundClientId, 'bound_redirect_uri' => $this->boundRedirectUri, + 'issuer_state' => $this->issuerState, ]; } @@ -166,6 +169,9 @@ protected function convertToJWT(): Token ->expiresAt($this->getExpiryDateTime()) ->relatedTo((string) $this->getUserIdentifier()) ->withClaim('scopes', $this->getScopes()); + if ($this->issuerState !== null) { + $jwtBuilder = $jwtBuilder->withClaim('issuer_state', $this->issuerState); + } return $this->jsonWebTokenBuilderService->getSignedProtocolJwt($jwtBuilder); } diff --git a/src/Entities/AuthCodeEntity.php b/src/Entities/AuthCodeEntity.php index e8e10f30..d38c8ec5 100644 --- a/src/Entities/AuthCodeEntity.php +++ b/src/Entities/AuthCodeEntity.php @@ -32,6 +32,12 @@ class AuthCodeEntity implements AuthCodeEntityInterface, MementoInterface use OidcAuthCodeTrait; use RevokeTokenTrait; + /** + * issuer state + * @var string $issuerState + */ + protected ?string $issuerState = null; + /** * @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes */ @@ -49,6 +55,7 @@ public function __construct( protected readonly ?array $authorizationDetails = null, protected readonly ?string $boundClientId = null, protected readonly ?string $boundRedirectUri = null, + protected ?string $issuer_state = null, ) { $this->identifier = $id; $this->client = $client; @@ -58,6 +65,7 @@ public function __construct( $this->redirectUri = $redirectUri; $this->nonce = $nonce; $this->isRevoked = $isRevoked; + $this->issuerState = $issuer_state; } /** @@ -81,6 +89,7 @@ public function getState(): array null, 'bound_client_id' => $this->boundClientId, 'bound_redirect_uri' => $this->boundRedirectUri, + 'issuer_state' => $this->issuerState, ]; } @@ -113,4 +122,10 @@ public function getBoundRedirectUri(): ?string { return $this->boundRedirectUri; } + + public function getIssuerState(): ?string + { + return $this->issuerState; + } + } diff --git a/src/Factories/Entities/AccessTokenEntityFactory.php b/src/Factories/Entities/AccessTokenEntityFactory.php index b3ed8f3a..2c1a5417 100644 --- a/src/Factories/Entities/AccessTokenEntityFactory.php +++ b/src/Factories/Entities/AccessTokenEntityFactory.php @@ -40,6 +40,7 @@ public function fromData( ?array $authorizationDetails = null, ?string $boundClientId = null, ?string $boundRedirectUri = null, + ?string $issuerState = null, ): AccessTokenEntity { return new AccessTokenEntity( $id, @@ -56,6 +57,7 @@ public function fromData( authorizationDetails: $authorizationDetails, boundClientId: $boundClientId, boundRedirectUri: $boundRedirectUri, + issuerState: $issuerState, ); } diff --git a/src/Factories/Entities/AuthCodeEntityFactory.php b/src/Factories/Entities/AuthCodeEntityFactory.php index 4449e74b..84c50dd0 100644 --- a/src/Factories/Entities/AuthCodeEntityFactory.php +++ b/src/Factories/Entities/AuthCodeEntityFactory.php @@ -31,6 +31,7 @@ public function fromData( ?string $userIdentifier = null, ?string $redirectUri = null, ?string $nonce = null, + ?string $issuer_state = null, bool $isRevoked = false, ?FlowTypeEnum $flowTypeEnum = null, ?string $txCode = null, @@ -52,6 +53,7 @@ public function fromData( $authorizationDetails, $boundClientId, $boundRedirectUri, + $issuer_state, ); } @@ -94,6 +96,7 @@ public function fromState(array $state): AuthCodeEntity $isRevoked = (bool) $state['is_revoked']; $flowType = empty($state['flow_type']) ? null : FlowTypeEnum::tryFrom((string)$state['flow_type']); $txCode = empty($state['tx_code']) ? null : (string)$state['tx_code']; + $issuerState = (string) $state['issuer_state']; /** @psalm-suppress MixedAssignment */ $authorizationDetails = isset($state['authorization_details']) && is_string($state['authorization_details']) ? @@ -112,6 +115,7 @@ public function fromState(array $state): AuthCodeEntity $userIdentifier, $redirectUri, $nonce, + $issuerState, $isRevoked, $flowType, $txCode, diff --git a/src/Factories/RequestRulesManagerFactory.php b/src/Factories/RequestRulesManagerFactory.php index a05bf4f0..26ffa479 100644 --- a/src/Factories/RequestRulesManagerFactory.php +++ b/src/Factories/RequestRulesManagerFactory.php @@ -24,7 +24,6 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Rules\CodeChallengeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\CodeVerifierRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IdTokenHintRule; -use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IssuerStateRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\MaxAgeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\PostLogoutRedirectUriRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\PromptRule; @@ -36,6 +35,7 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ScopeOfflineAccessRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ScopeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\StateRule; +use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IssuerStateRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\UiLocalesRule; use SimpleSAML\Module\oidc\Services\AuthenticationService; use SimpleSAML\Module\oidc\Services\LoggerService; @@ -89,6 +89,7 @@ private function getDefaultRules(): array { return [ new StateRule($this->requestParamsResolver, $this->helpers), + new IssuerStateRule($this->requestParamsResolver, $this->helpers), new ClientRule( $this->requestParamsResolver, $this->helpers, @@ -147,7 +148,6 @@ private function getDefaultRules(): array $this->protocolCache, ), new CodeVerifierRule($this->requestParamsResolver, $this->helpers), - new IssuerStateRule($this->requestParamsResolver, $this->helpers, $this->issuerStateRepository), new AuthorizationDetailsRule($this->requestParamsResolver, $this->helpers, $this->moduleConfig), new ClientIdRule($this->requestParamsResolver, $this->helpers), ]; diff --git a/src/Repositories/AccessTokenRepository.php b/src/Repositories/AccessTokenRepository.php index 9d55097b..ac535490 100644 --- a/src/Repositories/AccessTokenRepository.php +++ b/src/Repositories/AccessTokenRepository.php @@ -113,7 +113,8 @@ public function persistNewAccessToken(OAuth2AccessTokenEntityInterface $accessTo flow_type, authorization_details, bound_client_id, - bound_redirect_uri + bound_redirect_uri, + issuer_state ) " . "VALUES ( :id, @@ -127,7 +128,8 @@ public function persistNewAccessToken(OAuth2AccessTokenEntityInterface $accessTo :flow_type, :authorization_details, :bound_client_id, - :bound_redirect_uri + :bound_redirect_uri, + :issuer_state )", $this->getTableName(), ); @@ -267,7 +269,7 @@ private function update(AccessTokenEntity $accessTokenEntity): void . "client_id = :client_id, is_revoked = :is_revoked, auth_code_id = :auth_code_id, " . "requested_claims = :requested_claims, flow_type = :flow_type, " . "authorization_details = :authorization_details, bound_client_id = :bound_client_id, " . - "bound_redirect_uri = :bound_redirect_uri WHERE id = :id", + "bound_redirect_uri = :bound_redirect_uri, issuer_state = :issuer_state WHERE id = :id", $this->getTableName(), ); diff --git a/src/Repositories/AuthCodeRepository.php b/src/Repositories/AuthCodeRepository.php index a24afdf3..f083cfc8 100644 --- a/src/Repositories/AuthCodeRepository.php +++ b/src/Repositories/AuthCodeRepository.php @@ -84,7 +84,8 @@ public function persistNewAuthCode(OAuth2AuthCodeEntityInterface $authCodeEntity tx_code, authorization_details, bound_client_id, - bound_redirect_uri + bound_redirect_uri, + issuer_state ) VALUES ( :id, :scopes, @@ -98,7 +99,8 @@ public function persistNewAuthCode(OAuth2AuthCodeEntityInterface $authCodeEntity :tx_code, :authorization_details, :bound_client_id, - :bound_redirect_uri + :bound_redirect_uri, + :issuer_state ) EOS, $this->getTableName(), @@ -224,7 +226,8 @@ private function update(AuthCodeEntity $authCodeEntity): void tx_code = :tx_code, authorization_details = :authorization_details, bound_client_id = :bound_client_id, - bound_redirect_uri = :bound_redirect_uri + bound_redirect_uri = :bound_redirect_uri, + issuer_state = :issuer_state WHERE id = :id EOS , diff --git a/src/Server/AuthorizationServer.php b/src/Server/AuthorizationServer.php index 0c0367b4..82c049e3 100644 --- a/src/Server/AuthorizationServer.php +++ b/src/Server/AuthorizationServer.php @@ -23,6 +23,7 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IdTokenHintRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\PostLogoutRedirectUriRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\StateRule; +use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IssuerStateRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\UiLocalesRule; use SimpleSAML\Module\oidc\Server\RequestTypes\LogoutRequest; use SimpleSAML\Module\oidc\Services\LoggerService; @@ -83,6 +84,7 @@ public function validateAuthorizationRequest(ServerRequestInterface $request): O $rulesToExecute = [ StateRule::class, + IssuerStateRule::class, ClientRule::class, ClientRedirectUriRule::class, ]; diff --git a/src/Server/Grants/AuthCodeGrant.php b/src/Server/Grants/AuthCodeGrant.php index 53315271..79196e9a 100644 --- a/src/Server/Grants/AuthCodeGrant.php +++ b/src/Server/Grants/AuthCodeGrant.php @@ -52,7 +52,6 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Rules\CodeChallengeMethodRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\CodeChallengeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\CodeVerifierRule; -use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IssuerStateRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\MaxAgeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\PromptRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\RequestedClaimsRule; @@ -61,6 +60,7 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ScopeOfflineAccessRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ScopeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\StateRule; +use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IssuerStateRule; use SimpleSAML\Module\oidc\Server\RequestTypes\AuthorizationRequest; use SimpleSAML\Module\oidc\Server\ResponseTypes\Interfaces\AcrResponseTypeInterface; use SimpleSAML\Module\oidc\Server\ResponseTypes\Interfaces\AuthTimeResponseTypeInterface; @@ -347,6 +347,7 @@ protected function issueOidcAuthCode( $userIdentifier, $redirectUri, $authorizationRequest->getNonce(), + $authorizationRequest->getIssuerState(), flowTypeEnum: $flowType, authorizationDetails: $authorizationRequest->getAuthorizationDetails(), boundClientId: $authorizationRequest->getBoundClientId(), @@ -603,6 +604,12 @@ public function respondToAccessTokenRequest( json_decode(json_encode($authCodePayload->claims, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR) : null; + $auth_code_id = $authCodePayload->auth_code_id; + $authCodeEntity = $this->authCodeRepository->findById($auth_code_id); + + /** @var string $issuerState */ + $issuerState = $authCodeEntity->getIssuerState(); + // Issue and persist new access token $accessToken = $this->issueAccessToken( $accessTokenTTL, @@ -615,6 +622,7 @@ public function respondToAccessTokenRequest( $storedAuthCodeEntity->getAuthorizationDetails(), $storedAuthCodeEntity->getBoundClientId(), $storedAuthCodeEntity->getBoundRedirectUri(), + $issuerState, ); $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); $responseType->setAccessToken($accessToken); @@ -759,6 +767,8 @@ public function validateAuthorizationRequestWithRequestRules( $redirectUri = $resultBag->getOrFail(ClientRedirectUriRule::class)->getValue(); /** @var string|null $state */ $state = $resultBag->getOrFail(StateRule::class)->getValue(); + /** @var string|null $issuer_state */ + $issuer_state = $resultBag->getOrFail(IssuerStateRule::class)->getValue(); /** @var \SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface $client */ $client = $resultBag->getOrFail(ClientRule::class)->getValue(); @@ -881,9 +891,9 @@ public function validateAuthorizationRequestWithRequestRules( $authorizationRequest->setFlowType($flowType); /** @var ?string $issuerState */ - $issuerState = $resultBag->get(IssuerStateRule::class)?->getValue(); - $this->loggerService->debug('AuthCodeGrant: Issuer state: ', ['issuerState' => $issuerState]); - $authorizationRequest->setIssuerState($issuerState); + if ($issuer_state !== null) { + $authorizationRequest->setIssuerState($issuer_state); + } /** @var ?array $authorizationDetails */ $authorizationDetails = $resultBag->get(AuthorizationDetailsRule::class)?->getValue(); diff --git a/src/Server/Grants/Traits/IssueAccessTokenTrait.php b/src/Server/Grants/Traits/IssueAccessTokenTrait.php index 8ac538e3..488066f5 100644 --- a/src/Server/Grants/Traits/IssueAccessTokenTrait.php +++ b/src/Server/Grants/Traits/IssueAccessTokenTrait.php @@ -55,6 +55,7 @@ protected function issueAccessToken( ?array $authorizationDetails = null, ?string $boundClientId = null, ?string $boundRedirectUri = null, + ?string $issuerState = null, ): AccessTokenEntityInterface { $maxGenerationAttempts = AbstractGrant::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; @@ -79,6 +80,7 @@ protected function issueAccessToken( authorizationDetails: $authorizationDetails, boundClientId: $boundClientId, boundRedirectUri: $boundRedirectUri, + issuerState: $issuerState ); $this->accessTokenRepository->persistNewAccessToken($accessToken); return $accessToken; diff --git a/src/Server/RequestRules/Rules/IssuerStateRule.php b/src/Server/RequestRules/Rules/IssuerStateRule.php index f309b8ba..2526e96c 100644 --- a/src/Server/RequestRules/Rules/IssuerStateRule.php +++ b/src/Server/RequestRules/Rules/IssuerStateRule.php @@ -5,27 +5,15 @@ namespace SimpleSAML\Module\oidc\Server\RequestRules\Rules; use Psr\Http\Message\ServerRequestInterface; -use SimpleSAML\Module\oidc\Helpers; -use SimpleSAML\Module\oidc\Repositories\IssuerStateRepository; -use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException; use SimpleSAML\Module\oidc\Server\RequestRules\Interfaces\ResultBagInterface; use SimpleSAML\Module\oidc\Server\RequestRules\Interfaces\ResultInterface; use SimpleSAML\Module\oidc\Server\RequestRules\Result; use SimpleSAML\Module\oidc\Services\LoggerService; -use SimpleSAML\Module\oidc\Utils\RequestParamsResolver; use SimpleSAML\OpenID\Codebooks\HttpMethodsEnum; use SimpleSAML\OpenID\Codebooks\ParamsEnum; class IssuerStateRule extends AbstractRule { - public function __construct( - RequestParamsResolver $requestParamsResolver, - Helpers $helpers, - protected readonly IssuerStateRepository $issuerStateRepository, - ) { - parent::__construct($requestParamsResolver, $helpers); - } - /** * @inheritDoc */ @@ -37,25 +25,12 @@ public function checkRule( bool $useFragmentInHttpErrorResponses = false, array $allowedServerRequestMethods = [HttpMethodsEnum::GET], ): ?ResultInterface { - $loggerService->debug('IssuerStateRule::checkRule'); - - $issuerState = $this->requestParamsResolver->getAsStringBasedOnAllowedMethods( + $issuer_state = $this->requestParamsResolver->getAsStringBasedOnAllowedMethods( ParamsEnum::IssuerState->value, $request, $allowedServerRequestMethods, ); - if ($issuerState === null) { - return null; - } - - if ($this->issuerStateRepository->findValid($issuerState) === null) { - $loggerService->error('IssuerStateRule: Invalid issuer state: ' . $issuerState); - throw OidcServerException::invalidRequest(ParamsEnum::IssuerState->value); - } - - $loggerService->debug('IssuerStateRule: Valid issuer state: ' . $issuerState); - - return new Result($this->getKey(), $issuerState); + return new Result($this->getKey(), $issuer_state); } } diff --git a/src/Services/DatabaseMigration.php b/src/Services/DatabaseMigration.php index 8256ba90..3f0aa6e4 100644 --- a/src/Services/DatabaseMigration.php +++ b/src/Services/DatabaseMigration.php @@ -199,6 +199,16 @@ public function migrate(): void $this->version20250917163000(); $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20250917163000')"); } + + if (!in_array('20251021000001', $versions, true)) { + $this->version20251021000001(); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20251021000001')"); + } + + if (!in_array('20251021000002', $versions, true)) { + $this->version20251021000002(); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20251021000002')"); + } } private function versionsTableName(): string @@ -678,6 +688,26 @@ private function version20250917163000(): void ,); } + private function version20251021000001(): void + { + $clientTableName = $this->database->applyPrefix(AuthCodeRepository::TABLE_NAME); + $this->database->write(<<< EOT + ALTER TABLE {$clientTableName} + ADD issuer_state TEXT NULL +EOT + ,); + } + + private function version20251021000002(): void + { + $clientTableName = $this->database->applyPrefix(AccessTokenRepository::TABLE_NAME); + $this->database->write(<<< EOT + ALTER TABLE {$clientTableName} + ADD issuer_state TEXT NULL +EOT + ,); + } + /** * @param string[] $columnNames */ From d74d05eeb1825dd5a8649693d2f881a78ca696b5 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Tue, 21 Oct 2025 16:00:30 +0200 Subject: [PATCH 02/22] Fix jwtConfiguration --- src/Server/Validators/BearerTokenValidator.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Server/Validators/BearerTokenValidator.php b/src/Server/Validators/BearerTokenValidator.php index 3eaded18..e4955986 100644 --- a/src/Server/Validators/BearerTokenValidator.php +++ b/src/Server/Validators/BearerTokenValidator.php @@ -75,10 +75,7 @@ protected function initJwtConfiguration(): void $this->jwtConfiguration = Configuration::forSymmetricSigner( $this->moduleConfig->getProtocolSigner(), InMemory::plainText('empty', 'empty'), - ); - - /** @psalm-suppress DeprecatedMethod, ArgumentTypeCoercion */ - $this->jwtConfiguration->setValidationConstraints( + )->withValidationConstraints( new StrictValidAt(new SystemClock(new DateTimeZone(date_default_timezone_get()))), new SignedWith( $this->moduleConfig->getProtocolSigner(), From c53d1c6a53a319b1389266e80c8f059cfbcb04a5 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Wed, 22 Oct 2025 09:20:21 +0200 Subject: [PATCH 03/22] Add credential_configuration_id to scopes --- src/Server/Grants/AuthCodeGrant.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Server/Grants/AuthCodeGrant.php b/src/Server/Grants/AuthCodeGrant.php index 79196e9a..3381e383 100644 --- a/src/Server/Grants/AuthCodeGrant.php +++ b/src/Server/Grants/AuthCodeGrant.php @@ -28,6 +28,7 @@ use SimpleSAML\Module\oidc\Entities\Interfaces\AuthCodeEntityInterface; use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface; use SimpleSAML\Module\oidc\Entities\UserEntity; +use SimpleSAML\Module\oidc\Entities\ScopeEntity; use SimpleSAML\Module\oidc\Factories\Entities\AccessTokenEntityFactory; use SimpleSAML\Module\oidc\Factories\Entities\AuthCodeEntityFactory; use SimpleSAML\Module\oidc\Helpers; @@ -903,6 +904,23 @@ public function validateAuthorizationRequestWithRequestRules( ); $authorizationRequest->setAuthorizationDetails($authorizationDetails); + // TODO This is a band-aid fix for having credential claims in the userinfo endpoint when + // only VCI authorizationDetails are supplied. This requires configuring a matching OIDC scope + // that has all the credential type claims as well. + foreach ($authorizationDetails as $authorizationDetail) { + if ( + (isset($authorizationDetail['type'])) && + ($authorizationDetail['type']) === 'openid_credential' + ) { + $credentialConfigurationId = $authorizationDetail['credential_configuration_id'] ?? null; + if ($credentialConfigurationId !== null) { + array_push($scopes, new ScopeEntity($credentialConfigurationId)); + } + } + } + $this->loggerService->debug('authorizationDetails Resolved Scopes: ', ['scopes' => $scopes]); + $authorizationRequest->setScopes($scopes); + // Check if we are using a generic client for this request. This can happen for non-registered clients // in VCI flows. This can be removed once the VCI clients (wallets) are properly registered using DCR. if ($client->isGeneric()) { From 5f3371370e7da80114bd25808d2257d70b1d8791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 14:36:36 +0200 Subject: [PATCH 04/22] Remove redundant assignment --- src/Entities/AccessTokenEntity.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Entities/AccessTokenEntity.php b/src/Entities/AccessTokenEntity.php index e98e1fbb..0d93ca3a 100644 --- a/src/Entities/AccessTokenEntity.php +++ b/src/Entities/AccessTokenEntity.php @@ -90,7 +90,6 @@ public function __construct( $this->revoke(); } $jwtConfiguration !== null ? $this->jwtConfiguration = $jwtConfiguration : $this->initJwtConfiguration(); - $this->issuerState = $issuerState; } /** From ef4d60277a1c30670d84a8df227ed84f7bbe5336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 14:36:55 +0200 Subject: [PATCH 05/22] Make property readonly --- src/Entities/AccessTokenEntity.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entities/AccessTokenEntity.php b/src/Entities/AccessTokenEntity.php index 0d93ca3a..493ac7e1 100644 --- a/src/Entities/AccessTokenEntity.php +++ b/src/Entities/AccessTokenEntity.php @@ -74,7 +74,7 @@ public function __construct( protected readonly ?array $authorizationDetails = null, protected readonly ?string $boundClientId = null, protected readonly ?string $boundRedirectUri = null, - protected ?string $issuerState = null, + protected readonly ?string $issuerState = null, ) { $this->setIdentifier($id); $this->setClient($clientEntity); From fdfa873fa9133ac5cf8117c698f8067bdbed380a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 14:42:01 +0200 Subject: [PATCH 06/22] Add issuerState getter --- src/Entities/AccessTokenEntity.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Entities/AccessTokenEntity.php b/src/Entities/AccessTokenEntity.php index 493ac7e1..2fef7aaa 100644 --- a/src/Entities/AccessTokenEntity.php +++ b/src/Entities/AccessTokenEntity.php @@ -194,4 +194,9 @@ public function getBoundRedirectUri(): ?string { return $this->boundRedirectUri; } + + public function getIssuerState(): ?string + { + return $this->issuerState; + } } From 9b7a67fa7872ce6a45fd23f1e5f22c71f5b98fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 14:45:51 +0200 Subject: [PATCH 07/22] Remove redundant property initialization --- src/Entities/AuthCodeEntity.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Entities/AuthCodeEntity.php b/src/Entities/AuthCodeEntity.php index d38c8ec5..977dacc9 100644 --- a/src/Entities/AuthCodeEntity.php +++ b/src/Entities/AuthCodeEntity.php @@ -32,12 +32,6 @@ class AuthCodeEntity implements AuthCodeEntityInterface, MementoInterface use OidcAuthCodeTrait; use RevokeTokenTrait; - /** - * issuer state - * @var string $issuerState - */ - protected ?string $issuerState = null; - /** * @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes */ @@ -55,7 +49,7 @@ public function __construct( protected readonly ?array $authorizationDetails = null, protected readonly ?string $boundClientId = null, protected readonly ?string $boundRedirectUri = null, - protected ?string $issuer_state = null, + protected readonly ?string $issuerState = null, ) { $this->identifier = $id; $this->client = $client; @@ -65,7 +59,6 @@ public function __construct( $this->redirectUri = $redirectUri; $this->nonce = $nonce; $this->isRevoked = $isRevoked; - $this->issuerState = $issuer_state; } /** From ae334499ccc30dacd72345bbaf9ce4d112635231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 14:52:26 +0200 Subject: [PATCH 08/22] Make issuerState nullable --- src/Factories/Entities/AuthCodeEntityFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Factories/Entities/AuthCodeEntityFactory.php b/src/Factories/Entities/AuthCodeEntityFactory.php index 84c50dd0..aa2f841b 100644 --- a/src/Factories/Entities/AuthCodeEntityFactory.php +++ b/src/Factories/Entities/AuthCodeEntityFactory.php @@ -96,7 +96,7 @@ public function fromState(array $state): AuthCodeEntity $isRevoked = (bool) $state['is_revoked']; $flowType = empty($state['flow_type']) ? null : FlowTypeEnum::tryFrom((string)$state['flow_type']); $txCode = empty($state['tx_code']) ? null : (string)$state['tx_code']; - $issuerState = (string) $state['issuer_state']; + $issuerState = empty($state['issuer_state']) ? null : (string)$state['issuer_state']; /** @psalm-suppress MixedAssignment */ $authorizationDetails = isset($state['authorization_details']) && is_string($state['authorization_details']) ? From 4db6b3af04de6045bc0f3b9c7d3be5cc7168bae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 14:53:21 +0200 Subject: [PATCH 09/22] Use came-case for variable name --- src/Factories/Entities/AuthCodeEntityFactory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Factories/Entities/AuthCodeEntityFactory.php b/src/Factories/Entities/AuthCodeEntityFactory.php index aa2f841b..0304b804 100644 --- a/src/Factories/Entities/AuthCodeEntityFactory.php +++ b/src/Factories/Entities/AuthCodeEntityFactory.php @@ -31,7 +31,7 @@ public function fromData( ?string $userIdentifier = null, ?string $redirectUri = null, ?string $nonce = null, - ?string $issuer_state = null, + ?string $issuerState = null, bool $isRevoked = false, ?FlowTypeEnum $flowTypeEnum = null, ?string $txCode = null, @@ -53,7 +53,7 @@ public function fromData( $authorizationDetails, $boundClientId, $boundRedirectUri, - $issuer_state, + $issuerState, ); } From 6e7d24a53dde1851f2afae60f4edecf21a501c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 15:01:54 +0200 Subject: [PATCH 10/22] Use already available authCodeEntity instance to get issuerState --- src/Server/Grants/AuthCodeGrant.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Server/Grants/AuthCodeGrant.php b/src/Server/Grants/AuthCodeGrant.php index 3381e383..76947721 100644 --- a/src/Server/Grants/AuthCodeGrant.php +++ b/src/Server/Grants/AuthCodeGrant.php @@ -605,12 +605,6 @@ public function respondToAccessTokenRequest( json_decode(json_encode($authCodePayload->claims, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR) : null; - $auth_code_id = $authCodePayload->auth_code_id; - $authCodeEntity = $this->authCodeRepository->findById($auth_code_id); - - /** @var string $issuerState */ - $issuerState = $authCodeEntity->getIssuerState(); - // Issue and persist new access token $accessToken = $this->issueAccessToken( $accessTokenTTL, @@ -623,7 +617,7 @@ public function respondToAccessTokenRequest( $storedAuthCodeEntity->getAuthorizationDetails(), $storedAuthCodeEntity->getBoundClientId(), $storedAuthCodeEntity->getBoundRedirectUri(), - $issuerState, + $storedAuthCodeEntity->getIssuerState(), ); $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); $responseType->setAccessToken($accessToken); From 80f2c6739440a0bc1906d6870985982b5886f73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 15:09:54 +0200 Subject: [PATCH 11/22] Use more fluid way to fetch and set issuerState --- src/Server/Grants/AuthCodeGrant.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Server/Grants/AuthCodeGrant.php b/src/Server/Grants/AuthCodeGrant.php index 76947721..00d5e8f7 100644 --- a/src/Server/Grants/AuthCodeGrant.php +++ b/src/Server/Grants/AuthCodeGrant.php @@ -762,8 +762,6 @@ public function validateAuthorizationRequestWithRequestRules( $redirectUri = $resultBag->getOrFail(ClientRedirectUriRule::class)->getValue(); /** @var string|null $state */ $state = $resultBag->getOrFail(StateRule::class)->getValue(); - /** @var string|null $issuer_state */ - $issuer_state = $resultBag->getOrFail(IssuerStateRule::class)->getValue(); /** @var \SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface $client */ $client = $resultBag->getOrFail(ClientRule::class)->getValue(); @@ -886,9 +884,9 @@ public function validateAuthorizationRequestWithRequestRules( $authorizationRequest->setFlowType($flowType); /** @var ?string $issuerState */ - if ($issuer_state !== null) { - $authorizationRequest->setIssuerState($issuer_state); - } + $issuerState = $resultBag->get(IssuerStateRule::class)?->getValue(); + $this->loggerService->debug('AuthCodeGrant: Issuer state: ', ['issuerState' => $issuerState]); + $authorizationRequest->setIssuerState($issuerState); /** @var ?array $authorizationDetails */ $authorizationDetails = $resultBag->get(AuthorizationDetailsRule::class)?->getValue(); From 30b1ffeade6616a03305a9dec95f564102a59b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 15:16:55 +0200 Subject: [PATCH 12/22] Remove unneeded IssuerStateRepository --- src/Factories/RequestRulesManagerFactory.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Factories/RequestRulesManagerFactory.php b/src/Factories/RequestRulesManagerFactory.php index 26ffa479..25856c4e 100644 --- a/src/Factories/RequestRulesManagerFactory.php +++ b/src/Factories/RequestRulesManagerFactory.php @@ -66,7 +66,6 @@ public function __construct( private readonly JwksResolver $jwksResolver, private readonly FederationParticipationValidator $federationParticipationValidator, private readonly SspBridge $sspBridge, - private readonly IssuerStateRepository $issuerStateRepository, private readonly ?FederationCache $federationCache = null, private readonly ?ProtocolCache $protocolCache = null, ) { From 1dd4f7811e9f6b605b995621e87fa452b8828616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 15:18:34 +0200 Subject: [PATCH 13/22] Use camel case variable name --- src/Server/RequestRules/Rules/IssuerStateRule.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Server/RequestRules/Rules/IssuerStateRule.php b/src/Server/RequestRules/Rules/IssuerStateRule.php index 2526e96c..7ba9bf2d 100644 --- a/src/Server/RequestRules/Rules/IssuerStateRule.php +++ b/src/Server/RequestRules/Rules/IssuerStateRule.php @@ -25,12 +25,12 @@ public function checkRule( bool $useFragmentInHttpErrorResponses = false, array $allowedServerRequestMethods = [HttpMethodsEnum::GET], ): ?ResultInterface { - $issuer_state = $this->requestParamsResolver->getAsStringBasedOnAllowedMethods( + $issuerState = $this->requestParamsResolver->getAsStringBasedOnAllowedMethods( ParamsEnum::IssuerState->value, $request, $allowedServerRequestMethods, ); - return new Result($this->getKey(), $issuer_state); + return new Result($this->getKey(), $issuerState); } } From eb0d6d149f4fd41781f2f9c98356c4ffd3956609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 15:49:12 +0200 Subject: [PATCH 14/22] Don't run IssuerStateRule for all flows IssuerStateRule is already set to be executed on AuthCodeGrant: src/Server/Grants/AuthCodeGrant.php:754 --- src/Server/AuthorizationServer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Server/AuthorizationServer.php b/src/Server/AuthorizationServer.php index 82c049e3..d4d60f62 100644 --- a/src/Server/AuthorizationServer.php +++ b/src/Server/AuthorizationServer.php @@ -84,7 +84,6 @@ public function validateAuthorizationRequest(ServerRequestInterface $request): O $rulesToExecute = [ StateRule::class, - IssuerStateRule::class, ClientRule::class, ClientRedirectUriRule::class, ]; From 0c97643d02592233175aa65f87d29165a708b3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 15:56:02 +0200 Subject: [PATCH 15/22] Use proper variable name --- src/Services/DatabaseMigration.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Services/DatabaseMigration.php b/src/Services/DatabaseMigration.php index 3f0aa6e4..06020ef2 100644 --- a/src/Services/DatabaseMigration.php +++ b/src/Services/DatabaseMigration.php @@ -690,9 +690,9 @@ private function version20250917163000(): void private function version20251021000001(): void { - $clientTableName = $this->database->applyPrefix(AuthCodeRepository::TABLE_NAME); + $authCodeTableName = $this->database->applyPrefix(AuthCodeRepository::TABLE_NAME); $this->database->write(<<< EOT - ALTER TABLE {$clientTableName} + ALTER TABLE {$authCodeTableName} ADD issuer_state TEXT NULL EOT ,); @@ -700,9 +700,9 @@ private function version20251021000001(): void private function version20251021000002(): void { - $clientTableName = $this->database->applyPrefix(AccessTokenRepository::TABLE_NAME); + $accessTokenTableName = $this->database->applyPrefix(AccessTokenRepository::TABLE_NAME); $this->database->write(<<< EOT - ALTER TABLE {$clientTableName} + ALTER TABLE {$accessTokenTableName} ADD issuer_state TEXT NULL EOT ,); From 82af8a80716af07dbe9a6430da818828c678ade6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 15:59:00 +0200 Subject: [PATCH 16/22] phpcbf lint --- src/Entities/AuthCodeEntity.php | 1 - src/Factories/RequestRulesManagerFactory.php | 3 +-- src/Server/AuthorizationServer.php | 1 - src/Server/Grants/AuthCodeGrant.php | 4 ++-- src/Server/Grants/Traits/IssueAccessTokenTrait.php | 2 +- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Entities/AuthCodeEntity.php b/src/Entities/AuthCodeEntity.php index 977dacc9..2cacb7e1 100644 --- a/src/Entities/AuthCodeEntity.php +++ b/src/Entities/AuthCodeEntity.php @@ -120,5 +120,4 @@ public function getIssuerState(): ?string { return $this->issuerState; } - } diff --git a/src/Factories/RequestRulesManagerFactory.php b/src/Factories/RequestRulesManagerFactory.php index 25856c4e..27de4e3c 100644 --- a/src/Factories/RequestRulesManagerFactory.php +++ b/src/Factories/RequestRulesManagerFactory.php @@ -10,7 +10,6 @@ use SimpleSAML\Module\oidc\ModuleConfig; use SimpleSAML\Module\oidc\Repositories\ClientRepository; use SimpleSAML\Module\oidc\Repositories\CodeChallengeVerifiersRepository; -use SimpleSAML\Module\oidc\Repositories\IssuerStateRepository; use SimpleSAML\Module\oidc\Repositories\ScopeRepository; use SimpleSAML\Module\oidc\Server\RequestRules\RequestRulesManager; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\AcrValuesRule; @@ -24,6 +23,7 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Rules\CodeChallengeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\CodeVerifierRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IdTokenHintRule; +use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IssuerStateRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\MaxAgeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\PostLogoutRedirectUriRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\PromptRule; @@ -35,7 +35,6 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ScopeOfflineAccessRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ScopeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\StateRule; -use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IssuerStateRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\UiLocalesRule; use SimpleSAML\Module\oidc\Services\AuthenticationService; use SimpleSAML\Module\oidc\Services\LoggerService; diff --git a/src/Server/AuthorizationServer.php b/src/Server/AuthorizationServer.php index d4d60f62..0c0367b4 100644 --- a/src/Server/AuthorizationServer.php +++ b/src/Server/AuthorizationServer.php @@ -23,7 +23,6 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IdTokenHintRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\PostLogoutRedirectUriRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\StateRule; -use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IssuerStateRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\UiLocalesRule; use SimpleSAML\Module\oidc\Server\RequestTypes\LogoutRequest; use SimpleSAML\Module\oidc\Services\LoggerService; diff --git a/src/Server/Grants/AuthCodeGrant.php b/src/Server/Grants/AuthCodeGrant.php index 00d5e8f7..2d93acd0 100644 --- a/src/Server/Grants/AuthCodeGrant.php +++ b/src/Server/Grants/AuthCodeGrant.php @@ -27,8 +27,8 @@ use SimpleSAML\Module\oidc\Entities\Interfaces\AccessTokenEntityInterface; use SimpleSAML\Module\oidc\Entities\Interfaces\AuthCodeEntityInterface; use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface; -use SimpleSAML\Module\oidc\Entities\UserEntity; use SimpleSAML\Module\oidc\Entities\ScopeEntity; +use SimpleSAML\Module\oidc\Entities\UserEntity; use SimpleSAML\Module\oidc\Factories\Entities\AccessTokenEntityFactory; use SimpleSAML\Module\oidc\Factories\Entities\AuthCodeEntityFactory; use SimpleSAML\Module\oidc\Helpers; @@ -53,6 +53,7 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Rules\CodeChallengeMethodRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\CodeChallengeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\CodeVerifierRule; +use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IssuerStateRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\MaxAgeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\PromptRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\RequestedClaimsRule; @@ -61,7 +62,6 @@ use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ScopeOfflineAccessRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ScopeRule; use SimpleSAML\Module\oidc\Server\RequestRules\Rules\StateRule; -use SimpleSAML\Module\oidc\Server\RequestRules\Rules\IssuerStateRule; use SimpleSAML\Module\oidc\Server\RequestTypes\AuthorizationRequest; use SimpleSAML\Module\oidc\Server\ResponseTypes\Interfaces\AcrResponseTypeInterface; use SimpleSAML\Module\oidc\Server\ResponseTypes\Interfaces\AuthTimeResponseTypeInterface; diff --git a/src/Server/Grants/Traits/IssueAccessTokenTrait.php b/src/Server/Grants/Traits/IssueAccessTokenTrait.php index 488066f5..6e8f47b6 100644 --- a/src/Server/Grants/Traits/IssueAccessTokenTrait.php +++ b/src/Server/Grants/Traits/IssueAccessTokenTrait.php @@ -80,7 +80,7 @@ protected function issueAccessToken( authorizationDetails: $authorizationDetails, boundClientId: $boundClientId, boundRedirectUri: $boundRedirectUri, - issuerState: $issuerState + issuerState: $issuerState, ); $this->accessTokenRepository->persistNewAccessToken($accessToken); return $accessToken; From 01cb611fb9fce1317fe0f1cb40fbc0258249c7fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 16:03:21 +0200 Subject: [PATCH 17/22] Add a few more checks when resolving credentialConfigurationId --- src/Server/Grants/AuthCodeGrant.php | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Server/Grants/AuthCodeGrant.php b/src/Server/Grants/AuthCodeGrant.php index 2d93acd0..5693e220 100644 --- a/src/Server/Grants/AuthCodeGrant.php +++ b/src/Server/Grants/AuthCodeGrant.php @@ -899,19 +899,24 @@ public function validateAuthorizationRequestWithRequestRules( // TODO This is a band-aid fix for having credential claims in the userinfo endpoint when // only VCI authorizationDetails are supplied. This requires configuring a matching OIDC scope // that has all the credential type claims as well. - foreach ($authorizationDetails as $authorizationDetail) { - if ( - (isset($authorizationDetail['type'])) && - ($authorizationDetail['type']) === 'openid_credential' - ) { - $credentialConfigurationId = $authorizationDetail['credential_configuration_id'] ?? null; - if ($credentialConfigurationId !== null) { - array_push($scopes, new ScopeEntity($credentialConfigurationId)); + if (is_array($authorizationDetails)) { + /** @psalm-suppress MixedAssignment */ + foreach ($authorizationDetails as $authorizationDetail) { + if ( + is_array($authorizationDetail) && + (isset($authorizationDetail['type'])) && + ($authorizationDetail['type']) === 'openid_credential' + ) { + /** @psalm-suppress MixedAssignment */ + $credentialConfigurationId = $authorizationDetail['credential_configuration_id'] ?? null; + if (is_string($credentialConfigurationId)) { + $scopes[] = new ScopeEntity($credentialConfigurationId); + } } } + $this->loggerService->debug('authorizationDetails Resolved Scopes: ', ['scopes' => $scopes]); + $authorizationRequest->setScopes($scopes); } - $this->loggerService->debug('authorizationDetails Resolved Scopes: ', ['scopes' => $scopes]); - $authorizationRequest->setScopes($scopes); // Check if we are using a generic client for this request. This can happen for non-registered clients // in VCI flows. This can be removed once the VCI clients (wallets) are properly registered using DCR. From 9027e7078279539df10227d8b3ccb606dc7f5c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 16:11:57 +0200 Subject: [PATCH 18/22] Suppress psalm ArgumentTypeCoercion warning --- src/Server/Validators/BearerTokenValidator.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Server/Validators/BearerTokenValidator.php b/src/Server/Validators/BearerTokenValidator.php index e4955986..b1afbd23 100644 --- a/src/Server/Validators/BearerTokenValidator.php +++ b/src/Server/Validators/BearerTokenValidator.php @@ -72,6 +72,7 @@ public function setPublicKey(CryptKey $key): void */ protected function initJwtConfiguration(): void { + /** @psalm-suppress ArgumentTypeCoercion */ $this->jwtConfiguration = Configuration::forSymmetricSigner( $this->moduleConfig->getProtocolSigner(), InMemory::plainText('empty', 'empty'), From b74549fc1939aa71c8c1cf6cd9f4ecebe33bf069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 16:13:50 +0200 Subject: [PATCH 19/22] Update AuthCodeEntityTest --- tests/unit/src/Entities/AuthCodeEntityTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/src/Entities/AuthCodeEntityTest.php b/tests/unit/src/Entities/AuthCodeEntityTest.php index 877420eb..6b0be802 100644 --- a/tests/unit/src/Entities/AuthCodeEntityTest.php +++ b/tests/unit/src/Entities/AuthCodeEntityTest.php @@ -106,6 +106,7 @@ public function testCanGetState(): void 'authorization_details' => null, 'bound_client_id' => null, 'bound_redirect_uri' => null, + 'issuer_state' => null, ], ); } From 9e99a83586b955c1f2227dfc3e21eb795875ce45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 16:27:07 +0200 Subject: [PATCH 20/22] Update AccessTokenRepositoryTest --- tests/integration/src/Repositories/AccessTokenRepositoryTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/src/Repositories/AccessTokenRepositoryTest.php b/tests/integration/src/Repositories/AccessTokenRepositoryTest.php index 1eab1395..6ace21ac 100644 --- a/tests/integration/src/Repositories/AccessTokenRepositoryTest.php +++ b/tests/integration/src/Repositories/AccessTokenRepositoryTest.php @@ -120,6 +120,7 @@ public function setUp(): void 'authorization_details' => null, 'bound_client_id' => null, 'bound_redirect_uri' => null, + 'issuer_state' => null, ]; $this->accessTokenEntityMock = $this->createMock(AccessTokenEntity::class); From 5406a0c6b359b4ab0cf08e1ec9b0b223f8909734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 22 Oct 2025 16:32:29 +0200 Subject: [PATCH 21/22] Update conformance.sql --- docker/conformance.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/conformance.sql b/docker/conformance.sql index 42aa9078..bf09b946 100644 --- a/docker/conformance.sql +++ b/docker/conformance.sql @@ -65,6 +65,7 @@ CREATE TABLE oidc_access_token ( authorization_details TEXT NULL, bound_client_id TEXT NULL, bound_redirect_uri TEXT NULL, + issuer_state TEXT NULL, CONSTRAINT FK_43C1650EA76ED395 FOREIGN KEY (user_id) REFERENCES oidc_user (id) ON DELETE CASCADE, CONSTRAINT FK_43C1650E19EB6921 FOREIGN KEY (client_id) @@ -92,6 +93,7 @@ CREATE TABLE oidc_auth_code ( authorization_details TEXT NULL, bound_client_id TEXT NULL, bound_redirect_uri TEXT NULL, + issuer_state TEXT NULL, CONSTRAINT FK_97D32CA7A76ED395 FOREIGN KEY (user_id) REFERENCES oidc_user (id) ON DELETE CASCADE, CONSTRAINT FK_97D32CA719EB6921 FOREIGN KEY (client_id) From ce15fae75dc06cfd80d0bebc4a82c8c65c8d9c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Thu, 23 Oct 2025 13:29:53 +0200 Subject: [PATCH 22/22] Check issuer state during credential issuance --- .../CredentialIssuerCredentialController.php | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php b/src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php index 728356dd..c4ee2e85 100644 --- a/src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php +++ b/src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php @@ -10,6 +10,7 @@ use SimpleSAML\Module\oidc\Entities\AccessTokenEntity; use SimpleSAML\Module\oidc\ModuleConfig; use SimpleSAML\Module\oidc\Repositories\AccessTokenRepository; +use SimpleSAML\Module\oidc\Repositories\IssuerStateRepository; use SimpleSAML\Module\oidc\Repositories\UserRepository; use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException; use SimpleSAML\Module\oidc\Services\LoggerService; @@ -53,6 +54,7 @@ public function __construct( protected readonly RequestParamsResolver $requestParamsResolver, protected readonly UserRepository $userRepository, protected readonly Did $did, + protected readonly IssuerStateRepository $issuerStateRepository, ) { if (!$this->moduleConfig->getVerifiableCredentialEnabled()) { $this->loggerService->warning('Verifiable Credential capabilities not enabled.'); @@ -121,6 +123,30 @@ public function credential(Request $request): Response ); } + $issuerState = $accessToken->getIssuerState(); + if (!is_string($issuerState)) { + $this->loggerService->error( + 'CredentialIssuerCredentialController::credential: Issuer state missing in access token.', + ['access_token' => $accessToken], + ); + return $this->routes->newJsonErrorResponse( + 'invalid_token', + 'Issuer state missing in access token.', + 401, + ); + } + + if ($this->issuerStateRepository->findValid($issuerState) === null) { + $this->loggerService->warning( + 'CredentialIssuerCredentialController::credential: Issuer state not valid.', + ['issuer_state' => $issuerState], + ); + return $this->routes->newJsonErrorResponse( + 'invalid_token', + 'Issuer state not valid.', + ); + } + if ( isset($requestData[ClaimsEnum::CredentialConfigurationId->value]) && isset($requestData[ClaimsEnum::CredentialIdentifier->value]) @@ -652,7 +678,11 @@ public function credential(Request $request): Response throw new OpenIdException('Invalid credential format ID.'); } - $this->loggerService->debug('response', [ + $this->loggerService->debug('Revoking issuer state.', ['issuerState' => $issuerState]); + ; + $this->issuerStateRepository->revoke($issuerState); + + $this->loggerService->debug('Returning credential response.', [ 'credentials' => [ ['credential' => $verifiableCredential->getToken()], ],