|
17 | 17 | use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum; |
18 | 18 | use SimpleSAML\OpenID\Codebooks\AtContextsEnum; |
19 | 19 | use SimpleSAML\OpenID\Codebooks\ClaimsEnum; |
| 20 | +use SimpleSAML\OpenID\Codebooks\CredentialFormatIdentifiersEnum; |
20 | 21 | use SimpleSAML\OpenID\Codebooks\CredentialTypesEnum; |
21 | 22 | use SimpleSAML\OpenID\Codebooks\HttpMethodsEnum; |
| 23 | +use SimpleSAML\OpenID\Codebooks\JwtTypesEnum; |
22 | 24 | use SimpleSAML\OpenID\Did; |
23 | 25 | use SimpleSAML\OpenID\Exceptions\OpenId4VciProofException; |
24 | 26 | use SimpleSAML\OpenID\Jwk; |
|
28 | 30 |
|
29 | 31 | class CredentialIssuerCredentialController |
30 | 32 | { |
| 33 | + |
| 34 | + public const SD_JWT_FORMAT_IDS = [ |
| 35 | + CredentialFormatIdentifiersEnum::DcSdJwt->value, |
| 36 | + CredentialFormatIdentifiersEnum::VcSdJwt->value, |
| 37 | + ]; |
| 38 | + |
31 | 39 | /** |
32 | 40 | * @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException |
33 | 41 | */ |
@@ -106,47 +114,42 @@ public function credential(Request $request): Response |
106 | 114 | ); |
107 | 115 | } |
108 | 116 |
|
109 | | - if (!in_array($credentialConfigurationId, $this->moduleConfig->getCredentialConfigurationIdsSupported())) { |
| 117 | + if ( |
| 118 | + !is_array( |
| 119 | + $credentialConfiguration = $this->moduleConfig->getCredentialConfiguration($credentialConfigurationId), |
| 120 | + ) |
| 121 | + ) { |
110 | 122 | return $this->routes->newJsonErrorResponse( |
111 | 123 | 'unsupported_credential_type', |
112 | 124 | sprintf('Credential configuration ID "%s" is not supported.', $credentialConfigurationId), |
113 | 125 | ); |
114 | 126 | } |
115 | 127 |
|
116 | | - $userId = $accessToken->getUserIdentifier(); |
117 | | - $userEntity = $this->userRepository->getUserEntityByIdentifier($userId); |
118 | | - if ($userEntity === null) { |
119 | | - throw OidcServerException::invalidRequest('User not found'); |
120 | | - } |
| 128 | + $credentialFormatId = $credentialConfiguration[ClaimsEnum::Format->value] ?? null; |
121 | 129 |
|
122 | | - $userAttributes = $userEntity->getClaims(); |
| 130 | + if (is_null($credentialFormatId)) { |
| 131 | + throw OidcServerException::serverError( |
| 132 | + 'Credential format not specified for configuration ID: ' . $credentialConfigurationId, |
| 133 | + ); |
| 134 | + } |
123 | 135 |
|
124 | | - // Get valid claim paths so we can check if the user attribute is allowed to be included in the credential, |
125 | | - // as per the credential configuration supported configuration. |
126 | | - $validClaimPaths = $this->moduleConfig->getValidCredentialClaimPathsFor($credentialConfigurationId); |
| 136 | + if ( |
| 137 | + !in_array($credentialFormatId, [ |
| 138 | + CredentialFormatIdentifiersEnum::JwtVcJson->value, |
| 139 | + CredentialFormatIdentifiersEnum::DcSdJwt->value, |
| 140 | + CredentialFormatIdentifiersEnum::VcSdJwt->value, // Deprecated value, but let's support it for now. |
| 141 | + ]) |
| 142 | + ) { |
| 143 | + return $this->routes->newJsonErrorResponse( |
| 144 | + 'unsupported_credential_type', |
| 145 | + sprintf('Credential format ID "%s" is not supported.', $credentialFormatId), |
| 146 | + ); |
| 147 | + } |
127 | 148 |
|
128 | | - // Map user attributes to credential claims |
129 | | - $credentialSubject = []; |
130 | | - $attributeToCredentialClaimPathMap = $this->moduleConfig->getUserAttributeToCredentialClaimPathMapFor( |
131 | | - $credentialConfigurationId, |
132 | | - ); |
133 | | - foreach ($attributeToCredentialClaimPathMap as $mapEntry) { |
134 | | - $userAttributeName = key($mapEntry); |
135 | | - $credentialClaimPath = current($mapEntry); |
136 | | - if (!in_array($credentialClaimPath, $validClaimPaths)) { |
137 | | - $this->loggerService->warning( |
138 | | - 'Attribute "%s" does not use one of valid credential claim paths.', |
139 | | - $mapEntry, |
140 | | - ); |
141 | | - continue; |
142 | | - } |
143 | | - if (isset($userAttributes[$userAttributeName])) { |
144 | | - $this->setCredentialClaimValue( |
145 | | - $credentialSubject, |
146 | | - $credentialClaimPath, |
147 | | - $userAttributes[$userAttributeName], |
148 | | - ); |
149 | | - } |
| 149 | + $userId = $accessToken->getUserIdentifier(); |
| 150 | + $userEntity = $this->userRepository->getUserEntityByIdentifier($userId); |
| 151 | + if ($userEntity === null) { |
| 152 | + throw OidcServerException::invalidRequest('User not found.'); |
150 | 153 | } |
151 | 154 |
|
152 | 155 | // Placeholder sub identifier. Will do if proof is not provided. |
@@ -221,6 +224,71 @@ public function credential(Request $request): Response |
221 | 224 | } |
222 | 225 | } |
223 | 226 |
|
| 227 | + $userAttributes = $userEntity->getClaims(); |
| 228 | + |
| 229 | + // Get valid claim paths so we can check if the user attribute is allowed to be included in the credential, |
| 230 | + // as per the credential configuration supported configuration. |
| 231 | + $validClaimPaths = $this->moduleConfig->getValidCredentialClaimPathsFor($credentialConfigurationId); |
| 232 | + |
| 233 | + // Map user attributes to credential claims |
| 234 | + $credentialSubject = []; // For JwtVcJson |
| 235 | + $disclosureBag = $this->verifiableCredentials->disclosureBagFactory()->build(); // For DcSdJwt |
| 236 | + $attributeToCredentialClaimPathMap = $this->moduleConfig->getUserAttributeToCredentialClaimPathMapFor( |
| 237 | + $credentialConfigurationId, |
| 238 | + ); |
| 239 | + foreach ($attributeToCredentialClaimPathMap as $mapEntry) { |
| 240 | + $userAttributeName = key($mapEntry); |
| 241 | + $credentialClaimPath = current($mapEntry); |
| 242 | + if (!in_array($credentialClaimPath, $validClaimPaths)) { |
| 243 | + $this->loggerService->warning( |
| 244 | + 'Attribute "%s" does not use one of valid credential claim paths.', |
| 245 | + $mapEntry, |
| 246 | + ); |
| 247 | + continue; |
| 248 | + } |
| 249 | + |
| 250 | + if (!isset($userAttributes[$userAttributeName])) { |
| 251 | + $this->loggerService->warning( |
| 252 | + 'Attribute "%s" does not exist in user attributes.', |
| 253 | + $mapEntry, |
| 254 | + ); |
| 255 | + continue; |
| 256 | + } |
| 257 | + |
| 258 | + if ($credentialFormatId === CredentialFormatIdentifiersEnum::DcSdJwt->value) { |
| 259 | + $this->setCredentialClaimValue( |
| 260 | + $credentialSubject, |
| 261 | + $credentialClaimPath, |
| 262 | + $userAttributes[$userAttributeName], |
| 263 | + ); |
| 264 | + } |
| 265 | + |
| 266 | + if (in_array($credentialFormatId, self::SD_JWT_FORMAT_IDS, true)) { |
| 267 | + // For now, we will only support disclosures for object properties. |
| 268 | + $claimName = array_pop($credentialClaimPath); |
| 269 | + if (!is_string($claimName)) { |
| 270 | + $message = sprintf( |
| 271 | + 'Invalid credential claim path for user attribute name %s. Can not extract claim name.' . |
| 272 | + ' Path was: %s', |
| 273 | + $userAttributeName, |
| 274 | + print_r($credentialClaimPath, true), |
| 275 | + ); |
| 276 | + $this->loggerService->error($message); |
| 277 | + continue; |
| 278 | + } |
| 279 | + |
| 280 | + $disclosure = $this->verifiableCredentials->disclosureFactory()->build( |
| 281 | + value: $userAttributes[$userAttributeName], |
| 282 | + name: $claimName, |
| 283 | + path: is_array($credentialClaimPath) ? $credentialClaimPath : [], |
| 284 | + saltBlacklist: $disclosureBag->salts(), |
| 285 | + ); |
| 286 | + |
| 287 | + $disclosureBag->add($disclosure);; |
| 288 | + } |
| 289 | + } |
| 290 | + |
| 291 | + dd($disclosureBag->all()); |
224 | 292 | // Also make sure that the subject identifier is in credentialSubject claim. |
225 | 293 | $this->setCredentialClaimValue( |
226 | 294 | $credentialSubject, |
@@ -249,39 +317,66 @@ public function credential(Request $request): Response |
249 | 317 | $issuedAt = new \DateTimeImmutable(); |
250 | 318 |
|
251 | 319 | $vcId = $this->moduleConfig->getIssuer() . '/vc/' . uniqid(); |
252 | | - |
253 | | - $verifiableCredential = $this->verifiableCredentials->jwtVcJsonFactory()->fromData( |
254 | | - $signingKey, |
255 | | - SignatureAlgorithmEnum::from($this->moduleConfig->getProtocolSigner()->algorithmId()), |
256 | | - [ |
257 | | - ClaimsEnum::Vc->value => [ |
258 | | - ClaimsEnum::AtContext->value => [ |
259 | | - AtContextsEnum::W3Org2018CredentialsV1->value, |
| 320 | + $signatureAlgorithm = SignatureAlgorithmEnum::from($this->moduleConfig->getProtocolSigner()->algorithmId()); |
| 321 | + |
| 322 | + $verifiableCredential = null; |
| 323 | + |
| 324 | + if ($credentialFormatId === CredentialFormatIdentifiersEnum::JwtVcJson->value) { |
| 325 | + $verifiableCredential = $this->verifiableCredentials->jwtVcJsonFactory()->fromData( |
| 326 | + $signingKey, |
| 327 | + $signatureAlgorithm, |
| 328 | + [ |
| 329 | + ClaimsEnum::Vc->value => [ |
| 330 | + ClaimsEnum::AtContext->value => [ |
| 331 | + AtContextsEnum::W3Org2018CredentialsV1->value, |
| 332 | + ], |
| 333 | + ClaimsEnum::Type->value => [ |
| 334 | + CredentialTypesEnum::VerifiableCredential->value, |
| 335 | + $credentialConfigurationId, |
| 336 | + ], |
| 337 | + //ClaimsEnum::Issuer->value => $this->moduleConfig->getIssuer(), |
| 338 | + ClaimsEnum::Issuer->value => $issuerDid, |
| 339 | + //ClaimsEnum::Issuer->value => 'https://idp.mivanci.incubator.hexaa.eu/ssp/module.php/oidc/jwks', |
| 340 | + ClaimsEnum::Issuance_Date->value => $issuedAt->format(\DateTimeInterface::RFC3339), |
| 341 | + ClaimsEnum::Id->value => $vcId, |
| 342 | + ClaimsEnum::Credential_Subject->value => |
| 343 | + $credentialSubject[ClaimsEnum::Credential_Subject->value] ?? [], |
260 | 344 | ], |
261 | | - ClaimsEnum::Type->value => [ |
262 | | - CredentialTypesEnum::VerifiableCredential->value, |
263 | | - $credentialConfigurationId, |
264 | | - ], |
265 | | - //ClaimsEnum::Issuer->value => $this->moduleConfig->getIssuer(), |
266 | | - ClaimsEnum::Issuer->value => $issuerDid, |
267 | | - //ClaimsEnum::Issuer->value => 'https://idp.mivanci.incubator.hexaa.eu/ssp/module.php/oidc/jwks', |
268 | | - ClaimsEnum::Issuance_Date->value => $issuedAt->format(\DateTimeInterface::RFC3339), |
269 | | - ClaimsEnum::Id->value => $vcId, |
270 | | - ClaimsEnum::Credential_Subject->value => |
271 | | - $credentialSubject[ClaimsEnum::Credential_Subject->value] ?? [], |
| 345 | + //ClaimsEnum::Iss->value => $this->moduleConfig->getIssuer(), |
| 346 | + ClaimsEnum::Iss->value => $issuerDid, |
| 347 | + //ClaimsEnum::Iss->value => 'https://idp.mivanci.incubator.hexaa.eu/ssp/module.php/oidc/jwks', |
| 348 | + ClaimsEnum::Iat->value => $issuedAt->getTimestamp(), |
| 349 | + ClaimsEnum::Nbf->value => $issuedAt->getTimestamp(), |
| 350 | + ClaimsEnum::Sub->value => $sub, |
| 351 | + ClaimsEnum::Jti->value => $vcId, |
272 | 352 | ], |
273 | | - //ClaimsEnum::Iss->value => $this->moduleConfig->getIssuer(), |
274 | | - ClaimsEnum::Iss->value => $issuerDid, |
275 | | - //ClaimsEnum::Iss->value => 'https://idp.mivanci.incubator.hexaa.eu/ssp/module.php/oidc/jwks', |
276 | | - ClaimsEnum::Iat->value => $issuedAt->getTimestamp(), |
277 | | - ClaimsEnum::Nbf->value => $issuedAt->getTimestamp(), |
278 | | - ClaimsEnum::Sub->value => $sub, |
279 | | - ClaimsEnum::Jti->value => $vcId, |
280 | | - ], |
281 | | - [ |
282 | | - ClaimsEnum::Kid->value => $issuerDid . '#0', |
283 | | - ], |
284 | | - ); |
| 353 | + [ |
| 354 | + ClaimsEnum::Kid->value => $issuerDid . '#0', |
| 355 | + ], |
| 356 | + ); |
| 357 | + } |
| 358 | + |
| 359 | + if (in_array($credentialFormatId, self::SD_JWT_FORMAT_IDS, true)) { |
| 360 | + // TODO selectiveDisclosureBag |
| 361 | + |
| 362 | + $verifiableCredential = $this->verifiableCredentials->sdJwtVcFactory()->fromData( |
| 363 | + $signingKey, |
| 364 | + $signatureAlgorithm, |
| 365 | + [ |
| 366 | + ClaimsEnum::Iss->value => $issuerDid, |
| 367 | + ClaimsEnum::Iat->value => $issuedAt->getTimestamp(), |
| 368 | + ClaimsEnum::Nbf->value => $issuedAt->getTimestamp(), |
| 369 | + ClaimsEnum::Sub->value => $sub, |
| 370 | + ClaimsEnum::Jti->value => $vcId, |
| 371 | + ], |
| 372 | + [ |
| 373 | + ClaimsEnum::Kid->value => $issuerDid . '#0', |
| 374 | + ], |
| 375 | + jwtTypesEnum: JwtTypesEnum::VcSdJwt, |
| 376 | + ); |
| 377 | + } |
| 378 | + |
| 379 | + |
285 | 380 |
|
286 | 381 | $this->loggerService->debug('response', [ |
287 | 382 | 'credentials' => [ |
|
0 commit comments