From 416a6017c22f882d9b0ee27edf5d7932797d47d2 Mon Sep 17 00:00:00 2001 From: mposolda Date: Fri, 16 Jan 2026 21:39:02 +0100 Subject: [PATCH] Make authorizationDetails processing more generic and not tightly coupled to OID4VCI. Fixes closes #44961 Signed-off-by: mposolda --- .../keycloak/representations/AccessToken.java | 13 + .../representations/AccessTokenResponse.java | 14 ++ ...uthorizationDetailsJSONRepresentation.java | 4 +- .../AuthorizationDetailsResponse.java | 55 +++++ .../representations/RefreshToken.java | 10 +- integration/client-cli/admin-cli/pom.xml | 2 + .../main/java/org/keycloak/events/Errors.java | 3 + .../rar/AuthorizationDetailsProcessor.java | 53 ++++- .../AuthorizationDetailsProcessorFactory.java | 10 +- .../rar/AuthorizationDetailsProcessorSpi.java | 2 +- .../rar/AuthorizationDetailsResponse.java | 47 ---- .../InvalidAuthorizationDetailsException.java | 15 ++ ...=> OID4VCAuthorizationDetailResponse.java} | 46 ++-- .../OID4VCAuthorizationDetailsProcessor.java | 223 ++++++++++-------- ...CAuthorizationDetailsProcessorFactory.java | 17 +- .../oid4vc/issuance/OID4VCIssuerEndpoint.java | 92 +++----- .../CredentialOfferStorage.java | 8 +- .../oid4vc/model/ClaimsDescription.java | 7 +- ...il.java => OID4VCAuthorizationDetail.java} | 54 +---- .../protocol/oidc/OIDCWellKnownProvider.java | 10 +- .../keycloak/protocol/oidc/TokenManager.java | 25 +- .../grants/AuthorizationCodeGrantType.java | 24 +- .../oidc/grants/OAuth2GrantTypeBase.java | 65 ++--- .../grants/PreAuthorizedCodeGrantType.java | 14 +- .../AuthorizationDetailsProcessorManager.java | 105 +++++++++ ...D4VCAuthorizationDetailsProcessorTest.java | 128 +++++++--- .../util/oauth/AccessTokenRequest.java | 4 +- .../util/oauth/AccessTokenResponse.java | 18 +- .../OID4VCICredentialOfferMatrixTest.java | 22 +- .../OID4VCAuthorizationCodeFlowTestBase.java | 154 +++++++++--- ...ID4VCAuthorizationCodeFlowWithPARTest.java | 22 +- ...ID4VCAuthorizationDetailsFlowTestBase.java | 116 ++++----- .../signing/OID4VCIssuerEndpointTest.java | 6 +- .../oid4vc/issuance/signing/OID4VCTest.java | 13 +- 34 files changed, 825 insertions(+), 576 deletions(-) create mode 100644 core/src/main/java/org/keycloak/representations/AuthorizationDetailsResponse.java delete mode 100644 server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationDetailsResponse.java create mode 100644 server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/InvalidAuthorizationDetailsException.java rename services/src/main/java/org/keycloak/protocol/oid4vc/issuance/{OID4VCAuthorizationDetailsResponse.java => OID4VCAuthorizationDetailResponse.java} (67%) rename services/src/main/java/org/keycloak/protocol/oid4vc/model/{AuthorizationDetail.java => OID4VCAuthorizationDetail.java} (61%) create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationDetailsProcessorManager.java diff --git a/core/src/main/java/org/keycloak/representations/AccessToken.java b/core/src/main/java/org/keycloak/representations/AccessToken.java index 6abe58c9245..1f019d7b7ba 100755 --- a/core/src/main/java/org/keycloak/representations/AccessToken.java +++ b/core/src/main/java/org/keycloak/representations/AccessToken.java @@ -22,9 +22,11 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import org.keycloak.OAuth2Constants; import org.keycloak.TokenCategory; import org.keycloak.representations.idm.authorization.Permission; @@ -147,6 +149,9 @@ public class AccessToken extends IDToken { @JsonProperty("scope") protected String scope; + @JsonProperty(OAuth2Constants.AUTHORIZATION_DETAILS) + protected List authorizationDetails; + @JsonIgnore public Map getResourceAccess() { return resourceAccess == null ? Collections.emptyMap() : resourceAccess; @@ -274,6 +279,14 @@ public class AccessToken extends IDToken { this.scope = scope; } + public List getAuthorizationDetails() { + return authorizationDetails; + } + + public void setAuthorizationDetails(List authorizationDetails) { + this.authorizationDetails = authorizationDetails; + } + @Override public TokenCategory getCategory() { return TokenCategory.ACCESS; diff --git a/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java b/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java index 49fac581fe2..6f1714d242c 100755 --- a/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java +++ b/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java @@ -18,8 +18,11 @@ package org.keycloak.representations; import java.util.HashMap; +import java.util.List; import java.util.Map; +import org.keycloak.OAuth2Constants; + import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonProperty; @@ -61,6 +64,9 @@ public class AccessTokenResponse { @JsonProperty("scope") protected String scope; + @JsonProperty(OAuth2Constants.AUTHORIZATION_DETAILS) + protected List authorizationDetails; + @JsonProperty("error") protected String error; @@ -78,6 +84,14 @@ public class AccessTokenResponse { this.scope = scope; } + public List getAuthorizationDetails() { + return authorizationDetails; + } + + public void setAuthorizationDetails(List authorizationDetails) { + this.authorizationDetails = authorizationDetails; + } + public String getToken() { return token; } diff --git a/core/src/main/java/org/keycloak/representations/AuthorizationDetailsJSONRepresentation.java b/core/src/main/java/org/keycloak/representations/AuthorizationDetailsJSONRepresentation.java index 1b69fdc5883..35862a0b840 100644 --- a/core/src/main/java/org/keycloak/representations/AuthorizationDetailsJSONRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/AuthorizationDetailsJSONRepresentation.java @@ -30,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; * The JSON representation of a Rich Authorization Request's "authorization_details" object. * * @author Daniel Gozalo - * @see {@link Request parameter "authorization_details"} + * @see {@link Request parameter "authorization_details"} */ public class AuthorizationDetailsJSONRepresentation implements Serializable { @@ -156,4 +156,6 @@ public class AuthorizationDetailsJSONRepresentation implements Serializable { public int hashCode() { return Objects.hash(type, locations, actions, datatypes, identifier, privileges, customData); } + + } diff --git a/core/src/main/java/org/keycloak/representations/AuthorizationDetailsResponse.java b/core/src/main/java/org/keycloak/representations/AuthorizationDetailsResponse.java new file mode 100644 index 00000000000..78688c67637 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/AuthorizationDetailsResponse.java @@ -0,0 +1,55 @@ +/* + * 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.HashMap; +import java.util.Map; + +/** + * 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 Forkim Akwichek + */ +public class AuthorizationDetailsResponse extends AuthorizationDetailsJSONRepresentation { + + // Map of parsers for specific values of "type" claim of authorizationDetails + private static final Map> PARSERS = new HashMap<>(); + + public static void registerParser(String type, AuthorizationDetailsResponseParser parser) { + PARSERS.put(type, parser); + } + + public T asSubtype(Class clazz) { + AuthorizationDetailsResponseParser parser = (AuthorizationDetailsResponseParser) 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 asSubtype(AuthorizationDetailsResponse response); + + } +} diff --git a/core/src/main/java/org/keycloak/representations/RefreshToken.java b/core/src/main/java/org/keycloak/representations/RefreshToken.java index d73c5675679..55758986964 100755 --- a/core/src/main/java/org/keycloak/representations/RefreshToken.java +++ b/core/src/main/java/org/keycloak/representations/RefreshToken.java @@ -44,6 +44,7 @@ public class RefreshToken extends AccessToken { this.nonce = token.nonce; this.audience = new String[] { token.issuer }; this.scope = token.scope; + this.authorizationDetails = token.authorizationDetails; } /** @@ -54,14 +55,7 @@ public class RefreshToken extends AccessToken { * always be included in the response */ public RefreshToken(AccessToken token, Confirmation confirmation) { - this(); - this.issuer = token.issuer; - this.subject = token.subject; - this.issuedFor = token.issuedFor; - this.sessionId = token.sessionId; - this.nonce = token.nonce; - this.audience = new String[] { token.issuer }; - this.scope = token.scope; + this(token); this.confirmation = confirmation; } diff --git a/integration/client-cli/admin-cli/pom.xml b/integration/client-cli/admin-cli/pom.xml index cbc4683ab18..b4f5451514f 100755 --- a/integration/client-cli/admin-cli/pom.xml +++ b/integration/client-cli/admin-cli/pom.xml @@ -80,6 +80,8 @@ org/keycloak/representations/adapters/config/** org/keycloak/representations/adapters/action/** org/keycloak/representations/AccessTokenResponse.class + org/keycloak/representations/AuthorizationDetailsJSONRepresentation.class + org/keycloak/representations/AuthorizationDetailsResponse.class