Always Return Array of Credentilas for Credential Responses (#40409)

Closes #39283

Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>


Co-authored-by: Francis Pouatcha <francis.pouatcha@adorsys.com>
This commit is contained in:
forkimenjeckayang 2025-07-07 12:53:28 +01:00 committed by GitHub
parent eb7ce6ae15
commit 178b893492
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 60 additions and 13 deletions

View File

@ -408,7 +408,7 @@ public class OID4VCIssuerEndpoint {
Object theCredential = getCredential(authResult, supportedCredentialConfiguration, credentialRequestVO);
if (SUPPORTED_FORMATS.contains(requestedFormat)) {
responseVO.setCredential(theCredential);
responseVO.addCredential(theCredential);
} else {
throw new BadRequestException(getErrorResponse(ErrorType.UNSUPPORTED_CREDENTIAL_TYPE));
}

View File

@ -17,6 +17,9 @@
package org.keycloak.protocol.oid4vc.model;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -29,18 +32,37 @@ import com.fasterxml.jackson.annotation.JsonProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CredentialResponse {
// concrete type depends on the format
private Object credential;
@JsonProperty("credentials")
private List<Credential> credentials;
@JsonProperty("transaction_id")
private String transactionId;
@JsonProperty("notification_id")
private String notificationId;
public Object getCredential() {
return credential;
public List<Credential> getCredentials() {
return credentials;
}
public CredentialResponse setCredential(Object credential) {
this.credential = credential;
public CredentialResponse setCredentials(List<Credential> credentials) {
this.credentials = credentials;
return this;
}
public void addCredential(Object credential) {
if (this.credentials == null) {
this.credentials = new ArrayList<>();
}
this.credentials.add(new Credential().setCredential(credential));
}
public String getTransactionId() {
return transactionId;
}
public CredentialResponse setTransactionId(String transactionId) {
this.transactionId = transactionId;
return this;
}
@ -52,4 +74,22 @@ public class CredentialResponse {
this.notificationId = notificationId;
return this;
}
/**
* Inner class to represent a single credential object within the credentials array.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Credential {
@JsonProperty("credential")
private Object credential;
public Object getCredential() {
return credential;
}
public Credential setCredential(Object credential) {
this.credential = credential;
return this;
}
}
}

View File

@ -408,8 +408,14 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
protected static class CredentialResponseHandler {
protected void handleCredentialResponse(CredentialResponse credentialResponse) throws VerificationException {
assertNotNull("The credential should have been responded.", credentialResponse.getCredential());
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponse.getCredential(), JsonWebToken.class).getToken();
assertNotNull("The credentials array should be present in the response.", credentialResponse.getCredentials());
assertFalse("The credentials array should not be empty.", credentialResponse.getCredentials().isEmpty());
// Get the first credential from the array (maintaining compatibility with single credential tests)
CredentialResponse.Credential credentialObj = credentialResponse.getCredentials().get(0);
assertNotNull("The first credential in the array should not be null.", credentialObj);
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialObj.getCredential(), JsonWebToken.class).getToken();
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
VerifiableCredential credential = JsonSerialization.mapper.convertValue(jsonWebToken.getOtherClaims().get("vc"), VerifiableCredential.class);
assertEquals(List.of("VerifiableCredential"), credential.getType());

View File

@ -333,7 +333,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
assertEquals("The credential request should be answered successfully.", HttpStatus.SC_OK, credentialResponse.getStatus());
assertNotNull("A credential should be responded.", credentialResponse.getEntity());
CredentialResponse credentialResponseVO = JsonSerialization.mapper.convertValue(credentialResponse.getEntity(), CredentialResponse.class);
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponseVO.getCredential(), JsonWebToken.class).getToken();
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponseVO.getCredentials().get(0).getCredential(), JsonWebToken.class).getToken();
assertNotNull("A valid credential string should have been responded", jsonWebToken);
assertNotNull("The credentials should be included at the vc-claim.", jsonWebToken.getOtherClaims().get("vc"));
@ -442,7 +442,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
assertEquals(200, response.getStatus());
CredentialResponse credentialResponse = JsonSerialization.readValue(response.readEntity(String.class), CredentialResponse.class);
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponse.getCredential(), JsonWebToken.class).getToken();
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponse.getCredentials().get(0).getCredential(), JsonWebToken.class).getToken();
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
VerifiableCredential credential = JsonSerialization.mapper.convertValue(jsonWebToken.getOtherClaims().get("vc"), VerifiableCredential.class);

View File

@ -233,7 +233,8 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
CredentialResponse credentialResponseVO = JsonSerialization.mapper.convertValue(credentialResponse.getEntity(), CredentialResponse.class);
new TestCredentialResponseHandler(vct).handleCredentialResponse(credentialResponseVO);
return SdJwtVP.of(credentialResponseVO.getCredential().toString());
// Get the credential from the credentials array
return SdJwtVP.of(credentialResponseVO.getCredentials().get(0).getCredential().toString());
}
// Tests the complete flow from
@ -445,7 +446,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
@Override
protected void handleCredentialResponse(CredentialResponse credentialResponse) throws VerificationException {
// SDJWT have a special format.
SdJwtVP sdJwtVP = SdJwtVP.of(credentialResponse.getCredential().toString());
SdJwtVP sdJwtVP = SdJwtVP.of(credentialResponse.getCredentials().get(0).getCredential().toString());
JsonWebToken jsonWebToken = TokenVerifier.create(sdJwtVP.getIssuerSignedJWT().toJws(), JsonWebToken.class).getToken();
assertNotNull("A valid credential string should have been responded", jsonWebToken);