diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index 83ab3a540e9..39872d0ed33 100755 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -174,7 +174,4 @@ public interface OAuth2Constants { String DPOP_JWT_HEADER_TYPE = "dpop+jwt"; String ALGS_ATTRIBUTE = "algs"; - // OID4VCI - https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html - String OPENID_CREDENTIAL = "openid_credential"; - String CREDENTIAL_IDENTIFIERS = "credential_identifiers"; } diff --git a/core/src/main/java/org/keycloak/OID4VCConstants.java b/core/src/main/java/org/keycloak/OID4VCConstants.java index 1ae943818e6..f67eab8802d 100644 --- a/core/src/main/java/org/keycloak/OID4VCConstants.java +++ b/core/src/main/java/org/keycloak/OID4VCConstants.java @@ -64,6 +64,11 @@ public class OID4VCConstants { public static final String RESPONSE_TYPE_IMG_PNG = "image/png"; public static final String CREDENTIAL_OFFER_URI_CODE_SCOPE = "credential-offer"; + // OID4VCI - https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html + public static final String OPENID_CREDENTIAL = "openid_credential"; + public static final String CREDENTIAL_IDENTIFIERS = "credential_identifiers"; + public static final String CREDENTIAL_CONFIGURATION_ID = "credential_configuration_id"; + private OID4VCConstants() { } diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailResponse.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailResponse.java deleted file mode 100644 index d50e9ac7938..00000000000 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailResponse.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2025 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.keycloak.protocol.oid4vc.issuance; - -import java.util.List; -import java.util.Objects; - -import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * OID4VCI-specific authorization details response that extends the generic response - * with OID4VCI-specific fields like credential_identifiers. - * - * @author Forkim Akwichek - */ -public class OID4VCAuthorizationDetailResponse extends OID4VCAuthorizationDetail { - - public static final String CREDENTIAL_IDENTIFIERS = "credential_identifiers"; - - @JsonProperty(CREDENTIAL_IDENTIFIERS) - private List credentialIdentifiers; - - public List getCredentialIdentifiers() { - return credentialIdentifiers; - } - - public void setCredentialIdentifiers(List credentialIdentifiers) { - this.credentialIdentifiers = credentialIdentifiers; - } - - @Override - public String toString() { - return "OID4VCAuthorizationDetailsResponse {" + - " type='" + getType() + '\'' + - ", locations='" + getLocations() + '\'' + - ", credentialConfigurationId='" + getCredentialConfigurationId() + '\'' + - ", credentialIdentifiers=" + credentialIdentifiers + - ", claims=" + getClaims() + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - OID4VCAuthorizationDetailResponse that = (OID4VCAuthorizationDetailResponse) o; - return Objects.equals(credentialIdentifiers, that.credentialIdentifiers); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), credentialIdentifiers); - } -} 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 d6751c3e07a..7e506a880ea 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 @@ -48,15 +48,15 @@ import org.keycloak.util.JsonSerialization; import org.jboss.logging.Logger; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.CREDENTIAL_CONFIGURATION_ID; +import static org.keycloak.OID4VCConstants.CREDENTIAL_IDENTIFIERS; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.keycloak.models.Constants.AUTHORIZATION_DETAILS_RESPONSE; -import static org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse.CLAIMS; -import static org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse.CREDENTIAL_CONFIGURATION_ID; -import static org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse.CREDENTIAL_IDENTIFIERS; import static org.keycloak.protocol.oid4vc.model.ClaimsDescription.MANDATORY; import static org.keycloak.protocol.oid4vc.model.ClaimsDescription.PATH; +import static org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail.CLAIMS; -public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetailsProcessor { +public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetailsProcessor { private static final Logger logger = Logger.getLogger(OID4VCAuthorizationDetailsProcessor.class); private final KeycloakSession session; @@ -75,12 +75,12 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails } @Override - public Class getSupportedResponseJavaType() { - return OID4VCAuthorizationDetailResponse.class; + public Class getSupportedResponseJavaType() { + return OID4VCAuthorizationDetail.class; } @Override - public OID4VCAuthorizationDetailResponse process(UserSessionModel userSession, ClientSessionContext clientSessionCtx, AuthorizationDetailsJSONRepresentation authzDetail) { + public OID4VCAuthorizationDetail process(UserSessionModel userSession, ClientSessionContext clientSessionCtx, AuthorizationDetailsJSONRepresentation authzDetail) { OID4VCAuthorizationDetail detail = authzDetail.asSubtype(OID4VCAuthorizationDetail.class); Map supportedCredentials = OID4VCIssuerWellKnownProvider.getSupportedCredentials(session); @@ -89,7 +89,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails String issuerIdentifier = OID4VCIssuerWellKnownProvider.getIssuer(session.getContext()); validateAuthorizationDetail(detail, supportedCredentials, authorizationServers, issuerIdentifier); - OID4VCAuthorizationDetailResponse responseDetail = buildAuthorizationDetailResponse(detail, userSession, clientSessionCtx); + OID4VCAuthorizationDetail responseDetail = buildAuthorizationDetail(detail, userSession, clientSessionCtx); // For authorization code flow, create CredentialOfferState if credential identifiers are present // This allows credential requests with credential_identifier to find the associated offer state @@ -104,7 +104,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails * Processes all OID4VC authorization details to support multiple credential requests. */ private void createOfferStateForAuthorizationCodeFlow(UserSessionModel userSession, ClientSessionContext clientSessionCtx, - OID4VCAuthorizationDetailResponse oid4vcDetail) { + OID4VCAuthorizationDetail oid4vcDetail) { AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession(); ClientModel client = clientSession != null ? clientSession.getClient() : null; UserModel user = userSession != null ? userSession.getUser() : null; @@ -171,6 +171,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails String type = detail.getType(); String credentialConfigurationId = detail.getCredentialConfigurationId(); + List credentialIdentifiers = detail.getCredentialIdentifiers(); List claims = detail.getClaims(); // Validate type first @@ -188,6 +189,12 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails } } + // credential_identifiers not allowed + if (credentialIdentifiers != null && !credentialIdentifiers.isEmpty()) { + logger.warnf("Property credential_identifiers not allowed in authorization_details"); + throw getInvalidRequestException("credential_identifiers not allowed"); + } + // credential_configuration_id is REQUIRED if (credentialConfigurationId == null) { logger.warnf("Missing credential_configuration_id in authorization_details"); @@ -260,12 +267,12 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails } } - private OID4VCAuthorizationDetailResponse buildAuthorizationDetailResponse(OID4VCAuthorizationDetail detail, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + private OID4VCAuthorizationDetail buildAuthorizationDetail(OID4VCAuthorizationDetail detail, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { String credentialConfigurationId = detail.getCredentialConfigurationId(); // Try to reuse identifier from authorizationDetailsResponse in client session context List previousResponses = clientSessionCtx.getAttribute(AUTHORIZATION_DETAILS_RESPONSE, List.class); - List oid4vcPreviousResponses = getSupportedAuthorizationDetails(previousResponses); + List oid4vcPreviousResponses = getSupportedAuthorizationDetails(previousResponses); List credentialIdentifiers = oid4vcPreviousResponses != null && !oid4vcPreviousResponses.isEmpty() ? oid4vcPreviousResponses.get(0).getCredentialIdentifiers() : null; @@ -275,7 +282,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails credentialIdentifiers.add(UUID.randomUUID().toString()); } - OID4VCAuthorizationDetailResponse responseDetail = new OID4VCAuthorizationDetailResponse(); + OID4VCAuthorizationDetail responseDetail = new OID4VCAuthorizationDetail(); responseDetail.setType(OPENID_CREDENTIAL); responseDetail.setCredentialConfigurationId(credentialConfigurationId); responseDetail.setCredentialIdentifiers(credentialIdentifiers); @@ -292,7 +299,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails * @param clientSession the client session that contains the credential offer information * @return the authorization details response if generation was successful, null otherwise */ - private List generateAuthorizationDetailsFromCredentialOffer(AuthenticatedClientSessionModel clientSession) { + private List generateAuthorizationDetailsFromCredentialOffer(AuthenticatedClientSessionModel clientSession) { logger.debug("Processing authorization_details from credential offer"); // Get supported credentials @@ -311,7 +318,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails } // Generate authorization_details for each credential configuration - List authorizationDetailsList = new ArrayList<>(); + List authorizationDetailsList = new ArrayList<>(); for (String credentialConfigurationId : credentialConfigurationIds) { SupportedCredentialConfiguration config = supportedCredentials.get(credentialConfigurationId); @@ -324,7 +331,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails logger.debugf("Generated credential identifier '%s' for configuration '%s'", credentialIdentifier, credentialConfigurationId); - OID4VCAuthorizationDetailResponse authDetail = new OID4VCAuthorizationDetailResponse(); + OID4VCAuthorizationDetail authDetail = new OID4VCAuthorizationDetail(); authDetail.setType(OPENID_CREDENTIAL); authDetail.setCredentialConfigurationId(credentialConfigurationId); authDetail.setCredentialIdentifiers(List.of(credentialIdentifier)); @@ -363,7 +370,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails } @Override - public List handleMissingAuthorizationDetails(UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + public List handleMissingAuthorizationDetails(UserSessionModel userSession, ClientSessionContext clientSessionCtx) { // Only generate authorization_details from credential offer if: // 1. No authorization_details were processed yet, AND // 2. There's a credential offer note in the client session (indicating this is a credential offer flow) @@ -377,7 +384,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails } @Override - public OID4VCAuthorizationDetailResponse processStoredAuthorizationDetails(UserSessionModel userSession, ClientSessionContext clientSessionCtx, AuthorizationDetailsJSONRepresentation storedAuthDetails) + public OID4VCAuthorizationDetail processStoredAuthorizationDetails(UserSessionModel userSession, ClientSessionContext clientSessionCtx, AuthorizationDetailsJSONRepresentation storedAuthDetails) throws InvalidAuthorizationDetailsException { if (storedAuthDetails == null) { return null; @@ -412,15 +419,6 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails fillFields(authzDetail, detail); return clazz.cast(detail); } - } else if (OID4VCAuthorizationDetailResponse.class.equals(clazz)) { - if (authzDetail instanceof OID4VCAuthorizationDetailResponse) { - return clazz.cast(authzDetail); - } else { - OID4VCAuthorizationDetailResponse detail = new OID4VCAuthorizationDetailResponse(); - fillFields(authzDetail, detail); - detail.setCredentialIdentifiers((List) authzDetail.getCustomData().get(CREDENTIAL_IDENTIFIERS)); - return clazz.cast(detail); - } } else { throw new IllegalArgumentException("Authorization details '" + authzDetail + "' is unsupported to be parsed to '" + clazz + "'."); } @@ -430,6 +428,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails outDetail.setType(inDetail.getType()); outDetail.setLocations(inDetail.getLocations()); outDetail.setCredentialConfigurationId((String) inDetail.getCustomData().get(CREDENTIAL_CONFIGURATION_ID)); + outDetail.setCredentialIdentifiers((List) inDetail.getCustomData().get(CREDENTIAL_IDENTIFIERS)); outDetail.setClaims(parseClaims((List) inDetail.getCustomData().get(CLAIMS))); } diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorFactory.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorFactory.java index b0719f12e7d..7f214f0e29b 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorFactory.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorFactory.java @@ -22,7 +22,7 @@ import org.keycloak.protocol.oid4vc.OID4VCEnvironmentProviderFactory; import org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessorFactory; import org.keycloak.util.AuthorizationDetailsParser; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; /** * Factory for creating OID4VCI-specific authorization details processors. diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java index 87cefe5df4d..ae0a66a35d9 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java @@ -101,6 +101,7 @@ import org.keycloak.protocol.oid4vc.model.ErrorResponse; import org.keycloak.protocol.oid4vc.model.ErrorType; import org.keycloak.protocol.oid4vc.model.JwtProof; import org.keycloak.protocol.oid4vc.model.NonceResponse; +import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail; import org.keycloak.protocol.oid4vc.model.OfferUriType; import org.keycloak.protocol.oid4vc.model.PreAuthorizedCode; import org.keycloak.protocol.oid4vc.model.PreAuthorizedGrant; @@ -136,7 +137,7 @@ import org.apache.http.client.methods.HttpOptions; import org.apache.http.client.methods.HttpPost; import org.jboss.logging.Logger; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.keycloak.constants.OID4VCIConstants.CREDENTIAL_OFFER_CREATE; import static org.keycloak.constants.OID4VCIConstants.OID4VC_PROTOCOL; import static org.keycloak.protocol.oid4vc.model.ErrorType.INVALID_CREDENTIAL_OFFER_REQUEST; @@ -795,9 +796,9 @@ public class OID4VCIssuerEndpoint { } CredentialScopeModel requestedCredential; - OID4VCAuthorizationDetailResponse authDetails; CredentialOfferState offerState = null; CredentialOfferStorage offerStorage = null; + OID4VCAuthorizationDetail authDetails; // When the CredentialRequest contains a credential identifier the caller must have gone through the // CredentialOffer process or otherwise have set up a valid CredentialOfferState @@ -831,7 +832,7 @@ public class OID4VCIssuerEndpoint { // Validate that authorization_details from the token matches the offer state // This ensures the correct access token is being used for the credential request - OID4VCAuthorizationDetailResponse tokenAuthDetails = getAuthorizationDetailFromToken(accessToken); + OID4VCAuthorizationDetail tokenAuthDetails = getAuthorizationDetailFromToken(accessToken); if (tokenAuthDetails != null && !tokenAuthDetails.equals(authDetails)) { var errorMessage = "Authorization details in access token do not match the credential offer state. " + "The access token may not be the one issued for this credential offer."; @@ -954,10 +955,10 @@ public class OID4VCIssuerEndpoint { return response; } - private OID4VCAuthorizationDetailResponse getAuthorizationDetailFromToken(AccessToken accessToken) { + private OID4VCAuthorizationDetail getAuthorizationDetailFromToken(AccessToken accessToken) { List tokenAuthDetails = accessToken.getAuthorizationDetails(); - AuthorizationDetailsProcessor oid4vcProcessor = session.getProvider(AuthorizationDetailsProcessor.class, OPENID_CREDENTIAL); - List oid4vcResponses = oid4vcProcessor.getSupportedAuthorizationDetails(tokenAuthDetails); + AuthorizationDetailsProcessor oid4vcProcessor = session.getProvider(AuthorizationDetailsProcessor.class, OPENID_CREDENTIAL); + List oid4vcResponses = oid4vcProcessor.getSupportedAuthorizationDetails(tokenAuthDetails); return oid4vcResponses == null || oid4vcResponses.isEmpty() ? null : oid4vcResponses.get(0); } @@ -1442,7 +1443,7 @@ public class OID4VCIssuerEndpoint { */ private Object getCredential(AuthenticationManager.AuthResult authResult, SupportedCredentialConfiguration credentialConfig, - OID4VCAuthorizationDetailResponse authDetail, + OID4VCAuthorizationDetail authDetail, CredentialRequest credentialRequestVO, EventBuilder eventBuilder ) { @@ -1536,7 +1537,7 @@ public class OID4VCIssuerEndpoint { // builds the unsigned credential by applying all protocol mappers. private VCIssuanceContext getVCToSign(List protocolMappers, SupportedCredentialConfiguration credentialConfig, - AuthenticationManager.AuthResult authResult, OID4VCAuthorizationDetailResponse authDetail, CredentialRequest credentialRequestVO, + AuthenticationManager.AuthResult authResult, OID4VCAuthorizationDetail authDetail, CredentialRequest credentialRequestVO, CredentialScopeModel credentialScopeModel, EventBuilder eventBuilder) { // Compute issuance date and apply correlation-mitigation according to realm configuration @@ -1650,7 +1651,7 @@ public class OID4VCIssuerEndpoint { * @throws BadRequestException if mandatory requested claims are missing */ private void validateRequestedClaimsArePresent(Map allClaims, SupportedCredentialConfiguration credentialConfig, - UserModel user, OID4VCAuthorizationDetailResponse authzDetail, String scope, EventBuilder eventBuilder) { + UserModel user, OID4VCAuthorizationDetail authzDetail, String scope, EventBuilder eventBuilder) { // Protocol mappers from configuration Map, ClaimsDescription> claimsConfig = credentialConfig.getCredentialMetadata().getClaims() .stream() @@ -1696,7 +1697,7 @@ public class OID4VCIssuerEndpoint { } - private List getClaimsFromAuthzDetails(String scope, UserModel user, OID4VCAuthorizationDetailResponse authzDetail) { + private List getClaimsFromAuthzDetails(String scope, UserModel user, OID4VCAuthorizationDetail authzDetail) { List storedClaims = authzDetail == null ? null : authzDetail.getClaims(); if (storedClaims == null || storedClaims.isEmpty()) { String username = user.getUsername(); diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/credentialoffer/CredentialOfferStorage.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/credentialoffer/CredentialOfferStorage.java index 142997456c7..1461c34a003 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/credentialoffer/CredentialOfferStorage.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/credentialoffer/CredentialOfferStorage.java @@ -22,8 +22,8 @@ import java.util.Optional; import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Time; import org.keycloak.models.KeycloakSession; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.model.CredentialsOffer; +import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail; import org.keycloak.protocol.oid4vc.model.PreAuthorizedCode; import org.keycloak.protocol.oid4vc.model.PreAuthorizedGrant; import org.keycloak.provider.Provider; @@ -41,7 +41,7 @@ public interface CredentialOfferStorage extends Provider { private String userId; private String nonce; private int expiration; - private OID4VCAuthorizationDetailResponse authorizationDetails; + private OID4VCAuthorizationDetail authorizationDetails; public CredentialOfferState(CredentialsOffer credOffer, String clientId, String userId, int expiration) { this.credentialsOffer = credOffer; @@ -88,11 +88,11 @@ public interface CredentialOfferStorage extends Provider { return expiration; } - public OID4VCAuthorizationDetailResponse getAuthorizationDetails() { + public OID4VCAuthorizationDetail getAuthorizationDetails() { return authorizationDetails; } - public void setAuthorizationDetails(OID4VCAuthorizationDetailResponse authorizationDetails) { + public void setAuthorizationDetails(OID4VCAuthorizationDetail authorizationDetails) { this.authorizationDetails = authorizationDetails; } diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/OID4VCAuthorizationDetail.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/OID4VCAuthorizationDetail.java index fd94838ffc4..c10b4693dd5 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/model/OID4VCAuthorizationDetail.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/OID4VCAuthorizationDetail.java @@ -20,23 +20,36 @@ import java.util.List; import java.util.Objects; import org.keycloak.representations.AuthorizationDetailsJSONRepresentation; +import org.keycloak.util.JsonSerialization; import com.fasterxml.jackson.annotation.JsonProperty; +import static org.keycloak.OID4VCConstants.CREDENTIAL_CONFIGURATION_ID; +import static org.keycloak.OID4VCConstants.CREDENTIAL_IDENTIFIERS; + /** * Represents an authorization_details object in the Token Request as per OID4VCI. - * + * * @author Forkim Akwichek */ public class OID4VCAuthorizationDetail extends AuthorizationDetailsJSONRepresentation { - public static final String CREDENTIAL_CONFIGURATION_ID = "credential_configuration_id"; - public static final String CREDENTIAL_IDENTIFIERS = "credential_identifiers"; public static final String CLAIMS = "claims"; @JsonProperty(CREDENTIAL_CONFIGURATION_ID) private String credentialConfigurationId; + /** + * The 'credential_identifiers' property is populated by the Issuer in the AccessToken Response + *

+ * Identifying Credentials Being Issued Throughout the Issuance Flow + * https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-3.3.4 + *

+ * The property should not be used in Authorization or AccessToken requests. + */ + @JsonProperty(CREDENTIAL_IDENTIFIERS) + private List credentialIdentifiers; + @JsonProperty(CLAIMS) private List claims; @@ -48,6 +61,14 @@ public class OID4VCAuthorizationDetail extends AuthorizationDetailsJSONRepresent this.credentialConfigurationId = credentialConfigurationId; } + public List getCredentialIdentifiers() { + return credentialIdentifiers; + } + + public void setCredentialIdentifiers(List credentialIdentifiers) { + this.credentialIdentifiers = credentialIdentifiers; + } + public List getClaims() { return claims; } @@ -58,12 +79,7 @@ public class OID4VCAuthorizationDetail extends AuthorizationDetailsJSONRepresent @Override public String toString() { - return "OID4VCAuthorizationDetail {" + - " type='" + getType() + '\'' + - ", locations='" + getLocations() + '\'' + - ", credentialConfigurationId='" + credentialConfigurationId + '\'' + - ", claims=" + claims + - '}'; + return JsonSerialization.valueAsString(this); } @Override diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java index 4104e1a9c47..cabe42dc1d6 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java @@ -33,10 +33,10 @@ import org.keycloak.models.ClientSessionContext; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserSessionModel; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider; import org.keycloak.protocol.oid4vc.issuance.credentialoffer.CredentialOfferStorage; +import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.TokenManager.AccessTokenResponseBuilder; import org.keycloak.representations.AccessToken; @@ -171,7 +171,7 @@ public class PreAuthorizedCodeGrantType extends OAuth2GrantTypeBase { } // Add authorization_details to the OfferState and otherClaims - var authDetails = (OID4VCAuthorizationDetailResponse) authorizationDetailsResponses.get(0); + var authDetails = (OID4VCAuthorizationDetail) authorizationDetailsResponses.get(0); offerState.setAuthorizationDetails(authDetails); offerStorage.replaceOfferState(session, offerState); diff --git a/services/src/test/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorTest.java b/services/src/test/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorTest.java index 9867056b4b5..f4b88702fe1 100644 --- a/services/src/test/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorTest.java +++ b/services/src/test/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorTest.java @@ -30,8 +30,8 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; -import static org.keycloak.OAuth2Constants.CREDENTIAL_IDENTIFIERS; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.CREDENTIAL_IDENTIFIERS; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -135,17 +135,19 @@ public class OID4VCAuthorizationDetailsProcessorTest { assertNotNull("Locations should not be null", authDetail.getLocations()); assertEquals("Should have exactly one location", 1, authDetail.getLocations().size()); assertEquals("Location should match issuer", "https://test-issuer.com", authDetail.getLocations().get(0)); + assertNull("Has no credential_identifier", authDetail.getCredentialIdentifiers()); } /** * Asserts that an AuthorizationDetail has valid structure */ - private void assertValidAuthorizationDetailResponse(OID4VCAuthorizationDetailResponse authDetail) { + private void assertValidAuthorizationDetailResponse(OID4VCAuthorizationDetail authDetail) { assertEquals("Type should be openid_credential", OPENID_CREDENTIAL, authDetail.getType()); assertEquals("Credential configuration ID should be set", "test-config-id", authDetail.getCredentialConfigurationId()); assertNotNull("Locations should not be null", authDetail.getLocations()); assertEquals("Should have exactly one location", 1, authDetail.getLocations().size()); assertEquals("Location should match issuer", "https://test-issuer.com", authDetail.getLocations().get(0)); + assertNotNull("Has credential_identifier", authDetail.getCredentialIdentifiers()); assertEquals(1, authDetail.getCredentialIdentifiers().size()); assertEquals("test-identifier-123", authDetail.getCredentialIdentifiers().get(0)); } @@ -391,7 +393,7 @@ public class OID4VCAuthorizationDetailsProcessorTest { } @Test - public void testBuildAuthorizationDetailResponseLogic() { + public void testBuildAuthorizationDetailLogic() { // Test the response structure that would be built String expectedCredentialConfigurationId = "test-config-id"; List expectedCredentialIdentifiers = List.of("test-identifier-123"); @@ -403,7 +405,7 @@ public class OID4VCAuthorizationDetailsProcessorTest { authDetail.setCustomData(CREDENTIAL_IDENTIFIERS, expectedCredentialIdentifiers); authDetail.setClaims(expectedClaims); - // Verify the data structure that buildAuthorizationDetailResponse() would process + // Verify the data structure that buildAuthorizationDetail() would process assertValidAuthorizationDetail(authDetail); assertNotNull("Claims should not be null", authDetail.getClaims()); assertEquals("Should have exactly one claim", 1, authDetail.getClaims().size()); @@ -512,7 +514,7 @@ public class OID4VCAuthorizationDetailsProcessorTest { convertToResponseType(validDetail2), convertToResponseType(invalidDetail1) ); - List authzResponses = new OID4VCAuthorizationDetailsProcessor(null).getSupportedAuthorizationDetails(responses); + List authzResponses = new OID4VCAuthorizationDetailsProcessor(null).getSupportedAuthorizationDetails(responses); Assert.assertEquals(2, authzResponses.size()); assertValidAuthorizationDetailResponse(authzResponses.get(0)); diff --git a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AbstractUrlBuilder.java b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AbstractUrlBuilder.java index ac8cbd664ee..f33cb4fb0f3 100644 --- a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AbstractUrlBuilder.java +++ b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AbstractUrlBuilder.java @@ -1,6 +1,5 @@ package org.keycloak.testsuite.util.oauth; -import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; @@ -33,12 +32,8 @@ public abstract class AbstractUrlBuilder { } protected void parameter(String name, Object value) { - try { - String encoded = URLEncoder.encode(JsonSerialization.writeValueAsString(value), StandardCharsets.UTF_8); - parameter(name, encoded); - } catch (IOException e) { - throw new RuntimeException(e); - } + String encoded = URLEncoder.encode(JsonSerialization.valueAsString(value), StandardCharsets.UTF_8); + parameter(name, encoded); } public String build() { diff --git a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AccessTokenResponse.java b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AccessTokenResponse.java index 4191422bc1a..8ecf71457cd 100644 --- a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AccessTokenResponse.java +++ b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AccessTokenResponse.java @@ -2,17 +2,15 @@ package org.keycloak.testsuite.util.oauth; import java.io.IOException; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.keycloak.OAuth2Constants; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; +import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail; import org.keycloak.representations.AuthorizationDetailsJSONRepresentation; import org.keycloak.util.JsonSerialization; -import com.fasterxml.jackson.core.type.TypeReference; import org.apache.http.client.methods.CloseableHttpResponse; public class AccessTokenResponse extends AbstractHttpResponse { @@ -126,7 +124,17 @@ public class AccessTokenResponse extends AbstractHttpResponse { return authorizationDetails; } - public List getAuthorizationDetails(Class clazz) { + /** + * Get authorization details as OID4VC-specific response objects. + * This is useful when you need to access OID4VC-specific fields like credential_identifiers. + * + * @return a list of authorization details, or an empty list if none are present. + */ + public List getOid4vcAuthorizationDetails() { + return getAuthorizationDetails(OID4VCAuthorizationDetail.class); + } + + private List getAuthorizationDetails(Class clazz) { if (authorizationDetails == null) { return null; } @@ -134,30 +142,4 @@ public class AccessTokenResponse extends AbstractHttpResponse { .map(authzResponse -> authzResponse.asSubtype(clazz)) .toList(); } - - /** - * Get authorization details as OID4VC-specific response objects. - * This is useful when you need to access OID4VC-specific fields like credential_identifiers. - * - * @return a list of authorization details, or an empty list if none are present. - * @throws RuntimeException if there's an error parsing the JSON response - */ - public List getOid4vcAuthorizationDetails() { - if (responseJson == null) { - return Collections.emptyList(); - } - Object authDetailsObj = responseJson.get(OAuth2Constants.AUTHORIZATION_DETAILS); - if (authDetailsObj == null) { - return Collections.emptyList(); - } - try { - return JsonSerialization.readValue( - JsonSerialization.writeValueAsString(authDetailsObj), - new TypeReference>() { - } - ); - } catch (IOException e) { - throw new RuntimeException("Failed to parse authorization_details from token response", e); - } - } } diff --git a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/LoginUrlBuilder.java b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/LoginUrlBuilder.java index a1da41104c3..21c43d55472 100644 --- a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/LoginUrlBuilder.java +++ b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/LoginUrlBuilder.java @@ -1,5 +1,7 @@ package org.keycloak.testsuite.util.oauth; +import java.util.Arrays; + import org.keycloak.OAuth2Constants; import org.keycloak.models.Constants; import org.keycloak.protocol.oidc.OIDCLoginProtocol; @@ -21,6 +23,12 @@ public class LoginUrlBuilder extends AbstractUrlBuilder { return this; } + public LoginUrlBuilder scope(String... scopes) { + String joinedScopes = String.join(" ", Arrays.asList(scopes)); + parameter(OAuth2Constants.SCOPE, joinedScopes); + return this; + } + public LoginUrlBuilder state(String state) { parameter(OIDCLoginProtocol.STATE_PARAM, state); return this; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/OID4VCICredentialOfferMatrixTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/OID4VCICredentialOfferMatrixTest.java index 803dda30939..239661a11c2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/OID4VCICredentialOfferMatrixTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/OID4VCICredentialOfferMatrixTest.java @@ -27,7 +27,6 @@ import org.keycloak.OAuth2Constants; import org.keycloak.TokenVerifier; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.jose.jws.JWSInput; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.model.CredentialIssuer; import org.keycloak.protocol.oid4vc.model.CredentialOfferURI; import org.keycloak.protocol.oid4vc.model.CredentialRequest; @@ -54,8 +53,8 @@ import org.apache.directory.api.util.Strings; import org.apache.http.HttpStatus; import org.junit.Test; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; import static org.keycloak.OAuth2Constants.SCOPE_OPENID; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.keycloak.constants.OID4VCIConstants.CREDENTIAL_OFFER_CREATE; import static org.keycloak.protocol.oid4vc.model.ErrorType.INVALID_CREDENTIAL_OFFER_REQUEST; import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId; @@ -122,7 +121,7 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest { public void testCredentialWithoutOffer() throws Exception { var ctx = new TestContext(false, null, appUsername); - OID4VCAuthorizationDetailResponse authDetail = new OID4VCAuthorizationDetailResponse(); + OID4VCAuthorizationDetail authDetail = new OID4VCAuthorizationDetail(); authDetail.setType(OPENID_CREDENTIAL); authDetail.setCredentialConfigurationId(credConfigId); authDetail.setLocations(List.of(ctx.issuerMetadata.getCredentialIssuer())); @@ -138,14 +137,14 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest { // When authorization_details are sent in token request, they are returned in token response with credential_identifiers // The credential request MUST use credential_identifier (not credential_configuration_id) - List authDetailsResponse = JsonSerialization.readValue( + List authDetailsResponse = JsonSerialization.readValue( JsonSerialization.writeValueAsString(tokenAuthDetails), new TypeReference<>() {} ); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertFalse("authorization_details should not be empty", authDetailsResponse.isEmpty()); - OID4VCAuthorizationDetailResponse authDetailResponse = authDetailsResponse.get(0); + OID4VCAuthorizationDetail authDetailResponse = authDetailsResponse.get(0); List credentialIdentifiers = authDetailResponse.getCredentialIdentifiers(); assertNotNull("credential_identifiers should be present", credentialIdentifiers); assertFalse("credential_identifiers should not be empty", credentialIdentifiers.isEmpty()); @@ -273,14 +272,14 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest { // 4. does not reflect anything from the credential offer // AccessTokenResponse accessToken = getPreAuthorizedAccessTokenResponse(ctx, credOffer); - List authDetailsResponse = accessToken.getOid4vcAuthorizationDetails(); + List authDetailsResponse = accessToken.getOid4vcAuthorizationDetails(); if (authDetailsResponse == null || authDetailsResponse.isEmpty()) { throw new IllegalStateException("No authorization_details in token response"); } if (authDetailsResponse.size() > 1) { throw new IllegalStateException("Multiple authorization_details in token response"); } - OID4VCAuthorizationDetailResponse authDetailResponse = authDetailsResponse.get(0); + OID4VCAuthorizationDetail authDetailResponse = authDetailsResponse.get(0); // Get the credential and verify // @@ -340,9 +339,9 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest { } } - private List extractAuthorizationDetails(AccessTokenResponse tokenResponse) { + private List extractAuthorizationDetails(AccessTokenResponse tokenResponse) { // First check if already populated in token response - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); if (authDetailsResponse != null && !authDetailsResponse.isEmpty()) { return authDetailsResponse; } @@ -422,7 +421,7 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest { return accessTokenResponse; } - private CredentialResponse getCredentialByAuthDetail(String accessToken, OID4VCAuthorizationDetailResponse authDetail) throws Exception { + private CredentialResponse getCredentialByAuthDetail(String accessToken, OID4VCAuthorizationDetail authDetail) throws Exception { var credentialRequest = new CredentialRequest(); if (authDetail.getCredentialIdentifiers() != null) { credentialRequest.setCredentialIdentifier(authDetail.getCredentialIdentifiers().get(0)); @@ -439,7 +438,7 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest { var credentialRequest = new CredentialRequest(); // Extract authorization_details (from token response or JWT) - List authDetailsResponse = extractAuthorizationDetails(tokenResponse); + List authDetailsResponse = extractAuthorizationDetails(tokenResponse); if (authDetailsResponse != null && !authDetailsResponse.isEmpty()) { // If authorization_details are present, credential_identifier is required diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAttestationProofTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAttestationProofTest.java index 6288d28669f..df9fff43274 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAttestationProofTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAttestationProofTest.java @@ -17,7 +17,6 @@ import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.oid4vci.CredentialScopeModel; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.issuance.VCIssuanceContext; import org.keycloak.protocol.oid4vc.issuance.VCIssuerException; @@ -40,7 +39,7 @@ import org.keycloak.util.JsonSerialization; import org.jboss.logging.Logger; import org.junit.Test; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -154,7 +153,7 @@ public class OID4VCAttestationProofTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertFalse("authorization_details should not be empty", authDetailsResponse.isEmpty()); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); 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 d47b1b93ce2..e2c09afb3e4 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 @@ -35,7 +35,6 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.oid4vci.CredentialScopeModel; import org.keycloak.models.oid4vci.Oid4vcProtocolMapperModel; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.model.ClaimsDescription; import org.keycloak.protocol.oid4vc.model.CredentialIssuer; import org.keycloak.protocol.oid4vc.model.CredentialRequest; @@ -66,7 +65,7 @@ import org.hamcrest.Matchers; import org.junit.Rule; import org.junit.Test; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -252,7 +251,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn // Extract values from refreshed token for credential request String accessToken = tokenResponseRef.getAccessToken(); - List authDetails = tokenResponseRef.getOid4vcAuthorizationDetails(); + List authDetails = tokenResponseRef.getOid4vcAuthorizationDetails(); String credentialIdentifier = null; if (authDetails != null && !authDetails.isEmpty()) { @@ -991,11 +990,11 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn // Test successful token response. Returns "Credential identifier" of the VC credential private String assertTokenResponse(AccessTokenResponse tokenResponse) throws Exception { // Extract authorization_details from token response - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertEquals(1, authDetailsResponse.size()); - OID4VCAuthorizationDetailResponse authDetailResponse = authDetailsResponse.get(0); + OID4VCAuthorizationDetail authDetailResponse = authDetailsResponse.get(0); assertNotNull("Credential identifiers should be present", authDetailResponse.getCredentialIdentifiers()); assertEquals(1, authDetailResponse.getCredentialIdentifiers().size()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowWithPARTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowWithPARTest.java index 0e0d348bc74..a98e4165b23 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowWithPARTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowWithPARTest.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.UUID; import org.keycloak.models.oid4vci.CredentialScopeModel; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.model.ClaimsDescription; import org.keycloak.protocol.oid4vc.model.CredentialIssuer; import org.keycloak.protocol.oid4vc.model.CredentialResponse; @@ -39,7 +38,7 @@ import org.keycloak.testsuite.util.oauth.oid4vc.Oid4vcCredentialResponse; import org.apache.http.HttpStatus; import org.junit.Test; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -153,11 +152,11 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode()); // Step 4: Verify authorization_details is present in token response - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertEquals("Should have exactly one authorization detail", 1, authDetailsResponse.size()); - OID4VCAuthorizationDetailResponse authDetailResponse = authDetailsResponse.get(0); + OID4VCAuthorizationDetail authDetailResponse = authDetailsResponse.get(0); assertEquals("Type should be openid_credential", OPENID_CREDENTIAL, authDetailResponse.getType()); assertEquals("Credential configuration ID should match", credentialConfigurationId, authDetailResponse.getCredentialConfigurationId()); @@ -288,7 +287,7 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode()); // Step 4: Verify NO authorization_details in token response (since none was in PAR request) - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertTrue("authorization_details should NOT be present in the response when not used in PAR request", authDetailsResponse == null || authDetailsResponse.isEmpty()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java index 4c03f9c2ba6..567dcb16a79 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java @@ -17,6 +17,8 @@ package org.keycloak.testsuite.oid4vc.issuance.signing; +import java.net.URLEncoder; +import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -28,7 +30,6 @@ import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.oid4vci.CredentialScopeModel; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.model.ClaimsDescription; import org.keycloak.protocol.oid4vc.model.CredentialIssuer; import org.keycloak.protocol.oid4vc.model.CredentialOfferURI; @@ -41,6 +42,7 @@ import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.oauth.AccessTokenResponse; +import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse; import org.keycloak.testsuite.util.oauth.OAuthClient; import org.keycloak.testsuite.util.oauth.OpenIDProviderConfigurationResponse; import org.keycloak.testsuite.util.oauth.oid4vc.CredentialIssuerMetadataResponse; @@ -57,7 +59,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -167,6 +169,126 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue return ctx; } + @Test + public void testAuthorizationCodeFlowWithAuthorizationDetails() throws Exception { + + Oid4vcTestContext ctx = new Oid4vcTestContext(); + + // Get issuer metadata + CredentialIssuerMetadataResponse issuerMetadataResponse = oauth.oid4vc().issuerMetadataRequest().send(); + assertEquals(HttpStatus.SC_OK, issuerMetadataResponse.getStatusCode()); + ctx.credentialIssuer = issuerMetadataResponse.getMetadata(); + + // Get credential_configuration_id + ClientScopeRepresentation credClientScope = getCredentialClientScope(); + String credConfigId = credClientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID); + + // Build authorization_details + OID4VCAuthorizationDetail authDetail = new OID4VCAuthorizationDetail(); + authDetail.setType(OPENID_CREDENTIAL); + authDetail.setCredentialConfigurationId(credConfigId); + authDetail.setLocations(List.of(ctx.credentialIssuer.getCredentialIssuer())); + + String authDetailsJson = JsonSerialization.valueAsString(List.of(authDetail)); + String authDetailsEncoded = URLEncoder.encode(authDetailsJson, Charset.defaultCharset()); + + // [TODO #44320] Requires Credential scope in AuthorizationRequest although already given in AuthorizationDetails + AuthorizationEndpointResponse authEndpointResponse = oauth.loginForm() + .scope(credClientScope.getName()) + .param("authorization_details", authDetailsEncoded) + .doLogin("john","password"); + + String authCode = authEndpointResponse.getCode(); + assertNotNull("No authorization code", authCode); + + AccessTokenResponse tokenResponse = oauth.accessTokenRequest(authCode) + .authorizationDetails(List.of(authDetail)) + .send(); + assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode()); + + String accessToken = tokenResponse.getAccessToken(); + + String credentialIdentifier; + String credentialConfigurationId; + OID4VCAuthorizationDetail authDetailResponse; + + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + assertNotNull("authorization_details should be present in the response", authDetailsResponse); + assertEquals("Should have authorization_details for each credential configuration in the offer", + 1, authDetailsResponse.size()); + + // Use the first authorization detail for credential request + authDetailResponse = authDetailsResponse.get(0); + assertNotNull("Credential identifiers should be present", authDetailResponse.getCredentialIdentifiers()); + assertEquals(1, authDetailResponse.getCredentialIdentifiers().size()); + + credentialIdentifier = authDetailResponse.getCredentialIdentifiers().get(0); + assertNotNull("Credential identifier should not be null", credentialIdentifier); + + credentialConfigurationId = authDetailResponse.getCredentialConfigurationId(); + assertNotNull("Credential configuration id should not be null", credentialConfigurationId); + + Oid4vcCredentialResponse credentialResponse = oauth.oid4vc().credentialRequest() + .credentialIdentifier(credentialIdentifier) + .bearerToken(accessToken) + .send(); + + // Parse the credential response + CredentialResponse parsedResponse = credentialResponse.getCredentialResponse(); + assertNotNull("Credential response should not be null", parsedResponse); + assertNotNull("Credentials should be present", parsedResponse.getCredentials()); + assertEquals("Should have exactly one credential", 1, parsedResponse.getCredentials().size()); + + // Step 3: Verify that the issued credential structure is valid + CredentialResponse.Credential credentialWrapper = parsedResponse.getCredentials().get(0); + assertNotNull("Credential wrapper should not be null", credentialWrapper); + + // The credential is stored as Object, so we need to cast it + Object credentialObj = credentialWrapper.getCredential(); + assertNotNull("Credential object should not be null", credentialObj); + + // Verify the credential structure based on format + verifyCredentialStructure(credentialObj); + } + + @Test + public void testAuthorizationCodeFlowWithCredentialIdentifier() throws Exception { + + Oid4vcTestContext ctx = new Oid4vcTestContext(); + + // Get issuer metadata + CredentialIssuerMetadataResponse issuerMetadataResponse = oauth.oid4vc().issuerMetadataRequest().send(); + assertEquals(HttpStatus.SC_OK, issuerMetadataResponse.getStatusCode()); + ctx.credentialIssuer = issuerMetadataResponse.getMetadata(); + + // Get credential_configuration_id + ClientScopeRepresentation credClientScope = getCredentialClientScope(); + + // Build authorization_details + OID4VCAuthorizationDetail authDetail = new OID4VCAuthorizationDetail(); + authDetail.setType(OPENID_CREDENTIAL); + authDetail.setCredentialIdentifiers(List.of("credential_identifiers_not_allowed_here")); + authDetail.setLocations(List.of(ctx.credentialIssuer.getCredentialIssuer())); + + String authDetailsJson = JsonSerialization.valueAsString(List.of(authDetail)); + String authDetailsEncoded = URLEncoder.encode(authDetailsJson, Charset.defaultCharset()); + + // [TODO #44320] Requires Credential scope in AuthorizationRequest although already given in AuthorizationDetails + AuthorizationEndpointResponse authEndpointResponse = oauth.loginForm() + .scope(credClientScope.getName()) + .param("authorization_details", authDetailsEncoded) + .doLogin("john","password"); + + String authCode = authEndpointResponse.getCode(); + assertNotNull("No authorization code", authCode); + + AccessTokenResponse tokenResponse = oauth.accessTokenRequest(authCode) + .authorizationDetails(List.of(authDetail)) + .send(); + assertEquals(HttpStatus.SC_BAD_REQUEST, tokenResponse.getStatusCode()); + assertTrue(tokenResponse.getErrorDescription().contains("credential_identifiers not allowed")); + } + @Test public void testPreAuthorizedCodeWithAuthorizationDetailsCredentialConfigurationId() throws Exception { String token = getBearerToken(oauth, client, getCredentialClientScope().getName()); @@ -187,10 +309,10 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue .send(); assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode()); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertEquals(1, authDetailsResponse.size()); - OID4VCAuthorizationDetailResponse authDetailResponse = authDetailsResponse.get(0); + OID4VCAuthorizationDetail authDetailResponse = authDetailsResponse.get(0); assertEquals(OPENID_CREDENTIAL, authDetailResponse.getType()); assertEquals(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID), authDetailResponse.getCredentialConfigurationId()); assertNotNull(authDetailResponse.getCredentialIdentifiers()); @@ -241,10 +363,10 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue .send(); assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode()); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertEquals(1, authDetailsResponse.size()); - OID4VCAuthorizationDetailResponse authDetailResponse = authDetailsResponse.get(0); + OID4VCAuthorizationDetail authDetailResponse = authDetailsResponse.get(0); assertEquals(OPENID_CREDENTIAL, authDetailResponse.getType()); assertEquals(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID), authDetailResponse.getCredentialConfigurationId()); assertNotNull(authDetailResponse.getClaims()); @@ -364,7 +486,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue } else { // If it succeeds, verify the response structure assertEquals(HttpStatus.SC_OK, statusCode); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertEquals(1, authDetailsResponse.size()); } @@ -462,7 +584,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode()); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertEquals("Should have authorization_details for each credential configuration in the offer", ctx.credentialsOffer.getCredentialConfigurationIds().size(), authDetailsResponse.size()); @@ -470,7 +592,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue // Verify each credential configuration from the offer has corresponding authorization_details for (int i = 0; i < ctx.credentialsOffer.getCredentialConfigurationIds().size(); i++) { String expectedConfigId = ctx.credentialsOffer.getCredentialConfigurationIds().get(i); - OID4VCAuthorizationDetailResponse authDetailResponse = authDetailsResponse.get(i); + OID4VCAuthorizationDetail authDetailResponse = authDetailsResponse.get(i); assertEquals(OPENID_CREDENTIAL, authDetailResponse.getType()); assertEquals("Credential configuration ID should match the one from the offer", @@ -490,7 +612,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue } @Test - public void testCompleteFlowWithCredentialOfferBasedAuthorizationDetails() throws Exception { + public void testPreAuthorizedFlowWithCredentialOfferBasedAuthorizationDetails() throws Exception { String token = getBearerToken(oauth, client, getCredentialClientScope().getName()); Oid4vcTestContext ctx = prepareOid4vcTestContext(token); @@ -504,11 +626,11 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue String credentialIdentifier; String credentialConfigurationId; - OID4VCAuthorizationDetailResponse authDetailResponse; + OID4VCAuthorizationDetail authDetailResponse; assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode()); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertEquals("Should have authorization_details for each credential configuration in the offer", ctx.credentialsOffer.getCredentialConfigurationIds().size(), authDetailsResponse.size()); @@ -589,7 +711,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue String preAuthorizedToken = accessTokenResponse.getAccessToken(); assertNotNull("Access token should be present", preAuthorizedToken); - List authDetailsResponse = accessTokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = accessTokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present", authDetailsResponse); assertFalse("authorization_details should not be empty", authDetailsResponse.isEmpty()); @@ -674,7 +796,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode()); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); // Verify that we have authorization_details for each credential configuration in the offer @@ -683,7 +805,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue // Verify each authorization detail for (int i = 0; i < authDetailsResponse.size(); i++) { - OID4VCAuthorizationDetailResponse authDetail = authDetailsResponse.get(i); + OID4VCAuthorizationDetail authDetail = authDetailsResponse.get(i); String expectedConfigId = ctx.credentialsOffer.getCredentialConfigurationIds().get(i); // Verify structure @@ -750,10 +872,10 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue String credentialIdentifier; String credentialConfigurationId; - OID4VCAuthorizationDetailResponse authDetailResponse; + OID4VCAuthorizationDetail authDetailResponse; assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode()); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertEquals(1, authDetailsResponse.size()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsTypesSupportedTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsTypesSupportedTest.java index 1de38c362cb..228f4d39598 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsTypesSupportedTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsTypesSupportedTest.java @@ -33,7 +33,7 @@ import org.keycloak.testsuite.util.oauth.OAuthClient; import org.junit.Before; import org.junit.Test; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointEncryptionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointEncryptionTest.java index bbf4ef01cf9..d9f14b24098 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointEncryptionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointEncryptionTest.java @@ -39,7 +39,6 @@ import org.keycloak.jose.jwk.JWKParser; import org.keycloak.models.KeyManager; import org.keycloak.models.RealmModel; import org.keycloak.models.oid4vci.CredentialScopeModel; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.model.CredentialIssuer; import org.keycloak.protocol.oid4vc.model.CredentialRequest; @@ -59,7 +58,7 @@ import org.apache.http.HttpStatus; import org.jboss.logging.Logger; import org.junit.Test; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.keycloak.jose.jwe.JWEConstants.A256GCM; import static org.keycloak.protocol.oid4vc.model.ErrorType.INVALID_ENCRYPTION_PARAMETERS; import static org.keycloak.utils.MediaType.APPLICATION_JWT; @@ -92,7 +91,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest String authCode = getAuthorizationCode(oauth, client, "john", scopeName); AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertFalse("authorization_details should not be empty", authDetailsResponse.isEmpty()); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); @@ -201,7 +200,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest String authCode = getAuthorizationCode(oauth, client, "john", scopeName); AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertFalse("authorization_details should not be empty", authDetailsResponse.isEmpty()); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); @@ -281,7 +280,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest String authCode = getAuthorizationCode(oauth, client, "john", scopeName); AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertFalse("authorization_details should not be empty", authDetailsResponse.isEmpty()); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); @@ -547,7 +546,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest String authCode = getAuthorizationCode(oauth, client, "john", scopeName); AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertFalse("authorization_details should not be empty", authDetailsResponse.isEmpty()); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java index 7cfa2cc5149..8b20fa7cecd 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java @@ -74,7 +74,6 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserSessionModel; import org.keycloak.models.oid4vci.CredentialScopeModel; import org.keycloak.protocol.oid4vc.issuance.JWTVCIssuerWellKnownProviderFactory; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.issuance.TimeProvider; import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.CredentialBuilder; @@ -84,6 +83,7 @@ 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.DisplayObject; +import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail; import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration; import org.keycloak.protocol.oid4vc.model.VerifiableCredential; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; @@ -523,9 +523,9 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest { JsonWebToken jwt = new JWSInput(token).readJsonContent(JsonWebToken.class); Object authDetails = jwt.getOtherClaims().get(OAuth2Constants.AUTHORIZATION_DETAILS); if (authDetails != null) { - List authDetailsResponse = JsonSerialization.readValue( + List authDetailsResponse = JsonSerialization.readValue( JsonSerialization.writeValueAsString(authDetails), - new TypeReference>() { + new TypeReference>() { } ); if (!authDetailsResponse.isEmpty() && @@ -790,14 +790,14 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest { } } - protected List parseAuthorizationDetails(String responseBody) throws IOException { + protected List parseAuthorizationDetails(String responseBody) throws IOException { Map responseMap = JsonSerialization.readValue(responseBody, new TypeReference>() { }); Object authDetailsObj = responseMap.get("authorization_details"); assertNotNull("authorization_details should be present in the response", authDetailsObj); return JsonSerialization.readValue( JsonSerialization.writeValueAsString(authDetailsObj), - new TypeReference>() { + new TypeReference>() { } ); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java index 67df7b7c4c8..b1ed865a3cc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java @@ -37,7 +37,6 @@ import org.keycloak.common.VerificationException; import org.keycloak.common.util.Time; import org.keycloak.models.RealmModel; import org.keycloak.models.oid4vci.CredentialScopeModel; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider; import org.keycloak.protocol.oid4vc.issuance.credentialoffer.CredentialOfferStorage; @@ -75,8 +74,8 @@ import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; import static org.keycloak.OID4VCConstants.CREDENTIAL_SUBJECT; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -316,7 +315,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); try { @@ -374,7 +373,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertFalse("authorization_details should not be empty", authDetailsResponse.isEmpty()); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); @@ -501,7 +500,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { assertNotNull("Access token should be present", theToken); // Extract credential_identifier from authorization_details in token response - List authDetailsResponse = accessTokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = accessTokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertFalse("authorization_details should not be empty", authDetailsResponse.isEmpty()); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); @@ -644,7 +643,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); String cNonce = getCNonce(); @@ -952,7 +951,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertFalse("authorization_details should not be empty", authDetailsResponse.isEmpty()); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); @@ -999,7 +998,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); testingClient.server(TEST_REALM_NAME).run(session -> { @@ -1084,7 +1083,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); testingClient.server(TEST_REALM_NAME).run(session -> { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java index 5b5c700a14a..9be981c67dc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java @@ -34,7 +34,6 @@ import org.keycloak.models.ClientScopeModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.oid4vci.CredentialScopeModel; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider; import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.JwtCredentialBuilder; @@ -70,8 +69,8 @@ import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; import static org.keycloak.OID4VCConstants.CLAIM_NAME_SUBJECT_ID; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; @@ -102,7 +101,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); final String clientScopeString = toJsonString(sdJwtTypeCredentialClientScope); @@ -130,7 +129,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); final String clientScopeString = toJsonString(sdJwtTypeCredentialClientScope); @@ -163,7 +162,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); final String clientScopeString = toJsonString(sdJwtTypeCredentialClientScope); @@ -200,7 +199,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); final String clientScopeString = toJsonString(sdJwtTypeCredentialClientScope); @@ -245,7 +244,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); final String clientScopeString = toJsonString(sdJwtTypeCredentialClientScope); @@ -289,7 +288,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); final String clientScopeString = toJsonString(sdJwtTypeCredentialClientScope); @@ -424,7 +423,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest { assertNotNull("Access token should be present", theToken); // Extract credential_identifier from authorization_details in token response - List authDetailsResponse = accessTokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = accessTokenResponse.getOid4vcAuthorizationDetails(); assertNotNull("authorization_details should be present in the response", authDetailsResponse); assertFalse("authorization_details should not be empty", authDetailsResponse.isEmpty()); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java index e025e7857c4..ebf5566e414 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java @@ -112,8 +112,8 @@ import org.jboss.logging.Logger; import org.junit.Assert; import org.junit.BeforeClass; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; import static org.keycloak.OID4VCConstants.CLAIM_NAME_SUBJECT_ID; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.keycloak.testsuite.oid4vc.issuance.signing.OID4VCIssuerEndpointTest.TIME_PROVIDER; import static org.keycloak.testsuite.oid4vc.issuance.signing.OID4VCSdJwtIssuingEndpointTest.getCredentialIssuer; import static org.keycloak.testsuite.oid4vc.issuance.signing.OID4VCSdJwtIssuingEndpointTest.getJtiGeneratedIdMapper; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTimeNormalizationSdJwtTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTimeNormalizationSdJwtTest.java index d522b76e4d3..a0fca155819 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTimeNormalizationSdJwtTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTimeNormalizationSdJwtTest.java @@ -27,7 +27,6 @@ import org.keycloak.TokenVerifier; import org.keycloak.common.VerificationException; import org.keycloak.constants.OID4VCIConstants; import org.keycloak.models.oid4vci.CredentialScopeModel; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCIssuedAtTimeClaimMapper; import org.keycloak.protocol.oid4vc.model.CredentialIssuer; @@ -44,7 +43,7 @@ import org.keycloak.util.JsonSerialization; import org.apache.http.HttpStatus; import org.junit.Test; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -76,7 +75,7 @@ public class OID4VCTimeNormalizationSdJwtTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); final String clientScopeString = toJsonString(sdJwtTypeCredentialClientScope); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTimeNormalizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTimeNormalizationTest.java index 1b4f18aa9e2..1243b44d983 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTimeNormalizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTimeNormalizationTest.java @@ -26,7 +26,6 @@ import jakarta.ws.rs.core.Response; import org.keycloak.TokenVerifier; import org.keycloak.common.VerificationException; import org.keycloak.models.oid4vci.CredentialScopeModel; -import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.model.CredentialIssuer; import org.keycloak.protocol.oid4vc.model.CredentialRequest; @@ -40,7 +39,7 @@ import org.keycloak.util.JsonSerialization; import org.apache.http.HttpStatus; import org.junit.Test; -import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL; +import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -72,7 +71,7 @@ public class OID4VCTimeNormalizationTest extends OID4VCIssuerEndpointTest { String authCode = getAuthorizationCode(oauth, client, "john", scopeName); org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail); String token = tokenResponse.getAccessToken(); - List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); + List authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails(); String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0); testingClient.server(TEST_REALM_NAME).run(session -> {