From f641269ac1bad80740054f0ef26ed2fe5b7fa9ef Mon Sep 17 00:00:00 2001 From: Marek Posolda Date: Wed, 10 Dec 2025 12:02:52 +0100 Subject: [PATCH] =?UTF-8?q?CredentialRequest=20with=20credentialIdentifier?= =?UTF-8?q?=20does=20not=20work=20when=20creden=E2=80=A6=20(#44794)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #44793 Signed-off-by: mposolda --- .../OID4VCAuthorizationDetailsProcessor.java | 6 +- .../OID4VCAuthorizationCodeFlowTestBase.java | 55 ++++++++++++++----- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessor.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessor.java index ae6f9c1aaf8..c3d04b011fd 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessor.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessor.java @@ -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); } } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowTestBase.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowTestBase.java index 3a2561632d6..bbf49513279 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowTestBase.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowTestBase.java @@ -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 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 credRequestSupplier = (credentialConfigurationId, credentialIdentifier) -> { + CredentialRequest credentialRequest = new CredentialRequest(); + credentialRequest.setCredentialIdentifier(credentialIdentifier); + return credentialRequest; + }; + + testCompleteFlowWithClaimsValidationAuthorizationCode(credRequestSupplier); + } + + + private void testCompleteFlowWithClaimsValidationAuthorizationCode(BiFunction 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 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 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 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 parseAuthorizationDetails(String responseBody) { try { // Parse the JSON response to extract authorization_details - Map responseMap = JsonSerialization.readValue(responseBody, Map.class); - Object authDetailsObj = responseMap.get("authorization_details"); + Map 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>() { + new TypeReference<>() { }); } catch (Exception e) { throw new RuntimeException("Failed to parse authorization_details from response", e);