Remove AuthorizationDetailsResponse and make AuthorizationDetailsJSONRepresentation as base of RAR processors

closes #45706

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
mposolda
2026-01-23 10:34:42 +01:00
committed by Marek Posolda
parent b247ef12cd
commit e414050524
19 changed files with 171 additions and 194 deletions
@@ -150,7 +150,7 @@ public class AccessToken extends IDToken {
protected String scope;
@JsonProperty(OAuth2Constants.AUTHORIZATION_DETAILS)
protected List<AuthorizationDetailsResponse> authorizationDetails;
protected List<AuthorizationDetailsJSONRepresentation> authorizationDetails;
@JsonIgnore
public Map<String, Access> getResourceAccess() {
@@ -279,11 +279,11 @@ public class AccessToken extends IDToken {
this.scope = scope;
}
public List<AuthorizationDetailsResponse> getAuthorizationDetails() {
public List<AuthorizationDetailsJSONRepresentation> getAuthorizationDetails() {
return authorizationDetails;
}
public void setAuthorizationDetails(List<AuthorizationDetailsResponse> authorizationDetails) {
public void setAuthorizationDetails(List<AuthorizationDetailsJSONRepresentation> authorizationDetails) {
this.authorizationDetails = authorizationDetails;
}
@@ -65,7 +65,7 @@ public class AccessTokenResponse {
protected String scope;
@JsonProperty(OAuth2Constants.AUTHORIZATION_DETAILS)
protected List<AuthorizationDetailsResponse> authorizationDetails;
protected List<AuthorizationDetailsJSONRepresentation> authorizationDetails;
@JsonProperty("error")
protected String error;
@@ -84,11 +84,11 @@ public class AccessTokenResponse {
this.scope = scope;
}
public List<AuthorizationDetailsResponse> getAuthorizationDetails() {
public List<AuthorizationDetailsJSONRepresentation> getAuthorizationDetails() {
return authorizationDetails;
}
public void setAuthorizationDetails(List<AuthorizationDetailsResponse> authorizationDetails) {
public void setAuthorizationDetails(List<AuthorizationDetailsJSONRepresentation> authorizationDetails) {
this.authorizationDetails = authorizationDetails;
}
@@ -22,15 +22,19 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.keycloak.util.AuthorizationDetailsParser;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* The JSON representation of a Rich Authorization Request's "authorization_details" object.
* The "authorization_details" parameter is array of objects and this class represents single entry of that array. It is used as a base
* for "authorization_details" for both requests and responses.
*
* @author <a href="mailto:dgozalob@redhat.com">Daniel Gozalo</a>
* @see {@link <a href="https://datatracker.ietf.org/doc/html/rfc9396#section-2">Request parameter "authorization_details"</a>}
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9396#section-2">Request parameter "authorization_details"</a>
*/
public class AuthorizationDetailsJSONRepresentation implements Serializable {
@@ -126,6 +130,15 @@ public class AuthorizationDetailsJSONRepresentation implements Serializable {
'}';
}
/**
* @param clazz Subtype of {@link AuthorizationDetailsJSONRepresentation}, which will be returned by calling this method
* @return this authorizationDetails content cast to the class specified by clazz parameter as long as parser corresponding to the type returned by {@link #getType}
* is able to parse this authorizationDetails and convert it to that subtype
*/
public <T extends AuthorizationDetailsJSONRepresentation> T asSubtype(Class<T> clazz) {
return AuthorizationDetailsParser.parseToSubtype(this, clazz);
}
public String getScopeNameFromCustomData() {
if (this.getType().equalsIgnoreCase(DYNAMIC_SCOPE_RAR_TYPE) || this.getType().equalsIgnoreCase(STATIC_SCOPE_RAR_TYPE)) {
List<String> accessList = (List<String>) this.customData.get("access");
@@ -1,69 +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.representations;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Generic response object for authorization details processing.
* This class serves as a base for different types of authorization details responses
* from various RAR (Rich Authorization Requests) implementations.
*
* @author <a href="mailto:Forkim.Akwichek@adorsys.com">Forkim Akwichek</a>
*/
public class AuthorizationDetailsResponse extends AuthorizationDetailsJSONRepresentation {
// Map of parsers for specific values of "type" claim of authorizationDetails
private static final Map<String, AuthorizationDetailsResponseParser<?>> PARSERS = new ConcurrentHashMap<>();
/**
* Register new parser for specific type. This can be later used by {@link #asSubtype} method. Parsers are supposed to be
* registered before authorizationDetails are being used by the application and before method {@link #asSubtype} is called for the first time.
* Usually it is supposed to be registered at the startup of the application. If implementing Keycloak provider <em>AuthorizationDetailsProcessor</em>, it might
* be good to register corresponding parser in the <em>AuthorizationDetailsProcessorFactory.init</em> method of your provider
*
* @param type
* @param parser
*/
public static void registerParser(String type, AuthorizationDetailsResponseParser<?> parser) {
PARSERS.put(type, parser);
}
/**
* @param clazz Subtype of {@link AuthorizationDetailsResponse}, which will be returned by calling this method
* @return this authorizationDetails content cast to the class specified by clazz parameter as long as parser corresponding to the type returned by {@link #getType}
* is able to parse this authorizationDetails and convert it to that subtype
*/
public <T extends AuthorizationDetailsResponse> T asSubtype(Class<T> clazz) {
AuthorizationDetailsResponseParser<T> parser = (AuthorizationDetailsResponseParser<T>) PARSERS.get(getType());
if (parser == null) {
throw new IllegalArgumentException("Unsupported to parse response of type '" + getType() + "' to the type '" + clazz +
"'. Please make sure that corresponding parser is registered.");
}
return parser.asSubtype(this);
}
/**
* Parser, which is able to create specific subtype of {@link AuthorizationDetailsResponse} in performant way
*/
public interface AuthorizationDetailsResponseParser<T extends AuthorizationDetailsResponse> {
T asSubtype(AuthorizationDetailsResponse response);
}
}
@@ -0,0 +1,51 @@
package org.keycloak.util;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
/**
* Parser, which is able to create specific subtype of {@link AuthorizationDetailsJSONRepresentation} in performant way
*/
public interface AuthorizationDetailsParser {
<T extends AuthorizationDetailsJSONRepresentation> T asSubtype(AuthorizationDetailsJSONRepresentation authzDetail, Class<T> clazz);
Map<String, AuthorizationDetailsParser> PARSERS = new ConcurrentHashMap<>();
/**
* Register new parser for specific type. This can be later used by {@link #asSubtype} method. Parsers are supposed to be
* registered before authorizationDetails are being used by the application and before method {@link #asSubtype} is called for the first time.
* Usually it is supposed to be registered at the startup of the application. If implementing Keycloak provider <em>AuthorizationDetailsProcessor</em>, it might
* be good to register corresponding parser in the <em>AuthorizationDetailsProcessorFactory.init</em> method of your provider
*
* @param type Type as used in the "type" claim of "authorization_details" object entry
* @param parser Parser for this type
*/
static void registerParser(String type, AuthorizationDetailsParser parser) {
PARSERS.put(type, parser);
}
/**
* Method is not supposed to be called directly. Rather please make sure to use {@link #registerParser(String, AuthorizationDetailsParser)} and
* then use {@link #asSubtype(AuthorizationDetailsJSONRepresentation, Class)} to call directly from the application
*
* @param authzDetail Authorization detail object to cast
* @param clazz Subtype of {@link AuthorizationDetailsJSONRepresentation}, which will be returned by calling this method
* @return given authzDetail passed in <em>authzDetail</em> parameter cast to the class specified by clazz parameter as long as parser corresponding to the type
* returned by {@link AuthorizationDetailsJSONRepresentation#getType} is able to parse this authorizationDetails and convert it to that subtype
*/
static <T extends AuthorizationDetailsJSONRepresentation> T parseToSubtype(AuthorizationDetailsJSONRepresentation authzDetail, Class<T> clazz) {
if (authzDetail.getType() == null) {
throw new IllegalArgumentException("Used authzDetail entry does not have 'type' set. The used authzDetail entry was: " + authzDetail);
}
AuthorizationDetailsParser parser = PARSERS.get(authzDetail.getType());
if (parser == null) {
throw new IllegalArgumentException("Unsupported to parse response of type '" + authzDetail.getType() + "' to the class '" + clazz +
"'. Please make sure that corresponding parser is registered.");
}
return parser.asSubtype(authzDetail, clazz);
}
}
@@ -22,7 +22,6 @@ import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.Provider;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.representations.AuthorizationDetailsResponse;
/**
* Provider interface for processing authorization_details parameter in OAuth2/OIDC authorization and token requests.
@@ -33,7 +32,7 @@ import org.keycloak.representations.AuthorizationDetailsResponse;
*
* @author <a href="mailto:Forkim.Akwichek@adorsys.com">Forkim Akwichek</a>
*/
public interface AuthorizationDetailsProcessor<ADR extends AuthorizationDetailsResponse> extends Provider {
public interface AuthorizationDetailsProcessor<ADR extends AuthorizationDetailsJSONRepresentation> extends Provider {
/**
* Checks if this processor should be regarded as supported in the running context.
@@ -47,7 +46,7 @@ public interface AuthorizationDetailsProcessor<ADR extends AuthorizationDetailsR
String getSupportedType();
/**
* @return supported Java type of {@link AuthorizationDetailsResponse} subclass, which this processor can create in the token response
* @return supported Java type of {@link AuthorizationDetailsJSONRepresentation} subclass, which this processor can create in the token response
*/
Class<ADR> getSupportedResponseJavaType();
@@ -94,7 +93,7 @@ public interface AuthorizationDetailsProcessor<ADR extends AuthorizationDetailsR
* @param authzDetailsResponse all the authorizationDetails. May contain also authorizationDetails entries, with different "type" than the type understandable by this processor
* @return sublist of the list provided by "authDetailsResponse" parameter, which will contain just the authorizationDetails of the corresponding type of this processor.
*/
default List<ADR> getSupportedAuthorizationDetails(List<AuthorizationDetailsResponse> authzDetailsResponse) {
default List<ADR> getSupportedAuthorizationDetails(List<AuthorizationDetailsJSONRepresentation> authzDetailsResponse) {
if (authzDetailsResponse == null) {
return null;
}
@@ -18,8 +18,7 @@ package org.keycloak.protocol.oid4vc.issuance;
import java.util.List;
import org.keycloak.protocol.oid4vc.model.ClaimsDescription;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -29,29 +28,13 @@ import com.fasterxml.jackson.annotation.JsonProperty;
*
* @author <a href="mailto:Forkim.Akwichek@adorsys.com">Forkim Akwichek</a>
*/
public class OID4VCAuthorizationDetailResponse extends AuthorizationDetailsResponse {
public class OID4VCAuthorizationDetailResponse extends OID4VCAuthorizationDetail {
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;
@JsonProperty(CREDENTIAL_IDENTIFIERS)
private List<String> credentialIdentifiers;
@JsonProperty(CLAIMS)
private List<ClaimsDescription> claims;
public String getCredentialConfigurationId() {
return credentialConfigurationId;
}
public void setCredentialConfigurationId(String credentialConfigurationId) {
this.credentialConfigurationId = credentialConfigurationId;
}
public List<String> getCredentialIdentifiers() {
return credentialIdentifiers;
}
@@ -60,22 +43,14 @@ public class OID4VCAuthorizationDetailResponse extends AuthorizationDetailsRespo
this.credentialIdentifiers = credentialIdentifiers;
}
public List<ClaimsDescription> getClaims() {
return claims;
}
public void setClaims(List<ClaimsDescription> claims) {
this.claims = claims;
}
@Override
public String toString() {
return "OID4VCAuthorizationDetailsResponse{" +
"type='" + getType() + '\'' +
return "OID4VCAuthorizationDetailsResponse {" +
" type='" + getType() + '\'' +
", locations='" + getLocations() + '\'' +
", credentialConfigurationId='" + credentialConfigurationId + '\'' +
", credentialConfigurationId='" + getCredentialConfigurationId() + '\'' +
", credentialIdentifiers=" + credentialIdentifiers +
", claims=" + claims +
", claims=" + getClaims() +
'}';
}
}
@@ -43,7 +43,7 @@ import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
import org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessor;
import org.keycloak.protocol.oidc.rar.InvalidAuthorizationDetailsException;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.util.AuthorizationDetailsParser;
import org.keycloak.util.JsonSerialization;
import org.jboss.logging.Logger;
@@ -81,7 +81,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails
@Override
public OID4VCAuthorizationDetailResponse process(UserSessionModel userSession, ClientSessionContext clientSessionCtx, AuthorizationDetailsJSONRepresentation authzDetail) {
OID4VCAuthorizationDetail detail = convertRequestType(authzDetail);
OID4VCAuthorizationDetail detail = authzDetail.asSubtype(OID4VCAuthorizationDetail.class);
Map<String, SupportedCredentialConfiguration> supportedCredentials = OID4VCIssuerWellKnownProvider.getSupportedCredentials(session);
// Retrieve authorization servers and issuer identifier for locations check
@@ -264,7 +264,7 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails
String credentialConfigurationId = detail.getCredentialConfigurationId();
// Try to reuse identifier from authorizationDetailsResponse in client session context
List<AuthorizationDetailsResponse> previousResponses = clientSessionCtx.getAttribute(AUTHORIZATION_DETAILS_RESPONSE, List.class);
List<AuthorizationDetailsJSONRepresentation> previousResponses = clientSessionCtx.getAttribute(AUTHORIZATION_DETAILS_RESPONSE, List.class);
List<OID4VCAuthorizationDetailResponse> oid4vcPreviousResponses = getSupportedAuthorizationDetails(previousResponses);
List<String> credentialIdentifiers = oid4vcPreviousResponses != null && !oid4vcPreviousResponses.isEmpty()
? oid4vcPreviousResponses.get(0).getCredentialIdentifiers()
@@ -400,49 +400,52 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails
}
public static class OID4VCAuthorizationDetailsParser implements AuthorizationDetailsResponse.AuthorizationDetailsResponseParser<OID4VCAuthorizationDetailResponse> {
public static class OID4VCAuthorizationDetailsParser implements AuthorizationDetailsParser {
@Override
public OID4VCAuthorizationDetailResponse asSubtype(AuthorizationDetailsResponse response) {
if (response instanceof OID4VCAuthorizationDetailResponse) {
return (OID4VCAuthorizationDetailResponse) response;
public <T extends AuthorizationDetailsJSONRepresentation> T asSubtype(AuthorizationDetailsJSONRepresentation authzDetail, Class<T> clazz) {
if (OID4VCAuthorizationDetail.class.equals(clazz)) {
if (authzDetail instanceof OID4VCAuthorizationDetail) {
return clazz.cast(authzDetail);
} else {
OID4VCAuthorizationDetail detail = new OID4VCAuthorizationDetail();
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 {
OID4VCAuthorizationDetailResponse detail = new OID4VCAuthorizationDetailResponse();
detail.setType(response.getType());
detail.setLocations(response.getLocations());
detail.setCredentialConfigurationId((String) response.getCustomData().get(CREDENTIAL_CONFIGURATION_ID));
detail.setClaims(parseClaims((List<Map>) response.getCustomData().get(CLAIMS)));
detail.setCredentialIdentifiers((List<String>) response.getCustomData().get(CREDENTIAL_IDENTIFIERS));
return detail;
throw new IllegalArgumentException("Authorization details '" + authzDetail + "' is unsupported to be parsed to '" + clazz + "'.");
}
}
}
private void fillFields(AuthorizationDetailsJSONRepresentation inDetail, OID4VCAuthorizationDetail outDetail) {
outDetail.setType(inDetail.getType());
outDetail.setLocations(inDetail.getLocations());
outDetail.setCredentialConfigurationId((String) inDetail.getCustomData().get(CREDENTIAL_CONFIGURATION_ID));
outDetail.setClaims(parseClaims((List<Map>) inDetail.getCustomData().get(CLAIMS)));
}
private static OID4VCAuthorizationDetail convertRequestType(AuthorizationDetailsJSONRepresentation request) {
if (request instanceof OID4VCAuthorizationDetail) {
return (OID4VCAuthorizationDetail) request;
} else {
OID4VCAuthorizationDetail detail = new OID4VCAuthorizationDetail();
detail.setType(request.getType());
detail.setLocations(request.getLocations());
detail.setCredentialConfigurationId((String) request.getCustomData().get(CREDENTIAL_CONFIGURATION_ID));
detail.setClaims(parseClaims((List<Map>) request.getCustomData().get(CLAIMS)));
return detail;
private static List<ClaimsDescription> parseClaims(List<Map> genericClaims) {
if (genericClaims == null) {
return null;
}
return genericClaims.stream()
.map(claim -> {
List<Object> path = (List<Object>) claim.get(PATH);
Boolean mandatory = (Boolean) claim.get(MANDATORY);
return new ClaimsDescription(path, mandatory);
})
.toList();
}
}
private static List<ClaimsDescription> parseClaims(List<Map> genericClaims) {
if (genericClaims == null) {
return null;
}
return genericClaims.stream()
.map(claim -> {
List<Object> path = (List<Object>) claim.get(PATH);
Boolean mandatory = (Boolean) claim.get(MANDATORY);
return new ClaimsDescription(path, mandatory);
})
.toList();
}
}
@@ -20,7 +20,7 @@ import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oid4vc.OID4VCEnvironmentProviderFactory;
import org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessorFactory;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.util.AuthorizationDetailsParser;
import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL;
@@ -41,7 +41,7 @@ public class OID4VCAuthorizationDetailsProcessorFactory implements Authorization
@Override
public void init(Config.Scope config) {
AuthorizationDetailsResponse.registerParser(OPENID_CREDENTIAL, new OID4VCAuthorizationDetailsProcessor.OID4VCAuthorizationDetailsParser());
AuthorizationDetailsParser.registerParser(OPENID_CREDENTIAL, new OID4VCAuthorizationDetailsProcessor.OID4VCAuthorizationDetailsParser());
}
@Override
@@ -112,7 +112,7 @@ import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantType;
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
import org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessor;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.representations.dpop.DPoP;
import org.keycloak.saml.processing.api.util.DeflateUtil;
import org.keycloak.services.CorsErrorResponseException;
@@ -298,8 +298,8 @@ public class OID4VCIssuerEndpoint {
* the OpenId4VCI nonce-endpoint
*
* @return a short-lived c_nonce value that must be presented in key-bound proofs at the credential endpoint.
* @see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-16.html#name-nonce-endpoint
* @see https://datatracker.ietf.org/doc/html/draft-demarco-nonce-endpoint#name-nonce-response
* @see <a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-16.html#name-nonce-endpoint">Nonce endpoint</a>
* @see <a href="https://datatracker.ietf.org/doc/html/draft-demarco-nonce-endpoint#name-nonce-response">Nonce response</a>
*/
@POST
@Produces({MediaType.APPLICATION_JSON})
@@ -391,7 +391,7 @@ public class OID4VCIssuerEndpoint {
* @param type The response type, which can be 'uri' or 'qr-code'
* @param width The width of the QR code image
* @param height The height of the QR code image
* @see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-endpoint
* @see <a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-endpoint">Credential offer endpoint</a>
*/
@GET
@Produces({MediaType.APPLICATION_JSON, RESPONSE_TYPE_IMG_PNG})
@@ -935,7 +935,7 @@ public class OID4VCIssuerEndpoint {
}
private OID4VCAuthorizationDetailResponse getAuthorizationDetailFromToken(AccessToken accessToken) {
List<AuthorizationDetailsResponse> tokenAuthDetails = accessToken.getAuthorizationDetails();
List<AuthorizationDetailsJSONRepresentation> tokenAuthDetails = accessToken.getAuthorizationDetails();
AuthorizationDetailsProcessor<OID4VCAuthorizationDetailResponse> oid4vcProcessor = session.getProvider(AuthorizationDetailsProcessor.class, OPENID_CREDENTIAL);
List<OID4VCAuthorizationDetailResponse> oid4vcResponses = oid4vcProcessor.getSupportedAuthorizationDetails(tokenAuthDetails);
return oid4vcResponses == null || oid4vcResponses.isEmpty() ? null : oid4vcResponses.get(0);
@@ -29,10 +29,14 @@ import com.fasterxml.jackson.annotation.JsonProperty;
*/
public class OID4VCAuthorizationDetail extends AuthorizationDetailsJSONRepresentation {
@JsonProperty("credential_configuration_id")
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;
@JsonProperty("claims")
@JsonProperty(CLAIMS)
private List<ClaimsDescription> claims;
public String getCredentialConfigurationId() {
@@ -53,8 +57,8 @@ public class OID4VCAuthorizationDetail extends AuthorizationDetailsJSONRepresent
@Override
public String toString() {
return "OID4VCAuthorizationDetailsResponse{" +
"type='" + getType() + '\'' +
return "OID4VCAuthorizationDetail {" +
" type='" + getType() + '\'' +
", locations='" + getLocations() + '\'' +
", credentialConfigurationId='" + credentialConfigurationId + '\'' +
", claims=" + claims +
@@ -104,7 +104,6 @@ import org.keycloak.rar.AuthorizationRequestContext;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.LogoutToken;
@@ -355,7 +354,7 @@ public class TokenManager {
validation.userSession, validation.clientSessionCtx).offlineToken( TokenUtil.TOKEN_TYPE_OFFLINE.equals(refreshToken.getType())).accessToken(validation.newToken);
// Copy authorization_details from refresh token to new access token and to acessTokenResponse (if present)
List<AuthorizationDetailsResponse> authorizationDetails = refreshToken.getAuthorizationDetails();
List<AuthorizationDetailsJSONRepresentation> authorizationDetails = refreshToken.getAuthorizationDetails();
if (authorizationDetails != null) {
validation.newToken.setAuthorizationDetails(authorizationDetails);
validation.clientSessionCtx.setAttribute(AUTHORIZATION_DETAILS_RESPONSE, authorizationDetails);
@@ -1404,7 +1403,7 @@ public class TokenManager {
res.setScope(responseScope);
event.detail(Details.SCOPE, responseScope);
List<AuthorizationDetailsResponse> authDetailsResponse = clientSessionCtx.getAttribute(AUTHORIZATION_DETAILS_RESPONSE, List.class);
List<AuthorizationDetailsJSONRepresentation> authDetailsResponse = clientSessionCtx.getAttribute(AUTHORIZATION_DETAILS_RESPONSE, List.class);
if (authDetailsResponse != null && !authDetailsResponse.isEmpty()) {
res.setAuthorizationDetails(authDetailsResponse);
}
@@ -39,7 +39,7 @@ import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.OAuth2Code;
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.protocol.oidc.utils.PkceUtils;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.TokenRequestContext;
@@ -215,7 +215,7 @@ public class AuthorizationCodeGrantType extends OAuth2GrantTypeBase {
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, codeData.getNonce());
// Process authorization_details using provider discovery (if present in request)
List<AuthorizationDetailsResponse> authorizationDetailsResponse = null;
List<AuthorizationDetailsJSONRepresentation> authorizationDetailsResponse = null;
if (formParams.getFirst(AUTHORIZATION_DETAILS) != null) {
authorizationDetailsResponse = processAuthorizationDetails(userSession, clientSessionCtx);
if (authorizationDetailsResponse != null && !authorizationDetailsResponse.isEmpty()) {
@@ -252,7 +252,7 @@ public class AuthorizationCodeGrantType extends OAuth2GrantTypeBase {
return createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true, s -> {
// Add authorization_details to the access token and refresh token if they were processed
List<AuthorizationDetailsResponse> authDetailsResponse = clientSessionCtx.getAttribute(AUTHORIZATION_DETAILS_RESPONSE, List.class);
List<AuthorizationDetailsJSONRepresentation> authDetailsResponse = clientSessionCtx.getAttribute(AUTHORIZATION_DETAILS_RESPONSE, List.class);
if (authDetailsResponse != null && !authDetailsResponse.isEmpty()) {
s.getAccessToken().setAuthorizationDetails(authDetailsResponse);
// Also add to refresh token if one is generated
@@ -56,7 +56,7 @@ import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.rar.AuthorizationRequestContext;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
@@ -291,7 +291,7 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
* @param authorizationDetailsResponse the processed authorization details response
*/
protected void afterAuthorizationDetailsProcessed(UserSessionModel userSession, ClientSessionContext clientSessionCtx,
List<AuthorizationDetailsResponse> authorizationDetailsResponse) {
List<AuthorizationDetailsJSONRepresentation> authorizationDetailsResponse) {
// Default: do nothing
// Subclasses or processors can override/extend this to perform post-processing
}
@@ -304,7 +304,7 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
* @param clientSessionCtx the client session context
* @return the authorization details response if processing was successful, null otherwise
*/
protected List<AuthorizationDetailsResponse> processAuthorizationDetails(UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
protected List<AuthorizationDetailsJSONRepresentation> processAuthorizationDetails(UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
String authorizationDetailsParam = formParams.getFirst(AUTHORIZATION_DETAILS);
if (authorizationDetailsParam != null) {
try {
@@ -328,7 +328,7 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
* @param clientSessionCtx the client session context
* @return the authorization details response if generation was successful, null otherwise
*/
protected List<AuthorizationDetailsResponse> handleMissingAuthorizationDetails(UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
protected List<AuthorizationDetailsJSONRepresentation> handleMissingAuthorizationDetails(UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
try {
return new AuthorizationDetailsProcessorManager().handleMissingAuthorizationDetails(session, userSession, clientSessionCtx);
} catch (RuntimeException e) {
@@ -348,7 +348,7 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
* @param clientSessionCtx the client session context
* @return the authorization details response if processing was successful, null otherwise
*/
protected List<AuthorizationDetailsResponse> processStoredAuthorizationDetails(UserSessionModel userSession, ClientSessionContext clientSessionCtx) throws CorsErrorResponseException {
protected List<AuthorizationDetailsJSONRepresentation> processStoredAuthorizationDetails(UserSessionModel userSession, ClientSessionContext clientSessionCtx) throws CorsErrorResponseException {
// Check if authorization_details was stored during authorization request (e.g., from PAR)
String storedAuthDetails = clientSessionCtx.getClientSession().getNote(AUTHORIZATION_DETAILS);
if (storedAuthDetails != null) {
@@ -39,7 +39,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager.AccessTokenResponseBuilder;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.MediaType;
@@ -139,7 +139,7 @@ public class PreAuthorizedCodeGrantType extends OAuth2GrantTypeBase {
.user(userModel);
// Process authorization_details using provider discovery
List<AuthorizationDetailsResponse> authorizationDetailsResponses = processAuthorizationDetails(userSession, sessionContext);
List<AuthorizationDetailsJSONRepresentation> authorizationDetailsResponses = processAuthorizationDetails(userSession, sessionContext);
LOGGER.debugf("Initial authorization_details processing result: %s", authorizationDetailsResponses);
// If no authorization_details were processed from the request, try to generate them from credential offer
@@ -12,7 +12,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -22,14 +21,14 @@ public class AuthorizationDetailsProcessorManager {
private static final Logger logger = Logger.getLogger(AuthorizationDetailsProcessorManager.class);
public List<AuthorizationDetailsResponse> processAuthorizationDetails(KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx,
public List<AuthorizationDetailsJSONRepresentation> processAuthorizationDetails(KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx,
String authorizationDetailsParam) throws InvalidAuthorizationDetailsException {
return processAuthzDetailsImpl(session, authorizationDetailsParam,
(processor, authzDetail) -> processor.process(userSession, clientSessionCtx, authzDetail));
}
public List<AuthorizationDetailsResponse> processStoredAuthorizationDetails(KeycloakSession session, UserSessionModel userSession,
public List<AuthorizationDetailsJSONRepresentation> processStoredAuthorizationDetails(KeycloakSession session, UserSessionModel userSession,
ClientSessionContext clientSessionCtx,
String authorizationDetailsParam) throws InvalidAuthorizationDetailsException {
return processAuthzDetailsImpl(session, authorizationDetailsParam,
@@ -39,8 +38,8 @@ public class AuthorizationDetailsProcessorManager {
}
public List<AuthorizationDetailsResponse> handleMissingAuthorizationDetails(KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
List<AuthorizationDetailsResponse> allAuthzDetails = new ArrayList<>();
public List<AuthorizationDetailsJSONRepresentation> handleMissingAuthorizationDetails(KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
List<AuthorizationDetailsJSONRepresentation> allAuthzDetails = new ArrayList<>();
session.getKeycloakSessionFactory()
.getProviderFactoriesStream(AuthorizationDetailsProcessor.class)
.sorted((f1, f2) -> f2.order() - f1.order())
@@ -52,13 +51,13 @@ public class AuthorizationDetailsProcessorManager {
}
private List<AuthorizationDetailsResponse> processAuthzDetailsImpl(KeycloakSession session, String authorizationDetailsParam,
BiFunction<AuthorizationDetailsProcessor<?>, AuthorizationDetailsJSONRepresentation, AuthorizationDetailsResponse> function) throws InvalidAuthorizationDetailsException {
private List<AuthorizationDetailsJSONRepresentation> processAuthzDetailsImpl(KeycloakSession session, String authorizationDetailsParam,
BiFunction<AuthorizationDetailsProcessor<?>, AuthorizationDetailsJSONRepresentation, AuthorizationDetailsJSONRepresentation> function) throws InvalidAuthorizationDetailsException {
if (authorizationDetailsParam == null) {
return null;
}
List<AuthorizationDetailsResponse> authzResponses = new ArrayList<>();
List<AuthorizationDetailsJSONRepresentation> authzResponses = new ArrayList<>();
List<AuthorizationDetailsJSONRepresentation> authzDetails = parseAuthorizationDetails(authorizationDetailsParam);
@@ -76,7 +75,7 @@ public class AuthorizationDetailsProcessorManager {
throw new InvalidAuthorizationDetailsException(errorDetails);
}
function.apply(processor, authzDetail);
AuthorizationDetailsResponse response = function.apply(processor, authzDetail);
AuthorizationDetailsJSONRepresentation response = function.apply(processor, authzDetail);
if (response != null) {
authzResponses.add(response);
} else {
@@ -22,7 +22,8 @@ import java.util.List;
import org.keycloak.protocol.oid4vc.model.ClaimsDescription;
import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.util.AuthorizationDetailsParser;
import org.keycloak.util.JsonSerialization;
import org.junit.Assert;
@@ -53,7 +54,7 @@ public class OID4VCAuthorizationDetailsProcessorTest {
@BeforeClass
public static void beforeClass() {
AuthorizationDetailsResponse.registerParser(OPENID_CREDENTIAL, new OID4VCAuthorizationDetailsProcessor.OID4VCAuthorizationDetailsParser());
AuthorizationDetailsParser.registerParser(OPENID_CREDENTIAL, new OID4VCAuthorizationDetailsProcessor.OID4VCAuthorizationDetailsParser());
}
/**
@@ -505,7 +506,8 @@ public class OID4VCAuthorizationDetailsProcessorTest {
OID4VCAuthorizationDetail invalidDetail1 = createInvalidTypeAuthorizationDetail();
List<AuthorizationDetailsResponse> responses = List.of(
// Convert to the "generic" types to be able to test parser
List<AuthorizationDetailsJSONRepresentation> responses = List.of(
convertToResponseType(validDetail1),
convertToResponseType(validDetail2),
convertToResponseType(invalidDetail1)
@@ -519,8 +521,9 @@ public class OID4VCAuthorizationDetailsProcessorTest {
assertValidClaims(authzResponses.get(1).getClaims());
}
private AuthorizationDetailsResponse convertToResponseType(Object oid4vcDetails) throws IOException {
return JsonSerialization.readValue(JsonSerialization.writeValueAsString(oid4vcDetails), AuthorizationDetailsResponse.class);
private AuthorizationDetailsJSONRepresentation convertToResponseType(OID4VCAuthorizationDetail oid4vcDetails) throws IOException {
return JsonSerialization.readValue(JsonSerialization.writeValueAsString(oid4vcDetails), AuthorizationDetailsJSONRepresentation.class);
}
}
@@ -7,7 +7,7 @@ import java.util.List;
import java.util.Map;
import org.keycloak.OAuth2Constants;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.util.JsonSerialization;
import org.apache.http.client.methods.CloseableHttpResponse;
@@ -23,7 +23,7 @@ public class AccessTokenResponse extends AbstractHttpResponse {
private String refreshToken;
private String scope;
private String sessionState;
private List<AuthorizationDetailsResponse> authorizationDetails;
private List<AuthorizationDetailsJSONRepresentation> authorizationDetails;
private Map<String, Object> otherClaims;
@@ -68,7 +68,7 @@ public class AccessTokenResponse extends AbstractHttpResponse {
break;
case OAuth2Constants.AUTHORIZATION_DETAILS:
var valJson = JsonSerialization.valueAsString(entry.getValue());
var arr = JsonSerialization.valueFromString(valJson, AuthorizationDetailsResponse[].class);
var arr = JsonSerialization.valueFromString(valJson, AuthorizationDetailsJSONRepresentation[].class);
authorizationDetails = Arrays.asList(arr);
break;
default:
@@ -118,11 +118,11 @@ public class AccessTokenResponse extends AbstractHttpResponse {
return otherClaims;
}
public List<AuthorizationDetailsResponse> getAuthorizationDetails() {
public List<AuthorizationDetailsJSONRepresentation> getAuthorizationDetails() {
return authorizationDetails;
}
public <ADR extends AuthorizationDetailsResponse> List<ADR> getAuthorizationDetails(Class<ADR> clazz) {
public <ADR extends AuthorizationDetailsJSONRepresentation> List<ADR> getAuthorizationDetails(Class<ADR> clazz) {
if (getAuthorizationDetails() == null) {
return null;
} else {
@@ -85,7 +85,6 @@ import org.keycloak.protocol.oid4vc.model.ProofTypesSupported;
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AuthorizationDetailsResponse;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.ComponentExportRepresentation;
@@ -100,6 +99,7 @@ import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testsuite.util.oauth.AccessTokenRequest;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.testsuite.util.oauth.OAuthClient;
import org.keycloak.util.AuthorizationDetailsParser;
import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -142,7 +142,7 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
@BeforeClass
public static void beforeClass() {
AuthorizationDetailsResponse.registerParser(OPENID_CREDENTIAL, new OID4VCAuthorizationDetailsProcessor.OID4VCAuthorizationDetailsParser());
AuthorizationDetailsParser.registerParser(OPENID_CREDENTIAL, new OID4VCAuthorizationDetailsProcessor.OID4VCAuthorizationDetailsParser());
}
protected static CredentialSubject getCredentialSubject(Map<String, Object> claims) {