[java-idp-plugin-oidc-op-oidfed] 01/02: Initial integration of resolve entity API client to explicit registration

Henri Mikkonen henri.mikkonen at iki.fi
Wed Oct 8 11:49:40 UTC 2025


This is an automated email from the git hooks/post-receive script.

hjmikkon pushed a commit to branch main
in repository java-idp-plugin-oidc-op-oidfed.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-oidc-op-oidfed.git;a=commit;h=831fdd2cf01305ede6170ec5a721c404c767f48a

commit 831fdd2cf01305ede6170ec5a721c404c767f48a
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed Oct 8 14:04:15 2025 +0300

    Initial integration of resolve entity API client to explicit registration
    
    Configuration is currently similar to the automatic registration integration
    - idp.oidfed.trustchain.resolver.useResolverApiCondition can be used for wiring custom condition for activating the resolution via API (default false)
    - idp.oidfed.trustchain.resolver.fallbackToLocalCondition can be used for wiring custom condition for fallbacking to local resolution (default true)
    - shibboleth.oidfed.ResolveEntityTrustChainMetadataCache is used for caching responses
    - conf/oidfed/oidfed-trustchain-resolver.xml contains a configurable map of trusted entities for resolution via API
      - shibboleth.oidfed.DefaultTrustedEntitiesLookupStrategy
        - map keyed with entity IDs, value list of trust_anchor parameters used in the API request message
      - shibboleth.oidfed.TrustedEntitiesLookupStrategy can be used to wire custom Function<ProfileRequestContext, Map<String,List<String>>
---
 .../idp/flows/oidfed/register/register-beans.xml   |  20 +++++
 .../idp/flows/oidfed/register/register-flow.xml    |  28 +++++-
 .../flow/oidfed/AbstractFederationFlowTest.java    |  22 ++++-
 .../AuthorizeFlowAutomaticRegistrationTest.java    |   9 --
 .../profile/flow/oidfed/RegistrationFlowTest.java  | 100 +++++++++++++++++++++
 5 files changed, 163 insertions(+), 16 deletions(-)

diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
index 8d6c7ad..8b6529c 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
@@ -78,6 +78,26 @@
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultCombinedMetadataFromTrustChainLookupStrategy"
         p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
  
+     <bean id="CallResolveEntityApi"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.CallResolveEntityApi"
+        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+        p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
+        p:resolveEntityTrustChainMetadataCache-ref="shibboleth.oidfed.ResolveEntityTrustChainMetadataCache"
+        p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
+        p:trustedEntitiesLookupStrategy="#{getObject('shibboleth.oidfed.TrustedEntitiesLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultTrustedEntitiesLookupStrategy')}"
+        p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
+        p:requireEntityConfigurationCondition-ref="shibboleth.Conditions.TRUE">
+        <property name="entityConfigurationLookupStrategy">
+            <bean parent="shibboleth.Functions.Expression"
+                c:expression="#custom.apply(#input.ensureInboundMessageContext().getMessage().getEntityConfiguration(), null)">
+                <property name="customObject">
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementSignatureValidationFilterStrategy"
+                        p:trustEngine-ref="shibboleth.oidfed.DefaultEntityConfigurationTrustEngine"/>
+                </property>
+            </bean>
+        </property>
+    </bean>
+
     <bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
         scope="prototype"
         p:trustChainCache-ref="#{'%{idp.oidfed.register.TrustChainMetadataCache:FetchThroughTrustChainMetadataCache}'.trim()}"
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-flow.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-flow.xml
index 99d2135..35a5716 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-flow.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-flow.xml
@@ -12,7 +12,26 @@
 
     <decision-state id="SelectTrustChainResolution">
         <if test="opensamlProfileRequestContext.ensureInboundMessageContext().getMessage().getTrustChain() != null"
