Skip to content

Commit 07e7977

Browse files
mrvanescicnavi
andauthored
Reflect issuer_state in access_token (#317)
Separate the VC issuer from authorization server. The `issuer_state` is not checked in the authorization endpoint anymore, but simply accepted as an opaque identifier and afterwards reflected in the access token. The issuer can compare the previously offered `issuer_state` against the `issuer_state` in the access token. --------- Co-authored-by: Marko Ivančić <[email protected]>
1 parent 092bf54 commit 07e7977

File tree

16 files changed

+129
-39
lines changed

16 files changed

+129
-39
lines changed

docker/conformance.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ CREATE TABLE oidc_access_token (
6565
authorization_details TEXT NULL,
6666
bound_client_id TEXT NULL,
6767
bound_redirect_uri TEXT NULL,
68+
issuer_state TEXT NULL,
6869
CONSTRAINT FK_43C1650EA76ED395 FOREIGN KEY (user_id)
6970
REFERENCES oidc_user (id) ON DELETE CASCADE,
7071
CONSTRAINT FK_43C1650E19EB6921 FOREIGN KEY (client_id)
@@ -92,6 +93,7 @@ CREATE TABLE oidc_auth_code (
9293
authorization_details TEXT NULL,
9394
bound_client_id TEXT NULL,
9495
bound_redirect_uri TEXT NULL,
96+
issuer_state TEXT NULL,
9597
CONSTRAINT FK_97D32CA7A76ED395 FOREIGN KEY (user_id)
9698
REFERENCES oidc_user (id) ON DELETE CASCADE,
9799
CONSTRAINT FK_97D32CA719EB6921 FOREIGN KEY (client_id)

src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use SimpleSAML\Module\oidc\Entities\AccessTokenEntity;
1111
use SimpleSAML\Module\oidc\ModuleConfig;
1212
use SimpleSAML\Module\oidc\Repositories\AccessTokenRepository;
13+
use SimpleSAML\Module\oidc\Repositories\IssuerStateRepository;
1314
use SimpleSAML\Module\oidc\Repositories\UserRepository;
1415
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
1516
use SimpleSAML\Module\oidc\Services\LoggerService;
@@ -53,6 +54,7 @@ public function __construct(
5354
protected readonly RequestParamsResolver $requestParamsResolver,
5455
protected readonly UserRepository $userRepository,
5556
protected readonly Did $did,
57+
protected readonly IssuerStateRepository $issuerStateRepository,
5658
) {
5759
if (!$this->moduleConfig->getVerifiableCredentialEnabled()) {
5860
$this->loggerService->warning('Verifiable Credential capabilities not enabled.');
@@ -121,6 +123,30 @@ public function credential(Request $request): Response
121123
);
122124
}
123125

126+
$issuerState = $accessToken->getIssuerState();
127+
if (!is_string($issuerState)) {
128+
$this->loggerService->error(
129+
'CredentialIssuerCredentialController::credential: Issuer state missing in access token.',
130+
['access_token' => $accessToken],
131+
);
132+
return $this->routes->newJsonErrorResponse(
133+
'invalid_token',
134+
'Issuer state missing in access token.',
135+
401,
136+
);
137+
}
138+
139+
if ($this->issuerStateRepository->findValid($issuerState) === null) {
140+
$this->loggerService->warning(
141+
'CredentialIssuerCredentialController::credential: Issuer state not valid.',
142+
['issuer_state' => $issuerState],
143+
);
144+
return $this->routes->newJsonErrorResponse(
145+
'invalid_token',
146+
'Issuer state not valid.',
147+
);
148+
}
149+
124150
if (
125151
isset($requestData[ClaimsEnum::CredentialConfigurationId->value]) &&
126152
isset($requestData[ClaimsEnum::CredentialIdentifier->value])
@@ -652,7 +678,11 @@ public function credential(Request $request): Response
652678
throw new OpenIdException('Invalid credential format ID.');
653679
}
654680

655-
$this->loggerService->debug('response', [
681+
$this->loggerService->debug('Revoking issuer state.', ['issuerState' => $issuerState]);
682+
;
683+
$this->issuerStateRepository->revoke($issuerState);
684+
685+
$this->loggerService->debug('Returning credential response.', [
656686
'credentials' => [
657687
['credential' => $verifiableCredential->getToken()],
658688
],

src/Entities/AccessTokenEntity.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public function __construct(
7474
protected readonly ?array $authorizationDetails = null,
7575
protected readonly ?string $boundClientId = null,
7676
protected readonly ?string $boundRedirectUri = null,
77+
protected readonly ?string $issuerState = null,
7778
) {
7879
$this->setIdentifier($id);
7980
$this->setClient($clientEntity);
@@ -125,6 +126,7 @@ public function getState(): array
125126
null,
126127
'bound_client_id' => $this->boundClientId,
127128
'bound_redirect_uri' => $this->boundRedirectUri,
129+
'issuer_state' => $this->issuerState,
128130
];
129131
}
130132

@@ -166,6 +168,9 @@ protected function convertToJWT(): Token
166168
->expiresAt($this->getExpiryDateTime())
167169
->relatedTo((string) $this->getUserIdentifier())
168170
->withClaim('scopes', $this->getScopes());
171+
if ($this->issuerState !== null) {
172+
$jwtBuilder = $jwtBuilder->withClaim('issuer_state', $this->issuerState);
173+
}
169174

170175
return $this->jsonWebTokenBuilderService->getSignedProtocolJwt($jwtBuilder);
171176
}
@@ -189,4 +194,9 @@ public function getBoundRedirectUri(): ?string
189194
{
190195
return $this->boundRedirectUri;
191196
}
197+
198+
public function getIssuerState(): ?string
199+
{
200+
return $this->issuerState;
201+
}
192202
}

src/Entities/AuthCodeEntity.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public function __construct(
4949
protected readonly ?array $authorizationDetails = null,
5050
protected readonly ?string $boundClientId = null,
5151
protected readonly ?string $boundRedirectUri = null,
52+
protected readonly ?string $issuerState = null,
5253
) {
5354
$this->identifier = $id;
5455
$this->client = $client;
@@ -81,6 +82,7 @@ public function getState(): array
8182
null,
8283
'bound_client_id' => $this->boundClientId,
8384
'bound_redirect_uri' => $this->boundRedirectUri,
85+
'issuer_state' => $this->issuerState,
8486
];
8587
}
8688

@@ -113,4 +115,9 @@ public function getBoundRedirectUri(): ?string
113115
{
114116
return $this->boundRedirectUri;
115117
}
118+
119+
public function getIssuerState(): ?string
120+
{
121+
return $this->issuerState;
122+
}
116123
}

src/Factories/Entities/AccessTokenEntityFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public function fromData(
4040
?array $authorizationDetails = null,
4141
?string $boundClientId = null,
4242
?string $boundRedirectUri = null,
43+
?string $issuerState = null,
4344
): AccessTokenEntity {
4445
return new AccessTokenEntity(
4546
$id,
@@ -56,6 +57,7 @@ public function fromData(
5657
authorizationDetails: $authorizationDetails,
5758
boundClientId: $boundClientId,
5859
boundRedirectUri: $boundRedirectUri,
60+
issuerState: $issuerState,
5961
);
6062
}
6163

src/Factories/Entities/AuthCodeEntityFactory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public function fromData(
3131
?string $userIdentifier = null,
3232
?string $redirectUri = null,
3333
?string $nonce = null,
34+
?string $issuerState = null,
3435
bool $isRevoked = false,
3536
?FlowTypeEnum $flowTypeEnum = null,
3637
?string $txCode = null,
@@ -52,6 +53,7 @@ public function fromData(
5253
$authorizationDetails,
5354
$boundClientId,
5455
$boundRedirectUri,
56+
$issuerState,
5557
);
5658
}
5759

@@ -94,6 +96,7 @@ public function fromState(array $state): AuthCodeEntity
9496
$isRevoked = (bool) $state['is_revoked'];
9597
$flowType = empty($state['flow_type']) ? null : FlowTypeEnum::tryFrom((string)$state['flow_type']);
9698
$txCode = empty($state['tx_code']) ? null : (string)$state['tx_code'];
99+
$issuerState = empty($state['issuer_state']) ? null : (string)$state['issuer_state'];
97100

98101
/** @psalm-suppress MixedAssignment */
99102
$authorizationDetails = isset($state['authorization_details']) && is_string($state['authorization_details']) ?
@@ -112,6 +115,7 @@ public function fromState(array $state): AuthCodeEntity
112115
$userIdentifier,
113116
$redirectUri,
114117
$nonce,
118+
$issuerState,
115119
$isRevoked,
116120
$flowType,
117121
$txCode,

src/Factories/RequestRulesManagerFactory.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use SimpleSAML\Module\oidc\ModuleConfig;
1111
use SimpleSAML\Module\oidc\Repositories\ClientRepository;
1212
use SimpleSAML\Module\oidc\Repositories\CodeChallengeVerifiersRepository;
13-
use SimpleSAML\Module\oidc\Repositories\IssuerStateRepository;
1413
use SimpleSAML\Module\oidc\Repositories\ScopeRepository;
1514
use SimpleSAML\Module\oidc\Server\RequestRules\RequestRulesManager;
1615
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\AcrValuesRule;
@@ -66,7 +65,6 @@ public function __construct(
6665
private readonly JwksResolver $jwksResolver,
6766
private readonly FederationParticipationValidator $federationParticipationValidator,
6867
private readonly SspBridge $sspBridge,
69-
private readonly IssuerStateRepository $issuerStateRepository,
7068
private readonly ?FederationCache $federationCache = null,
7169
private readonly ?ProtocolCache $protocolCache = null,
7270
) {
@@ -89,6 +87,7 @@ private function getDefaultRules(): array
8987
{
9088
return [
9189
new StateRule($this->requestParamsResolver, $this->helpers),
90+
new IssuerStateRule($this->requestParamsResolver, $this->helpers),
9291
new ClientRule(
9392
$this->requestParamsResolver,
9493
$this->helpers,
@@ -147,7 +146,6 @@ private function getDefaultRules(): array
147146
$this->protocolCache,
148147
),
149148
new CodeVerifierRule($this->requestParamsResolver, $this->helpers),
150-
new IssuerStateRule($this->requestParamsResolver, $this->helpers, $this->issuerStateRepository),
151149
new AuthorizationDetailsRule($this->requestParamsResolver, $this->helpers, $this->moduleConfig),
152150
new ClientIdRule($this->requestParamsResolver, $this->helpers),
153151
];

src/Repositories/AccessTokenRepository.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ public function persistNewAccessToken(OAuth2AccessTokenEntityInterface $accessTo
113113
flow_type,
114114
authorization_details,
115115
bound_client_id,
116-
bound_redirect_uri
116+
bound_redirect_uri,
117+
issuer_state
117118
) "
118119
. "VALUES (
119120
:id,
@@ -127,7 +128,8 @@ public function persistNewAccessToken(OAuth2AccessTokenEntityInterface $accessTo
127128
:flow_type,
128129
:authorization_details,
129130
:bound_client_id,
130-
:bound_redirect_uri
131+
:bound_redirect_uri,
132+
:issuer_state
131133
)",
132134
$this->getTableName(),
133135
);
@@ -267,7 +269,7 @@ private function update(AccessTokenEntity $accessTokenEntity): void
267269
. "client_id = :client_id, is_revoked = :is_revoked, auth_code_id = :auth_code_id, "
268270
. "requested_claims = :requested_claims, flow_type = :flow_type, " .
269271
"authorization_details = :authorization_details, bound_client_id = :bound_client_id, " .
270-
"bound_redirect_uri = :bound_redirect_uri WHERE id = :id",
272+
"bound_redirect_uri = :bound_redirect_uri, issuer_state = :issuer_state WHERE id = :id",
271273
$this->getTableName(),
272274
);
273275

src/Repositories/AuthCodeRepository.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ public function persistNewAuthCode(OAuth2AuthCodeEntityInterface $authCodeEntity
8484
tx_code,
8585
authorization_details,
8686
bound_client_id,
87-
bound_redirect_uri
87+
bound_redirect_uri,
88+
issuer_state
8889
) VALUES (
8990
:id,
9091
:scopes,
@@ -98,7 +99,8 @@ public function persistNewAuthCode(OAuth2AuthCodeEntityInterface $authCodeEntity
9899
:tx_code,
99100
:authorization_details,
100101
:bound_client_id,
101-
:bound_redirect_uri
102+
:bound_redirect_uri,
103+
:issuer_state
102104
)
103105
EOS,
104106
$this->getTableName(),
@@ -224,7 +226,8 @@ private function update(AuthCodeEntity $authCodeEntity): void
224226
tx_code = :tx_code,
225227
authorization_details = :authorization_details,
226228
bound_client_id = :bound_client_id,
227-
bound_redirect_uri = :bound_redirect_uri
229+
bound_redirect_uri = :bound_redirect_uri,
230+
issuer_state = :issuer_state
228231
WHERE id = :id
229232
EOS
230233
,

src/Server/Grants/AuthCodeGrant.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use SimpleSAML\Module\oidc\Entities\Interfaces\AccessTokenEntityInterface;
2828
use SimpleSAML\Module\oidc\Entities\Interfaces\AuthCodeEntityInterface;
2929
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
30+
use SimpleSAML\Module\oidc\Entities\ScopeEntity;
3031
use SimpleSAML\Module\oidc\Entities\UserEntity;
3132
use SimpleSAML\Module\oidc\Factories\Entities\AccessTokenEntityFactory;
3233
use SimpleSAML\Module\oidc\Factories\Entities\AuthCodeEntityFactory;
@@ -347,6 +348,7 @@ protected function issueOidcAuthCode(
347348
$userIdentifier,
348349
$redirectUri,
349350
$authorizationRequest->getNonce(),
351+
$authorizationRequest->getIssuerState(),
350352
flowTypeEnum: $flowType,
351353
authorizationDetails: $authorizationRequest->getAuthorizationDetails(),
352354
boundClientId: $authorizationRequest->getBoundClientId(),
@@ -615,6 +617,7 @@ public function respondToAccessTokenRequest(
615617
$storedAuthCodeEntity->getAuthorizationDetails(),
616618
$storedAuthCodeEntity->getBoundClientId(),
617619
$storedAuthCodeEntity->getBoundRedirectUri(),
620+
$storedAuthCodeEntity->getIssuerState(),
618621
);
619622
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
620623
$responseType->setAccessToken($accessToken);
@@ -893,6 +896,28 @@ public function validateAuthorizationRequestWithRequestRules(
893896
);
894897
$authorizationRequest->setAuthorizationDetails($authorizationDetails);
895898

899+
// TODO This is a band-aid fix for having credential claims in the userinfo endpoint when
900+
// only VCI authorizationDetails are supplied. This requires configuring a matching OIDC scope
901+
// that has all the credential type claims as well.
902+
if (is_array($authorizationDetails)) {
903+
/** @psalm-suppress MixedAssignment */
904+
foreach ($authorizationDetails as $authorizationDetail) {
905+
if (
906+
is_array($authorizationDetail) &&
907+
(isset($authorizationDetail['type'])) &&
908+
($authorizationDetail['type']) === 'openid_credential'
909+
) {
910+
/** @psalm-suppress MixedAssignment */
911+
$credentialConfigurationId = $authorizationDetail['credential_configuration_id'] ?? null;
912+
if (is_string($credentialConfigurationId)) {
913+
$scopes[] = new ScopeEntity($credentialConfigurationId);
914+
}
915+
}
916+
}
917+
$this->loggerService->debug('authorizationDetails Resolved Scopes: ', ['scopes' => $scopes]);
918+
$authorizationRequest->setScopes($scopes);
919+
}
920+
896921
// Check if we are using a generic client for this request. This can happen for non-registered clients
897922
// in VCI flows. This can be removed once the VCI clients (wallets) are properly registered using DCR.
898923
if ($client->isGeneric()) {

0 commit comments

Comments
 (0)