8000 [OID4VCI] Always Return Array for Credential Responses by forkimenjeckayang · Pull Request #40409 · keycloak/keycloak · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

[OID4VCI] Always Return Array for Credential Responses #40409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ public Response requestCredential(

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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -29,18 +32,37 @@
@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;
}

Expand All @@ -52,4 +74,22 @@ public CredentialResponse setNotificationId(String notificationId) {
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -408,8 +408,14 @@ protected Map<String, String> getCredentialDefinitionAttributes() {

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.cla 8000 ss).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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ public void testRequestCredential() {
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"));
Expand Down Expand Up @@ -442,7 +442,7 @@ public void testCredentialIssuanceWithAuthZCodeWithScopeMatched() throws Excepti
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ private static SdJwtVP testRequestTestCredential(KeycloakSession session, String
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
Expand Down Expand Up @@ -445,7 +446,7 @@ static class TestCredentialResponseHandler extends CredentialResponseHandler {
@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);
Expand Down
Loading
0