[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