CredentialRequest with credentialIdentifier does not work when creden… (#44794)

closes #44793


Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
Marek Posolda
2025-12-10 12:02:52 +01:00
committed by GitHub
parent 921b10ee80
commit f641269ac1
2 changed files with 45 additions and 16 deletions

View File

@@ -137,18 +137,18 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails
// Use a reasonable expiration time (e.g., 1 hour)
int expiration = Time.currentTime() + 3600;
CredentialOfferStorage.CredentialOfferState offerState = new CredentialOfferStorage.CredentialOfferState(
credOffer, client.getClientId(), user.getUsername(), expiration);
credOffer, client.getClientId(), user.getId(), expiration);
offerState.setAuthorizationDetails(oid4vcDetail);
offerStorage.putOfferState(session, offerState);
logger.debugf("Created credential offer state for authorization code flow: [cid=%s, uid=%s, credConfigId=%s, credId=%s]",
client.getClientId(), user.getUsername(), oid4vcDetail.getCredentialConfigurationId(), credentialId);
client.getClientId(), offerState.getUserId(), oid4vcDetail.getCredentialConfigurationId(), credentialId);
} else {
// Update existing offer state with new authorization details (e.g., if same credential identifier is reused)
existingState.setAuthorizationDetails(oid4vcDetail);
offerStorage.replaceOfferState(session, existingState);
logger.debugf("Updated existing credential offer state for authorization code flow: [cid=%s, uid=%s, credConfigId=%s, credId=%s]",
client.getClientId(), user.getUsername(), oid4vcDetail.getCredentialConfigurationId(), credentialId);
client.getClientId(), existingState.getUserId(), oid4vcDetail.getCredentialConfigurationId(), credentialId);
}
}
}

View File

@@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import jakarta.ws.rs.core.HttpHeaders;
@@ -34,7 +35,6 @@ import org.keycloak.protocol.oid4vc.model.ClaimsDescription;
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.ClientScopeRepresentation;
@@ -70,7 +70,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
*/
protected static class Oid4vcTestContext {
public CredentialIssuer credentialIssuer;
public CredentialsOffer credentialsOffer;
public OIDCConfigurationRepresentation openidConfig;
}
@@ -92,7 +91,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
/**
* Prepare OID4VC test context by fetching issuer metadata and credential offer
*/
protected Oid4vcTestContext prepareOid4vcTestContext(String token) throws Exception {
protected Oid4vcTestContext prepareOid4vcTestContext() throws Exception {
Oid4vcTestContext ctx = new Oid4vcTestContext();
// Get credential issuer metadata
@@ -114,9 +113,33 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
return ctx;
}
// Test for the whole authorization_code flow with the credentialRequest using credential_configuration_id
@Test
public void testCompleteFlowWithClaimsValidationAuthorizationCode() throws Exception {
Oid4vcTestContext ctx = prepareOid4vcTestContext(null);
public void testCompleteFlowWithClaimsValidationAuthorizationCode_credentialRequestWithConfigurationId() throws Exception {
BiFunction<String, String, CredentialRequest> credRequestSupplier = (credentialConfigurationId, credentialIdentifier) -> {
CredentialRequest credentialRequest = new CredentialRequest();
credentialRequest.setCredentialConfigurationId(credentialConfigurationId);
return credentialRequest;
};
testCompleteFlowWithClaimsValidationAuthorizationCode(credRequestSupplier);
}
// Test for the whole authorization_code flow with the credentialRequest using credential_identifier
@Test
public void testCompleteFlowWithClaimsValidationAuthorizationCode_credentialRequestWithCredentialIdentifier() throws Exception {
BiFunction<String, String, CredentialRequest> credRequestSupplier = (credentialConfigurationId, credentialIdentifier) -> {
CredentialRequest credentialRequest = new CredentialRequest();
credentialRequest.setCredentialIdentifier(credentialIdentifier);
return credentialRequest;
};
testCompleteFlowWithClaimsValidationAuthorizationCode(credRequestSupplier);
}
private void testCompleteFlowWithClaimsValidationAuthorizationCode(BiFunction<String, String, CredentialRequest> credentialRequestSupplier) throws Exception {
Oid4vcTestContext ctx = prepareOid4vcTestContext();
// Perform authorization code flow to get authorization code
oauth.client(client.getClientId());
@@ -132,7 +155,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
// Construct claim path based on credential format
List<Object> claimPath;
if ("sd_jwt_vc".equals(getCredentialFormat())) {
claimPath = Arrays.asList(getExpectedClaimPath());
claimPath = Collections.singletonList(getExpectedClaimPath());
} else {
claimPath = Arrays.asList("credentialSubject", getExpectedClaimPath());
}
@@ -142,7 +165,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
AuthorizationDetail authDetail = new AuthorizationDetail();
authDetail.setType(OPENID_CREDENTIAL);
authDetail.setCredentialConfigurationId(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID));
authDetail.setClaims(Arrays.asList(claim));
authDetail.setClaims(List.of(claim));
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
List<AuthorizationDetail> authDetails = List.of(authDetail);
@@ -156,7 +179,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId()));
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, "password"));
tokenParameters.add(new BasicNameValuePair("authorization_details", authDetailsJson));
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.AUTHORIZATION_DETAILS, authDetailsJson));
UrlEncodedFormEntity tokenFormEntity = new UrlEncodedFormEntity(tokenParameters, StandardCharsets.UTF_8);
postToken.setEntity(tokenFormEntity);
@@ -179,13 +202,18 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
String credentialConfigurationId = authDetailResponse.getCredentialConfigurationId();
assertNotNull("Credential configuration id should not be null", credentialConfigurationId);
List<String> credentialIdentifiers = authDetailResponse.getCredentialIdentifiers();
assertNotNull("Credential identifiers should not be null", credentialIdentifiers);
assertEquals("Credential identifiers expected to have 1 item. It had " + credentialIdentifiers.size() + " with value " + credentialIdentifiers,
1, credentialIdentifiers.size());
String credentialIdentifier = credentialIdentifiers.get(0);
// Request the actual credential using the identifier
HttpPost postCredential = new HttpPost(ctx.credentialIssuer.getCredentialEndpoint());
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + tokenResponse.getToken());
postCredential.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
CredentialRequest credentialRequest = new CredentialRequest();
credentialRequest.setCredentialConfigurationId(credentialConfigurationId);
CredentialRequest credentialRequest = credentialRequestSupplier.apply(credentialConfigurationId, credentialIdentifier);
String requestBody = JsonSerialization.writeValueAsString(credentialRequest);
postCredential.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
@@ -228,8 +256,9 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
protected List<OID4VCAuthorizationDetailsResponse> parseAuthorizationDetails(String responseBody) {
try {
// Parse the JSON response to extract authorization_details
Map<String, Object> responseMap = JsonSerialization.readValue(responseBody, Map.class);
Object authDetailsObj = responseMap.get("authorization_details");
Map<String, Object> responseMap = JsonSerialization.readValue(responseBody, new TypeReference<>() {
});
Object authDetailsObj = responseMap.get(OAuth2Constants.AUTHORIZATION_DETAILS);
if (authDetailsObj == null) {
return Collections.emptyList();
@@ -237,7 +266,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
// Convert to list of OID4VCAuthorizationDetailsResponse
return JsonSerialization.readValue(JsonSerialization.writeValueAsString(authDetailsObj),
new TypeReference<List<OID4VCAuthorizationDetailsResponse>>() {
new TypeReference<>() {
});
} catch (Exception e) {
throw new RuntimeException("Failed to parse authorization_details from response", e);