From ea06651da5ba74f72297318dacfeaebeb9adfee3 Mon Sep 17 00:00:00 2001 From: Ingrid Kamga Date: Fri, 31 Oct 2025 11:32:24 +0100 Subject: [PATCH] [OID4VCI] Ensure `openid_credential` is one of `authorization_details_types_supported` on the Authorization Server metadata (#43599) Closes #43398 Signed-off-by: Ingrid Kamga --- .../OIDCConfigurationRepresentation.java | 11 ++ .../rar/AuthorizationDetailsProcessor.java | 5 + .../OID4VCAuthorizationDetailsProcessor.java | 5 + ...CAuthorizationDetailsProcessorFactory.java | 2 +- .../protocol/oidc/OIDCWellKnownProvider.java | 17 +++ ...uthorizationDetailsTypesSupportedTest.java | 127 ++++++++++++++++++ 6 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsTypesSupportedTest.java diff --git a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java index ea4e4bad2b5..b3c5c67eb20 100755 --- a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -202,6 +202,9 @@ public class OIDCConfigurationRepresentation { @JsonProperty("authorization_response_iss_parameter_supported") private Boolean authorizationResponseIssParameterSupported; + @JsonProperty("authorization_details_types_supported") + private List authorizationDetailsTypesSupported; + protected Map otherClaims = new HashMap(); public String getIssuer() { @@ -665,4 +668,12 @@ public class OIDCConfigurationRepresentation { public void setPromptValuesSupported(List promptValuesSupported) { this.promptValuesSupported = promptValuesSupported; } + + public List getAuthorizationDetailsTypesSupported() { + return authorizationDetailsTypesSupported; + } + + public void setAuthorizationDetailsTypesSupported(List authorizationDetailsTypesSupported) { + this.authorizationDetailsTypesSupported = authorizationDetailsTypesSupported; + } } diff --git a/server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationDetailsProcessor.java b/server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationDetailsProcessor.java index 6445a7f2e41..ce1227cbdb6 100644 --- a/server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationDetailsProcessor.java +++ b/server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationDetailsProcessor.java @@ -33,6 +33,11 @@ import java.util.List; */ public interface AuthorizationDetailsProcessor extends Provider { + /** + * Checks if this processor should be regarded as supported in the running context. + */ + boolean isSupported(); + /** * Processes the authorization_details parameter and returns a response if this processor * is able to handle the given authorization_details parameter. diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessor.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessor.java index 7bed19856bc..cadd4dd29c0 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessor.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessor.java @@ -52,6 +52,11 @@ public class OID4VCAuthorizationDetailsProcessor implements AuthorizationDetails this.session = session; } + @Override + public boolean isSupported() { + return session.getContext().getRealm().isVerifiableCredentialsEnabled(); + } + @Override public List process(UserSessionModel userSession, ClientSessionContext clientSessionCtx, String authorizationDetailsParameter) { if (authorizationDetailsParameter == null) { diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorFactory.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorFactory.java index 3d6562c7d5c..395aee75ecb 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorFactory.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCAuthorizationDetailsProcessorFactory.java @@ -31,7 +31,7 @@ import org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessorFactory; */ public class OID4VCAuthorizationDetailsProcessorFactory implements AuthorizationDetailsProcessorFactory, OID4VCEnvironmentProviderFactory { - public static final String PROVIDER_ID = "oid4vci-authorization-details-processor"; + public static final String PROVIDER_ID = OID4VCAuthorizationDetailsProcessor.OPENID_CREDENTIAL_TYPE; @Override public AuthorizationDetailsProcessor create(KeycloakSession session) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index f83ca89f059..40b79b33f12 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -38,6 +38,8 @@ import org.keycloak.protocol.oidc.grants.OAuth2GrantType; import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType; import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint; import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint; +import org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessor; +import org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessorFactory; import org.keycloak.protocol.oidc.representations.MTLSEndpointAliases; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import org.keycloak.protocol.oidc.utils.AcrUtils; @@ -215,6 +217,11 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setAuthorizationResponseIssParameterSupported(true); + List authorizationDetailsTypesSupported = getAuthorizationDetailsTypesSupported(); + if (!authorizationDetailsTypesSupported.isEmpty()) { + config.setAuthorizationDetailsTypesSupported(authorizationDetailsTypesSupported); + } + config = checkConfigOverride(config); return config; } @@ -323,6 +330,16 @@ public class OIDCWellKnownProvider implements WellKnownProvider { return mtls_endpoints; } + private List getAuthorizationDetailsTypesSupported() { + return session.getKeycloakSessionFactory() + .getProviderFactoriesStream(AuthorizationDetailsProcessor.class) + .map(AuthorizationDetailsProcessorFactory.class::cast) + .map(factory -> Map.entry(factory.getId(), factory.create(session))) + .filter(entry -> entry.getValue().isSupported()) + .map(Map.Entry::getKey) + .toList(); + } + private OIDCConfigurationRepresentation checkConfigOverride(OIDCConfigurationRepresentation config) { if (openidConfigOverride != null) { Map asMap = JsonSerialization.mapper.convertValue(config, Map.class); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsTypesSupportedTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsTypesSupportedTest.java new file mode 100644 index 00000000000..6d3022a05e5 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsTypesSupportedTest.java @@ -0,0 +1,127 @@ +/* + * 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.testsuite.oid4vc.issuance.signing; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.Response; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.common.Profile; +import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailsProcessor; +import org.keycloak.protocol.oid4vc.model.CredentialIssuer; +import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.oauth.OAuthClient; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailsProcessor.OPENID_CREDENTIAL_TYPE; + +/** + * Test to verify that authorization_details_types_supported is included in the OAuth Authorization Server + * metadata endpoint (/.well-known/oauth-authorization-server/) and that credential issuance works with + * authorization_details when scope is absent. + */ +@EnableFeature(value = Profile.Feature.OID4VC_VCI, skipRestart = true) +public class OID4VCAuthorizationDetailsTypesSupportedTest extends OID4VCIssuerEndpointTest { + + @Before + public void enableOpenID4VC() { + enableOpenID4VC(true); + } + + public void enableOpenID4VC(boolean enabled) { + RealmRepresentation realmRep = adminClient.realm(TEST_REALM_NAME).toRepresentation(); + realmRep.setVerifiableCredentialsEnabled(enabled); + adminClient.realm(TEST_REALM_NAME).update(realmRep); + } + + @Test + public void testAuthorizationDetailsTypesSupportedInOAuthAuthorizationServerMetadata() { + try (Client client = AdminClientUtil.createResteasyClient()) { + // Get OAuth Authorization Server metadata as required by OID4VC spec + OIDCConfigurationRepresentation oauthConfig = getOAuth2WellKnownConfiguration(client); + + // Verify that authorization_details_types_supported is present + assertNotNull("authorization_details_types_supported should be present", + oauthConfig.getAuthorizationDetailsTypesSupported()); + + // Verify that it contains openid_credential + List supportedTypes = oauthConfig.getAuthorizationDetailsTypesSupported(); + assertTrue("authorization_details_types_supported should contain openid_credential", + supportedTypes.contains(OID4VCAuthorizationDetailsProcessor.OPENID_CREDENTIAL_TYPE)); + + } + } + + @Test + public void testCredentialIssuerAuthorizationServerMetadata() { + try (Client client = AdminClientUtil.createResteasyClient()) { + // Get credential issuer metadata + CredentialIssuer credentialIssuer = getCredentialIssuerMetadata(); + assertNotNull("Credential issuer should not be null", credentialIssuer); + assertNotNull("Authorization servers should be present", credentialIssuer.getAuthorizationServers()); + assertFalse("Authorization servers should not be empty", credentialIssuer.getAuthorizationServers().isEmpty()); + + // Verify the authorization server from credential issuer metadata + String authServerUri = credentialIssuer.getAuthorizationServers().get(0); + OIDCConfigurationRepresentation authServerConfig = getOAuth2WellKnownConfiguration(client, authServerUri + "/.well-known/oauth-authorization-server"); + assertNotNull("Authorization server should support authorization_details_types_supported", + authServerConfig.getAuthorizationDetailsTypesSupported()); + assertTrue("Authorization server should support openid_credential", + authServerConfig.getAuthorizationDetailsTypesSupported().contains(OPENID_CREDENTIAL_TYPE)); + } + } + + @Test + public void testAuthorizationDetailsTypesSupportedNotInOAuth2WellKnownWhenOID4VCDisabled() { + // Disable OID4VC for this realm + enableOpenID4VC(false); + + try (Client client = AdminClientUtil.createResteasyClient()) { + // Get OAuth2 well-known configuration + OIDCConfigurationRepresentation oauth2Config = getOAuth2WellKnownConfiguration(client); + + // Verify that authorization_details_types_supported is not present + assertNull("authorization_details_types_supported should not be present when OID4VC is disabled", + oauth2Config.getAuthorizationDetailsTypesSupported()); + + } + } + + private OIDCConfigurationRepresentation getOAuth2WellKnownConfiguration(Client client) { + return getOAuth2WellKnownConfiguration(client, OAuthClient.AUTH_SERVER_ROOT + "/.well-known/oauth-authorization-server/realms/test"); + } + + private OIDCConfigurationRepresentation getOAuth2WellKnownConfiguration(Client client, String oauth2WellKnownUri) { + Response response = client.target(oauth2WellKnownUri) + .request() + .get(); + + assertEquals("OAuth Authorization Server metadata endpoint should return 200", 200, response.getStatus()); + + return response.readEntity(OIDCConfigurationRepresentation.class); + } +}