[OID4VCI] Simplify OID4VCAuthorizationDetail handling

Signed-off-by: Thomas Diesler <tdiesler@ibm.com>
This commit is contained in:
Thomas Diesler 2026-01-31 19:32:11 +01:00 committed by Marek Posolda
parent bd703eb767
commit 44e7cf2da9
26 changed files with 295 additions and 248 deletions

View file

@ -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";
}

View file

@ -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() {
}

View file

@ -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 <a href="mailto:Forkim.Akwichek@adorsys.com">Forkim Akwichek</a>
*/
public class OID4VCAuthorizationDetailResponse extends OID4VCAuthorizationDetail {
public static final String CREDENTIAL_IDENTIFIERS = "credential_identifiers";
@JsonProperty(CREDENTIAL_IDENTIFIERS)
private List<String> credentialIdentifiers;
public List<String> getCredentialIdentifiers() {
return credentialIdentifiers;
}
public void setCredentialIdentifiers(List<String> 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);
}
}

View file

@ -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<OID4VCAuthorizationDetailResponse> {
public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetailsProcessor<OID4VCAuthorizationDetail> {
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<OID4VCAuthorizationDetailResponse> getSupportedResponseJavaType() {
return OID4VCAuthorizationDetailResponse.class;
public Class<OID4VCAuthorizationDetail> 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<String, SupportedCredentialConfiguration> 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<String> credentialIdentifiers = detail.getCredentialIdentifiers();
List<ClaimsDescription> 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<AuthorizationDetailsJSONRepresentation> previousResponses = clientSessionCtx.getAttribute(AUTHORIZATION_DETAILS_RESPONSE, List.class);
List<OID4VCAuthorizationDetailResponse> oid4vcPreviousResponses = getSupportedAuthorizationDetails(previousResponses);
List<OID4VCAuthorizationDetail> oid4vcPreviousResponses = getSupportedAuthorizationDetails(previousResponses);
List<String> 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<OID4VCAuthorizationDetailResponse> generateAuthorizationDetailsFromCredentialOffer(AuthenticatedClientSessionModel clientSession) {
private List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authorizationDetailsList = new ArrayList<>();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> handleMissingAuthorizationDetails(UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
public List<OID4VCAuthorizationDetail> 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<String>) 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<String>) inDetail.getCustomData().get(CREDENTIAL_IDENTIFIERS));
outDetail.setClaims(parseClaims((List<Map>) inDetail.getCustomData().get(CLAIMS)));
}

View file

@ -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.

View file

@ -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<AuthorizationDetailsJSONRepresentation> tokenAuthDetails = accessToken.getAuthorizationDetails();
AuthorizationDetailsProcessor<OID4VCAuthorizationDetailResponse> oid4vcProcessor = session.getProvider(AuthorizationDetailsProcessor.class, OPENID_CREDENTIAL);
List<OID4VCAuthorizationDetailResponse> oid4vcResponses = oid4vcProcessor.getSupportedAuthorizationDetails(tokenAuthDetails);
AuthorizationDetailsProcessor<OID4VCAuthorizationDetail> oid4vcProcessor = session.getProvider(AuthorizationDetailsProcessor.class, OPENID_CREDENTIAL);
List<OID4VCAuthorizationDetail> 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<OID4VCMapper> 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<String, Object> allClaims, SupportedCredentialConfiguration credentialConfig,
UserModel user, OID4VCAuthorizationDetailResponse authzDetail, String scope, EventBuilder eventBuilder) {
UserModel user, OID4VCAuthorizationDetail authzDetail, String scope, EventBuilder eventBuilder) {
// Protocol mappers from configuration
Map<List<Object>, ClaimsDescription> claimsConfig = credentialConfig.getCredentialMetadata().getClaims()
.stream()
@ -1696,7 +1697,7 @@ public class OID4VCIssuerEndpoint {
}
private List<ClaimsDescription> getClaimsFromAuthzDetails(String scope, UserModel user, OID4VCAuthorizationDetailResponse authzDetail) {
private List<ClaimsDescription> getClaimsFromAuthzDetails(String scope, UserModel user, OID4VCAuthorizationDetail authzDetail) {
List<ClaimsDescription> storedClaims = authzDetail == null ? null : authzDetail.getClaims();
if (storedClaims == null || storedClaims.isEmpty()) {
String username = user.getUsername();

View file

@ -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;
}

View file

@ -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 <a href="mailto:Forkim.Akwichek@adorsys.com">Forkim Akwichek</a>
*/
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
* <p/>
* Identifying Credentials Being Issued Throughout the Issuance Flow
* https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-3.3.4
* <p/>
* The property should not be used in Authorization or AccessToken requests.
*/
@JsonProperty(CREDENTIAL_IDENTIFIERS)
private List<String> credentialIdentifiers;
@JsonProperty(CLAIMS)
private List<ClaimsDescription> claims;
@ -48,6 +61,14 @@ public class OID4VCAuthorizationDetail extends AuthorizationDetailsJSONRepresent
this.credentialConfigurationId = credentialConfigurationId;
}
public List<String> getCredentialIdentifiers() {
return credentialIdentifiers;
}
public void setCredentialIdentifiers(List<String> credentialIdentifiers) {
this.credentialIdentifiers = credentialIdentifiers;
}
public List<ClaimsDescription> 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

View file

@ -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);

View file

@ -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<String> 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<OID4VCAuthorizationDetailResponse> authzResponses = new OID4VCAuthorizationDetailsProcessor(null).getSupportedAuthorizationDetails(responses);
List<OID4VCAuthorizationDetail> authzResponses = new OID4VCAuthorizationDetailsProcessor(null).getSupportedAuthorizationDetails(responses);
Assert.assertEquals(2, authzResponses.size());
assertValidAuthorizationDetailResponse(authzResponses.get(0));

View file

@ -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() {

View file

@ -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 <ADR extends AuthorizationDetailsJSONRepresentation> List<ADR> getAuthorizationDetails(Class<ADR> 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<OID4VCAuthorizationDetail> getOid4vcAuthorizationDetails() {
return getAuthorizationDetails(OID4VCAuthorizationDetail.class);
}
private <ADR extends AuthorizationDetailsJSONRepresentation> List<ADR> getAuthorizationDetails(Class<ADR> 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<OID4VCAuthorizationDetailResponse> 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<List<OID4VCAuthorizationDetailResponse>>() {
}
);
} catch (IOException e) {
throw new RuntimeException("Failed to parse authorization_details from token response", e);
}
}
}