-            then="ValidateProvidedTrustChain" else="ResolveTrustChains" />
+            then="ValidateProvidedTrustChain" else="ChooseResolutionMethod" />
+    </decision-state>
+
+    <decision-state id="ChooseResolutionMethod">
+        <if test="UseResolverApiCondition.test(opensamlProfileRequestContext)"
+            then="CallResolveEntityApi" else="ResolveTrustChains" />
+    </decision-state>
+
+    <action-state id="CallResolveEntityApi">
+        <evaluate expression="CallResolveEntityApi" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="SelectTrustChain">
+            <set name="flowScope.transitionForReselectTrustChain" value="'CallResolveEntityApi'" />
+        </transition>
+        <transition on="CheckFallback" to="CheckIfFallbackToLocalResolution" />
+    </action-state>
+
+    <decision-state id="CheckIfFallbackToLocalResolution">
+        <if test="FallbackToLocalResolutionCondition.test(opensamlProfileRequestContext)"
+            then="ResolveTrustChains" else="SelectTrustChain" />
     </decision-state>
 
     <action-state id="ValidateProvidedTrustChain">
@@ -24,10 +43,13 @@
     <action-state id="ResolveTrustChains">
         <evaluate expression="ResolveTrustChains" />
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="SelectTrustChain" />
+        <transition on="proceed" to="SelectTrustChain"/>
     </action-state>
 
     <action-state id="SelectTrustChain">
+        <on-entry>
+            <evaluate expression="flowRequestContext.getFlowScope().get('transitionForReselectTrustChain') != null ? flowRequestContext.getFlowScope().get('transitionForReselectTrustChain') : 'SelectTrustChain'" result="flowScope.reselectTrustChainTransition"/>
+        </on-entry>
         <evaluate expression="SelectTrustChain" />
         <evaluate expression="ResolveTrustMarks" />
         <evaluate expression="SelectExplicitRegistrationRelyingPartyConfiguration" />
@@ -35,7 +57,7 @@
         <evaluate expression="ValidateExplicitRegistrationProfileConfiguration" />
         <evaluate expression="PopulateInboundInterceptContext" />
         <evaluate expression="'proceed'" />
-        <transition on="ReselectTrustChain" to="SelectTrustChain" />
+        <transition on="ReselectTrustChain" to="#{reselectTrustChainTransition}" />
         <transition on="proceed" to="CheckInboundInterceptContext" />
     </action-state>
 
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
index 9d44226..baab2b2 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
@@ -50,6 +50,7 @@ import org.springframework.core.io.Resource;
 import org.springframework.webflow.executor.FlowExecutionResult;
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
@@ -85,6 +86,9 @@ import net.shibboleth.shared.logic.Constraint;
  */
 public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
 
+    public static final String USE_CUSTOM_RESOLVER_API_CONDITION = "useCustomResolverApi";
+    public static final String USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION = "useCustomFallbackToLocal";
+
     final static AtomicInteger clientIndex = new AtomicInteger();
     final static AtomicInteger intermediateIndex = new AtomicInteger();
     final String redirectUri = "https://rp.federation.local/cb";
@@ -122,6 +126,12 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         intermediateKey = initializeNewJwk("RSA", 2048, "mockIntermediateKey");
     }
 
