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