@@ -296,20 +296,30 @@ private void checkScope(CredentialRequest credentialRequestVO) {
296
296
if (vcIssuanceFlow == null || !vcIssuanceFlow .equals (PreAuthorizedCodeGrantTypeFactory .GRANT_TYPE )) {
297
297
// Authorization Code Flow
298
298
RealmModel realm = session .getContext ().getRealm ();
299
- String credentialIdentifier = credentialRequestVO .getCredentialIdentifier ();
300
-
301
- String scope = realm .getAttribute ("vc." + credentialIdentifier + ".scope" );
302
-
303
299
AccessToken accessToken = bearerTokenAuthenticator .authenticate ().getToken ();
304
- if (scope == null || Arrays .stream (accessToken .getScope ().split (" " ))
305
- .noneMatch (tokenScope -> tokenScope .equals (scope ))) {
306
- LOGGER .debugf ("Scope check failure: credentialIdentifier = %s, required scope = %s, scope in access token = %s." ,
307
- credentialIdentifier , scope , accessToken .getScope ());
308
- throw new CorsErrorResponseException (cors ,
309
- ErrorType .UNSUPPORTED_CREDENTIAL_TYPE .toString (),
310
- "Scope check failure" ,
311
- Response .Status .BAD_REQUEST );
312
- } else {
300
+
301
+ // Get all credential identifiers from the request
302
+ List <String > credentialIdentifiers = credentialRequestVO .getCredentialSpecs () != null
303
+ ? credentialRequestVO .getCredentialSpecs ().stream ()
304
+ .map (CredentialRequest .CredentialSpec ::getCredentialIdentifier )
305
+ .filter (Objects ::nonNull )
306
+ .collect (Collectors .toList ())
307
+ : List .of (credentialRequestVO .getCredentialIdentifier ());
308
+
309
+ for (String credentialIdentifier : credentialIdentifiers ) {
310
+ if (credentialIdentifier == null ) {
311
+ continue ; // Skip if no identifier is provided (e.g., format-based request)
312
+ }
313
+ String scope = realm .getAttribute ("vc." + credentialIdentifier + ".scope" );
314
+ if (scope == null || Arrays .stream (accessToken .getScope ().split (" " ))
315
+ .noneMatch (tokenScope -> tokenScope .equals (scope ))) {
316
+ LOGGER .debugf ("Scope check failure: credentialIdentifier = %s, required scope = %s, scope in access token = %s." ,
317
+ credentialIdentifier , scope , accessToken .getScope ());
318
+ throw new CorsErrorResponseException (cors ,
319
+ ErrorType .UNSUPPORTED_CREDENTIAL_TYPE .toString (),
320
+ "Scope check failure for credential: " + credentialIdentifier ,
321
+ Response .Status .BAD_REQUEST );
322
+ }
313
323
LOGGER .debugf ("Scope check success: credentialIdentifier = %s, required scope = %s, scope in access token = %s." ,
314
324
credentialIdentifier , scope , accessToken .getScope ());
315
325
}
@@ -337,84 +347,94 @@ public Response requestCredential(CredentialRequest credentialRequestVO) {
337
347
checkScope (credentialRequestVO );
338
348
}
339
349
340
- // Both Format and identifier are optional.
341
- // If the credential_identifier is present, Format can't be present. But this implementation will
342
- // tolerate the presence of both, waiting for clarity in specifications.
343
- // This implementation will privilege the presence of the credential config identifier.
344
- String requestedCredentialId = credentialRequestVO .getCredentialIdentifier ();
345
- String requestedFormat = credentialRequestVO .getFormat ();
350
+ Map <String , SupportedCredentialConfiguration > supportedCredentials = OID4VCIssuerWellKnownProvider .getSupportedCredentials (this .session );
351
+ CredentialResponse responseVO = new CredentialResponse ();
352
+
353
+ // Handle single or multiple credential specs
354
+ List <CredentialRequest .CredentialSpec > specs = credentialRequestVO .getCredentialSpecs () != null
355
+ ? credentialRequestVO .getCredentialSpecs ()
356
+ : List .of (new CredentialRequest .CredentialSpec ()
357
+ .setFormat (credentialRequestVO .getFormat ())
358
+ .setCredentialIdentifier (credentialRequestVO .getCredentialIdentifier ())
359
+ .setVct (credentialRequestVO .getVct ())
360
+ .setCredentialDefinition (credentialRequestVO .getCredentialDefinition ())
361
+ .setProof (credentialRequestVO .getProof ()));
362
+
363
+ // Process each credential specification
364
+ List <CredentialResponse .CredentialEntry > credentials = specs .stream ()
365
+ .map (spec -> {
366
+ // Resolve credential configuration
367
+ SupportedCredentialConfiguration config = resolveCredentialConfiguration (spec , supportedCredentials );
368
+
369
+ // Single credential request for compatibility with existing getCredential method
370
+ CredentialRequest singleRequest = new CredentialRequest ()
371
+ .setFormat (spec .getFormat ())
372
+ .setCredentialIdentifier (spec .getCredentialIdentifier ())
373
+ .setVct (spec .getVct ())
374
+ .setCredentialDefinition (spec .getCredentialDefinition ())
375
+ .setProof (spec .getProof ());
376
+
377
+ // Generate credential
378
+ Object credential = getCredential (authResult , config , singleRequest );
379
+
380
+ // Ensure format is supported
381
+ if (!SUPPORTED_FORMATS .contains (spec .getFormat ())) {
382
+ throw new BadRequestException (getErrorResponse (ErrorType .UNSUPPORTED_CREDENTIAL_TYPE ));
383
+ }
346
384
347
- // Check if at least one of both is available.
385
+ return new CredentialResponse .CredentialEntry ()
386
+ .setFormat (spec .getFormat ())
387
+ .setCredential (credential );
388
+ })
389
+ .collect (Collectors .toList ());
390
+
391
+ responseVO .setCredentials (credentials );
392
+ return Response .ok ().entity (responseVO ).build ();
393
+ }
394
+
395
+ private SupportedCredentialConfiguration resolveCredentialConfiguration (CredentialRequest .CredentialSpec spec , Map <String , SupportedCredentialConfiguration > supportedCredentials ) {
396
+ String requestedCredentialId = spec .getCredentialIdentifier ();
397
+ String requestedFormat = spec .getFormat ();
398
+
399
+ // Check if at least one of credential_identifier or format is provided
348
400
if (requestedCredentialId == null && requestedFormat == null ) {
349
401
LOGGER .debugf ("Missing both configuration id and requested format. At least one shall be specified." );
350
402
throw new BadRequestException (getErrorResponse (ErrorType .MISSING_CREDENTIAL_CONFIG_AND_FORMAT ));
351
403
}
352
404
353
- Map <String , SupportedCredentialConfiguration > supportedCredentials = OID4VCIssuerWellKnownProvider .getSupportedCredentials (this .session );
354
-
355
- // Resolve from identifier first
356
- SupportedCredentialConfiguration supportedCredentialConfiguration = null ;
405
+ SupportedCredentialConfiguration config = null ;
357
406
if (requestedCredentialId != null ) {
358
- supportedCredentialConfiguration = supportedCredentials .get (requestedCredentialId );
359
- if (supportedCredentialConfiguration == null ) {
407
+ config = supportedCredentials .get (requestedCredentialId );
408
+ if (config == null ) {
360
409
LOGGER .debugf ("Credential with configuration id %s not found." , requestedCredentialId );
361
410
throw new BadRequestException (getErrorResponse (ErrorType .UNSUPPORTED_CREDENTIAL_TYPE ));
362
411
}
363
- // Then for format. We know spec does not allow both parameter. But we are tolerant if you send both
364
- // Was found by id, check that the format matches.
365
- if ( requestedFormat != null && ! requestedFormat . equals ( supportedCredentialConfiguration . getFormat ())) {
366
- LOGGER . debugf ( "Credential with configuration id %s does not support requested format %s, but supports %s." , requestedCredentialId , requestedFormat , supportedCredentialConfiguration .getFormat ());
412
+ // Check format compatibility if provided
413
+ if ( requestedFormat != null && ! requestedFormat . equals ( config . getFormat ())) {
414
+ LOGGER . debugf ( "Credential with configuration id %s does not support requested format %s, but supports %s." ,
415
+ requestedCredentialId , requestedFormat , config .getFormat ());
367
416
throw new BadRequestException (getErrorResponse (ErrorType .UNSUPPORTED_CREDENTIAL_FORMAT ));
368
417
}
369
418
}
370
419
371
- if (supportedCredentialConfiguration == null && requestedFormat != null ) {
420
+ if (config == null && requestedFormat != null ) {
372
421
// Search by format
373
- supportedCredentialConfiguration = getSupportedCredentialConfiguration (credentialRequestVO , supportedCredentials , requestedFormat );
374
- if (supportedCredentialConfiguration == null ) {
375
- LOGGER .debugf ("Credential with requested format %s, not supported." , requestedFormat );
422
+ config = getSupportedCredentialConfiguration (spec , supportedCredentials , requestedFormat );
423
+ if (config == null ) {
424
+ LOGGER .debugf ("Credential with requested format %s not supported." , requestedFormat );
376
425
throw new BadRequestException (getErrorResponse (ErrorType .UNSUPPORTED_CREDENTIAL_FORMAT ));
377
426
}
378
427
}
379
428
380
- CredentialResponse responseVO = new CredentialResponse ();
381
-
382
- // Handle multiple credentials if proofs are provided
383
- List <Proof > proofs = credentialRequestVO .getProofs () != null ? credentialRequestVO .getProofs () : List .of ();
384
- if (proofs .isEmpty ()) {
385
- // Single credential request without proof
386
- Object theCredential = getCredential (authResult , supportedCredentialConfiguration , credentialRequestVO );
387
- if (SUPPORTED_FORMATS .contains (requestedFormat )) {
388
- responseVO .setCredential (List .of (theCredential ));
389
- } else {
390
- throw new BadRequestException (getErrorResponse (ErrorType .UNSUPPORTED_CREDENTIAL_TYPE ));
391
- }
392
- } else {
393
- // Multiple credential request with proofs
394
- SupportedCredentialConfiguration finalSupportedCredentialConfiguration = supportedCredentialConfiguration ;
395
- List <Object > credentials = proofs .stream ()
396
- .map (proof -> {
397
- CredentialRequest singleProofRequest = new CredentialRequest ()
398
- .setFormat (credentialRequestVO .getFormat ())
399
- .setCredentialIdentifier (credentialRequestVO .getCredentialIdentifier ())
400
- .setVct (credentialRequestVO .getVct ())
401
- .setCredentialDefinition (credentialRequestVO .getCredentialDefinition ())
402
- .setProof (proof );
403
- return getCredential (authResult , finalSupportedCredentialConfiguration , singleProofRequest );
404
- })
405
- .collect (Collectors .toList ());
406
- if (SUPPORTED_FORMATS .contains (requestedFormat )) {
407
- responseVO .setCredential (credentials );
408
- } else {
409
- throw new BadRequestException (getErrorResponse (ErrorType .UNSUPPORTED_CREDENTIAL_TYPE ));
410
- }
429
+ if (config == null ) {
430
+ throw new BadRequestException (getErrorResponse (ErrorType .UNSUPPORTED_CREDENTIAL_TYPE ));
411
431
}
412
432
413
- return Response . ok (). entity ( responseVO ). build () ;
433
+ return config ;
414
434
}
415
435
416
- private SupportedCredentialConfiguration getSupportedCredentialConfiguration (CredentialRequest credentialRequestVO , Map <String , SupportedCredentialConfiguration > supportedCredentials , String requestedFormat ) {
417
- // 1. Format resolver
436
+ private SupportedCredentialConfiguration getSupportedCredentialConfiguration (CredentialRequest . CredentialSpec spec , Map <String , SupportedCredentialConfiguration > supportedCredentials , String requestedFormat ) {
437
+ // Filter by format
418
438
List <SupportedCredentialConfiguration > configs = supportedCredentials .values ().stream ()
419
439
.filter (supportedCredential -> Objects .equals (supportedCredential .getFormat (), requestedFormat ))
420
440
.collect (Collectors .toList ());
@@ -425,14 +445,14 @@ private SupportedCredentialConfiguration getSupportedCredentialConfiguration(Cre
425
445
case SD_JWT_VC :
426
446
// Resolve from vct for sd-jwt
427
447
matchingConfigs = configs .stream ()
428
- .filter (supportedCredential -> Objects .equals (supportedCredential .getVct (), credentialRequestVO .getVct ()))
448
+ .filter (supportedCredential -> Objects .equals (supportedCredential .getVct (), spec .getVct ()))
429
449
.collect (Collectors .toList ());
430
450
break ;
431
451
case JWT_VC :
432
452
case LDP_VC :
433
- // Will detach this when each format provides logic on how to resolve from definition.
453
+ // Resolve from credential_definition
434
454
matchingConfigs = configs .stream ()
435
- .filter (supportedCredential -> Objects .equals (supportedCredential .getCredentialDefinition (), credentialRequestVO .getCredentialDefinition ()))
455
+ .filter (supportedCredential -> Objects .equals (supportedCredential .getCredentialDefinition (), spec .getCredentialDefinition ()))
436
456
.collect (Collectors .toList ());
437
457
break ;
438
458
default :
@@ -587,6 +607,17 @@ private Response getErrorResponse(ErrorType errorType) {
587
607
.build ();
588
608
}
589
609
610
+ private Response getErrorResponse (ErrorType errorType , String errorDescription ) {
611
+ var errorResponse = new ErrorResponse ();
612
+ errorResponse .setError (errorType );
613
+ errorResponse .setErrorDescription (errorDescription );
614
+ return Response
615
+ .status (Response .Status .BAD_REQUEST )
616
+ .entity (errorResponse )
617
+ .type (MediaType .APPLICATION_JSON )
618
+ .build ();
619
+ }
620
+
590
621
private ClientModel getClient (String clientId ) {
591
622
return session .clients ().getClientByClientId (session .getContext ().getRealm (), clientId );
592
623
}
0 commit comments