+    @BeforeMethod
+    public void removeHeaders() {
+        request.removeHeader(USE_CUSTOM_RESOLVER_API_CONDITION);
+        request.removeHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION);
+    }
+
     protected JWK initializeNewJwk(final String algorithm, final int size, final String kid)
             throws NoSuchAlgorithmException {
         if ("RSA".equals(algorithm)) {
@@ -454,15 +464,19 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     }
 
     protected void rpResolveEntityConfigureMockHttpClient(final String clientId) {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(URI.create(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        rpResolveEntityConfigureMockHttpClient(clientId, metadata);
+    }
+
+    protected void rpResolveEntityConfigureMockHttpClient(final String clientId, final OIDCClientMetadata metadata) {
         try {
             mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
-            final OIDCClientMetadata metadata = new OIDCClientMetadata();
-            metadata.setRedirectionURI(new URI(redirectUri));
-            metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
             mapResponse(resolveEntityUrl(anchorResolveEndpoint, clientId, anchorId),
                     mockResponse(rpResolveEntityResponse(clientId, Map.of("openid_relying_party",
                             metadata.toJSONObject()))));
-        } catch (UnsupportedOperationException | IOException | URISyntaxException  e) {
+        } catch (UnsupportedOperationException | IOException e) {
             Assert.fail("Could not initialize mock HTTP client", e);
         }
     }
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
index c5ea9e0..12d8aae 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
@@ -35,7 +35,6 @@ import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.webflow.executor.FlowExecutionResult;
 import org.testng.Assert;
-import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -59,9 +58,6 @@ import net.shibboleth.shared.security.DataSealerException;
 
 public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFlowTest {
 
-    public static final String USE_CUSTOM_RESOLVER_API_CONDITION = "useCustomResolverApi";
-    public static final String USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION = "useCustomFallbackToLocal";
-
     @Autowired
     @Qualifier("shibboleth.StorageService")
     StorageService storageService;
@@ -70,11 +66,6 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         super(AuthorizeFlowTest.FLOW_ID);
     }
     
-    @BeforeMethod
-    public void removeHeaders() {
-        request.removeHeader(USE_CUSTOM_RESOLVER_API_CONDITION);
-    }
-
     @Test
     public void testWithValidTrustChain_noRequestObject()
             throws IOException, UnsupportedOperationException, URISyntaxException {
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
index 0e5e013..6d987a4 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
@@ -110,6 +110,55 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
     }
 
+    @Test
+    public void testValidEntityConfiguration_resolveApi() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        rpResolveEntityConfigureMockHttpClient(clientId);
+        setRequest("POST", rpEntityConfiguration(clientId), "application/entity-statement+jwt");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
+    }
+
+    @Test
+    public void testValidEntityConfiguration_resolveApi_failsNoFailback() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        rpConfigureMockHttpClient(clientId);
+        setRequest("POST", rpEntityConfiguration(clientId), "application/entity-statement+jwt");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        assertErrorCode(result, "invalid_request");
+    }
+
+    @Test
+    public void testValidEntityConfiguration_resolveApi_failsAndFailbackFails() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        request.addHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION, "true");
+        setRequest("POST", rpEntityConfiguration(clientId), "application/entity-statement+jwt");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        assertErrorCode(result, "invalid_request");
+    }
+
+    @Test
+    public void testValidEntityConfiguration_resolveApi_failsAndFailbackSuccess() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        request.addHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION, "true");
+        rpConfigureMockHttpClient(clientId);
+        setRequest("POST", rpEntityConfiguration(clientId), "application/entity-statement+jwt");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
+    }
+
     @Test
     public void testValidEntityConfiguration_customScope() throws Exception {
         final String clientId = uniqueClientId();
@@ -128,6 +177,25 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         Assert.assertEquals(providedMetadata.getScope(), scope);
     }
 
+    @Test
+    public void testValidEntityConfiguration_customScope_resolveApi() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        final Scope scope = Scope.parse("openid profile email custom");
+        metadata.setScope(scope);
+        rpResolveEntityConfigureMockHttpClient(clientId, metadata);
+        setRequest("POST", rpEntityConfiguration(clientId, metadata), "application/entity-statement+jwt");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        final OIDCClientMetadata providedMetadata = assertResponseStatement(
+                parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
+        Assert.assertEquals(providedMetadata.getScope(), scope);
+    }
+
     @Test
     public void testValidEntityConfiguration_repeat() throws Exception {
         final String clientId = uniqueClientId();
@@ -141,6 +209,20 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         }
     }
 
+    @Test
+    public void testValidEntityConfiguration_repeat_resolveApi() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        for (int i = 0; i < 2; i++) {
+            rpResolveEntityConfigureMockHttpClient(clientId);
+            setRequest("POST", rpEntityConfiguration(clientId), "application/entity-statement+jwt");
+            final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+            verify(federationHttpClient, times(0)).executeOpen(any(),
+                    argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+            assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
+        }
+    }
+
     @Test
     public void testValidTrustChain() throws Exception {
         final String clientId = uniqueClientId();
@@ -156,6 +238,24 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
     }
 
+    @Test
+    public void testValidTrustChain_resolveApi() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+                subordinateStatement(clientId, Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
+                + "\", \"" + trustedAnchorConfiguration() + "\"]";
+        setRequest("POST", trustChain, "application/trust-chain+json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(subordinateStatementUrl(anchorFetchEndpoint, clientId))), any());
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(resolveEntityUrl(anchorResolveEndpoint, clientId, anchorId))), any());
+        assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
+    }
+
     @Test
     public void testValidTrustChain_validMaxLengthInAnchor() throws Exception {
         final String clientId = uniqueClientId();

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list