View file

@ -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;

View file

@ -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<OID4VCAuthorizationDetailResponse> authDetailsResponse = JsonSerialization.readValue(
List<OID4VCAuthorizationDetail> 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<String> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = accessToken.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> extractAuthorizationDetails(AccessTokenResponse tokenResponse) {
private List<OID4VCAuthorizationDetail> extractAuthorizationDetails(AccessTokenResponse tokenResponse) {
// First check if already populated in token response
List<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = extractAuthorizationDetails(tokenResponse);
List<OID4VCAuthorizationDetail> authDetailsResponse = extractAuthorizationDetails(tokenResponse);
if (authDetailsResponse != null && !authDetailsResponse.isEmpty()) {
// If authorization_details are present, credential_identifier is required

View file

@ -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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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);

View file

@ -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<OID4VCAuthorizationDetailResponse> authDetails = tokenResponseRef.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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());

View file

@ -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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
assertTrue("authorization_details should NOT be present in the response when not used in PAR request",
authDetailsResponse == null || authDetailsResponse.isEmpty());
}

View file

@ -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<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = accessTokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
assertNotNull("authorization_details should be present in the response", authDetailsResponse);
assertEquals(1, authDetailsResponse.size());

View file

@ -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;

View file

@ -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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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);

View file

@ -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<OID4VCAuthorizationDetailResponse> authDetailsResponse = JsonSerialization.readValue(
List<OID4VCAuthorizationDetail> authDetailsResponse = JsonSerialization.readValue(
JsonSerialization.writeValueAsString(authDetails),
new TypeReference<List<OID4VCAuthorizationDetailResponse>>() {
new TypeReference<List<OID4VCAuthorizationDetail>>() {
}
);
if (!authDetailsResponse.isEmpty() &&
@ -790,14 +790,14 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
}
}
protected List<OID4VCAuthorizationDetailResponse> parseAuthorizationDetails(String responseBody) throws IOException {
protected List<OID4VCAuthorizationDetail> parseAuthorizationDetails(String responseBody) throws IOException {
Map<String, Object> responseMap = JsonSerialization.readValue(responseBody, new TypeReference<Map<String, Object>>() {
});
Object authDetailsObj = responseMap.get("authorization_details");
assertNotNull("authorization_details should be present in the response", authDetailsObj);
return JsonSerialization.readValue(
JsonSerialization.writeValueAsString(authDetailsObj),
new TypeReference<List<OID4VCAuthorizationDetailResponse>>() {
new TypeReference<List<OID4VCAuthorizationDetail>>() {
}
);
}

View file

@ -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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = accessTokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0);
testingClient.server(TEST_REALM_NAME).run(session -> {

View file

@ -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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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<OID4VCAuthorizationDetailResponse> authDetailsResponse = accessTokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> 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);

View file

@ -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;

View file

@ -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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0);
final String clientScopeString = toJsonString(sdJwtTypeCredentialClientScope);

View file

@ -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<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
List<OID4VCAuthorizationDetail> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0);
testingClient.server(TEST_REALM_NAME).run(session -> {