[java-idp-plugin-oidc-op-oidfed] 01/02: Initial (still incomplete) implementation for resolve entity API client

Henri Mikkonen henri.mikkonen at iki.fi
Fri Sep 26 14:16:33 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=a1c9c7f57b9a9b15e30007aef7a5e95f12f2db10

commit a1c9c7f57b9a9b15e30007aef7a5e95f12f2db10
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Sep 26 17:04:37 2025 +0300

    Initial (still incomplete) implementation for resolve entity API client
    
    - Currently integrated only to the automatic registration process
      - 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>>
---
 ...ntityStatementSignatureValidationComponent.java |  47 ++
 ...actTrustEngineSignatureValidationComponent.java |  14 +-
 .../DefaultEntityStatementFetchingStrategy.java    |   4 +-
 ...StatementSignatureValidationFilterStrategy.java |   2 +-
 ...efaultProvidedTrustChainValidationStrategy.java |   2 +-
 ...yResponseSignatureValidationFilterStrategy.java | 128 +++++
 ...ultResolveEntityTrustChainFetchingStrategy.java | 218 +++++++++
 .../oidfed/profile/impl/CallResolveEntityApi.java  | 515 +++++++++++++++++++++
 ...eAutomaticRegistrationProfileConfiguration.java |  16 +-
 .../DefaultLocalMetadataPolicyMergingStrategy.java |   3 +-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  51 +-
 .../oidfed/metadata-lookup-ext-oidfed-beans.xml    |  19 +
 .../oidfed/metadata-lookup-ext-oidfed-flow.xml     |  29 +-
 .../idp/service/relying-party/postconfig.xml       |  22 +-
 .../conf/oidfed/oidfed-trustchain-resolver.xml     |  40 ++
 .../idp/plugin/oidc/op/oidfed/module.properties    |   4 +
 .../flow/oidfed/AbstractFederationFlowTest.java    |  51 +-
 .../AuthorizeFlowAutomaticRegistrationTest.java    |  98 ++++
 .../net/shibboleth/idp/module/conf/global.xml      |   8 +
 .../net/shibboleth/idp/module/conf/oidc.properties |   4 +-
 .../conf/oidfed/oidfed-trustchain-resolver.xml     |  24 +
 21 files changed, 1268 insertions(+), 31 deletions(-)

diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractEntityStatementSignatureValidationComponent.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractEntityStatementSignatureValidationComponent.java
new file mode 100644
index 0000000..2c48bf2
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractEntityStatementSignatureValidationComponent.java
@@ -0,0 +1,47 @@
+/*
+ * 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 net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.trust.TrustEngine;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Abstract component performing entity statement signature validation via {@link TrustEngine}.
+ */
+public class AbstractEntityStatementSignatureValidationComponent
+    extends AbstractTrustEngineSignatureValidationComponent {
+
+    /**
+     * Validates the given entity statement via trust engine and the given criteria.
+     * 
+     * @param entityStatement the entity statement to be validated
+     * @param criteria the criteria (expanded with the optional default criteria)
+     * @param entityId the entity ID used for logging
+     * @return true if validation was successful, false otherwise
+     */
+    protected boolean validateStatement(@Nonnull final EntityStatement entityStatement,
+            @Nonnull final CriteriaSet criteria, @Nullable final String entityId) {
+        final SignedJWT jwt = entityStatement.getSignedStatement();
+        assert jwt != null;
+        return validateJwt(jwt, criteria, entityId);
+    }
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
index 45c847c..73ec509 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
@@ -22,7 +22,6 @@ import org.opensaml.security.trust.TrustEngine;
 import org.slf4j.Logger;
 
 import com.nimbusds.jwt.SignedJWT;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
@@ -75,21 +74,19 @@ public class AbstractTrustEngineSignatureValidationComponent extends AbstractIde
     }
 
     /**
-     * Validates the given entity statement via trust engine and the given criteria.
+     * Validates the given JWT via trust engine and the given criteria.
      * 
-     * @param entityStatement the entity statement to be validated
+     * @param jwt the JWT to be validated
      * @param criteria the criteria (expanded with the optional default criteria)
      * @param entityId the entity ID used for logging
      * @return true if validation was successful, false otherwise
      */
-    protected boolean validateStatement(@Nonnull final EntityStatement entityStatement,
-            @Nonnull final CriteriaSet criteria, @Nullable final String entityId) {
+    protected boolean validateJwt(@Nonnull final SignedJWT jwt, @Nonnull final CriteriaSet criteria,
+            @Nullable final String entityId) {
         if (defaultCriteria != null && !defaultCriteria.isEmpty()) {
             criteria.addAll(defaultCriteria);
         }
         try {
-            final SignedJWT jwt = entityStatement.getSignedStatement();
-            assert jwt != null;
             if (trustEngine.validate(jwt, criteria)) {
                 log.debug("Successfully validated entity statement for {}", entityId);
                 return true;
@@ -100,5 +97,4 @@ public class AbstractTrustEngineSignatureValidationComponent extends AbstractIde
         log.warn("Trust Engine validation failed for {}", entityId);
         return false;
     }
-
-}
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
index acf0579..f508d1f 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
@@ -138,11 +138,11 @@ public class DefaultEntityStatementFetchingStrategy extends AbstractIdentifiable
             final String scheme = httpRequest.getUri().getScheme();
             assert scheme != null;
             HttpClientSecuritySupport.checkTLSCredentialEvaluated(httpContext, scheme);
-            if (response.getCode() == HttpStatus.SC_OK) {
+            if (response != null && response.getCode() == HttpStatus.SC_OK) {
                 return EntityStatementHelper.deserializeEntityStatement(EntityUtils.toString(response.getEntity()));
             } else {
                 log.debug("Unable to fetch entity configuration from URI: {} (HTTP status {})", uri,
-                        response.getCode());
+                        response == null ? null : response.getCode());
                 return null;
             }
         } catch (final ParseException | URISyntaxException | IOException e) {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
index a3a06e4..e4952b2 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
@@ -35,7 +35,7 @@ import net.shibboleth.shared.resolver.CriteriaSet;
  */
 @ThreadSafeAfterInit
 public class DefaultEntityStatementSignatureValidationFilterStrategy
-    extends AbstractTrustEngineSignatureValidationComponent
+    extends AbstractEntityStatementSignatureValidationComponent
     implements BiFunction<EntityStatement, MetadataFilterContext, EntityStatement> {
 
     /** Class logger. */
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
index d6bbcd7..cd48e81 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
@@ -43,7 +43,7 @@ import net.shibboleth.shared.resolver.CriteriaSet;
  * trust anchor signature validation filter.
  */
 public class DefaultProvidedTrustChainValidationStrategy
-    extends AbstractTrustEngineSignatureValidationComponent 
+    extends AbstractEntityStatementSignatureValidationComponent 
     implements BiPredicate<ProfileRequestContext, List<EntityStatement>> {
 
     /** Class logger. */
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseSignatureValidationFilterStrategy.java
new file mode 100644
index 0000000..7284e9c
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseSignatureValidationFilterStrategy.java
@@ -0,0 +1,128 @@
+/*
+ * 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 net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.trust.TrustEngine;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default signature validating filter for resolve entity response. The signature validation is performed via
+ * configurable {@link TrustEngine}.
+ */
+ at ThreadSafeAfterInit
+public class DefaultResolveEntityResponseSignatureValidationFilterStrategy
+    extends AbstractTrustEngineSignatureValidationComponent
+    implements BiFunction<ResolveEntityResponseContainer, MetadataFilterContext, ResolveEntityResponseContainer> {
+
+    /** Class logger. */
+    @Nonnull private Logger log =
+            LoggerFactory.getLogger(DefaultResolveEntityResponseSignatureValidationFilterStrategy.class);
+
+    /** Cache used to fetch the issuer entity configuration from. */
+    @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+
+    /**
+     * Set the cache used to fetch the issuer entity configuration from.
+     * 
+     * @param cache cache used to fetch the issuer entity configuration from
+     */
+    public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+        checkSetterPreconditions();
+        entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (entityConfigurationCache == null) {
+            throw new ComponentInitializationException("Entity Configuration cache cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public ResolveEntityResponseContainer apply(@Nullable final ResolveEntityResponseContainer response,
+            @Nullable final MetadataFilterContext filterContext) {
+        checkComponentActive();
+        if (response == null) {
+            return null;
+        }
+        if (response.getResponse() instanceof ResolveEntityResponse successResponse) {
+            final SignedJWT jwt = successResponse.getJWT();
+            final String entityId;
+            try {
+                entityId = jwt.getJWTClaimsSet().getIssuer();
+            } catch (final ParseException e) {
+                log.error("Could not parse issuer entity ID value from the response", e);
+                return null;
+            }
+            final EntityStatement issuerStatement = fetchIssuerStatement(entityId);
+            if (issuerStatement == null) {
+                return null;
+            }
+            log.trace("Starting signature validation of success response from {}", entityId);
+            final CriteriaSet criteria = new CriteriaSet(new SubjectEntityStatementCriterion(issuerStatement));
+            if (validateJwt(jwt, criteria, entityId)) {
+                return response;
+            }
+        } else {
+            log.trace("Ignoring signagure validation for the error response");
+            return response;
+        }
+        return null;
+    }
+
+    /**
+     * Fetch the issuer entity configuration from the metadata cache.
+     * 
+     * @param issuer the issuer entity ID
+     * @return the issuer entity configuration, or null if could not be fetched
+     */
+    @Nullable protected EntityStatement fetchIssuerStatement(@Nonnull final String issuer) {
+        final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(issuer));
+        try {
+            final List<EntityStatement> result = entityConfigurationCache.get(criteria);
+            if (!result.isEmpty()) {
+                return result.get(0);
+            }
+        } catch (final MetadataCacheException e) {
+            log.debug("Error while fetching issuer entity configuration for {}", issuer, e);
+        }
+        log.warn("Could not fetch entity configuration for {}", issuer);
+        return null;
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityTrustChainFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityTrustChainFetchingStrategy.java
new file mode 100644
index 0000000..03bfa2d
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityTrustChainFetchingStrategy.java
@@ -0,0 +1,218 @@
+/*
+ * 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 net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
+import org.apache.hc.core5.net.URIBuilder;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default strategy for fetching trust chains via resolve entity API for an entity specified in the criteria set.
+ * Caches for entity configurations and subordinate statements are exploited for actual fetching of the entity
+ * statements. The entity configuration may also be delivered via {@link SubjectEntityStatementCriterion} in the
+ * criteria set.
+ */
+ at ThreadSafeAfterInit
+public class DefaultResolveEntityTrustChainFetchingStrategy extends AbstractIdentifiableInitializableComponent
+        implements Function<CriteriaSet, ResolveEntityResponseContainer> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultResolveEntityTrustChainFetchingStrategy.class);
+
+    /** Cache containing local copies of trusted trust anchor keys. */
+    @NonnullAfterInit private MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
+
+    /** HTTP client to use. */
+    @NonnullAfterInit protected HttpClient httpClient;
+
+    /** HTTP client security parameters. */
+    @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+
+    /** JSON object mapper used for decoding JSON into Map. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /**
+     * Set the cache containing local copies of trusted trust anchor keys.
+     * 
+     * @param cache cache containing local copies of trusted trust anchor keys.
+     */
+    public void setLocalTrustAnchorsCache(@Nonnull final MetadataCache<Map<String, LocalKeyContainer>> cache) {
+        checkSetterPreconditions();
+        localTrustAnchorsCache = Constraint.isNotNull(cache, "Local Trust Anchor cache cannot be null");
+    }
+
+    /**
+     * Set the {@link HttpClient} to use.
+     * 
+     * @param client HTTP client to use
+     */
+    public void setHttpClient(@Nonnull final HttpClient client) {
+        checkSetterPreconditions();
+        httpClient = Constraint.isNotNull(client, "HttpClient cannot be null");
+    }
+
+    /**
+     * Set the optional client security parameters.
+     * 
+     * @param params the new client security parameters
+     */
+    public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+        checkSetterPreconditions();
+        httpClientSecurityParameters = params;
+    }
+    /**
+
+     * Set the JSON {@link ObjectMapper} used for decoding JSON into Map.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (localTrustAnchorsCache == null) {
+            throw new ComponentInitializationException("Local Trust Anchor cache cannot be null");
+        }
+        if (httpClient == null) {
+            throw new ComponentInitializationException("Httpclient cannot be null");
+        }
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public ResolveEntityResponseContainer apply(@Nullable final CriteriaSet criteria) {
+        checkComponentActive();
+        if (criteria == null) {
+            return null;
+        }
+
+        final ResolveEntityRequestCriterion requestCriterion = criteria.get(ResolveEntityRequestCriterion.class);
+        if (requestCriterion == null) {
+            log.debug("No request criterion given, returning null");
+            return null;
+        }
+        final ResponseContainerExpirationCriterion expirationCriterion =
+                criteria.get(ResponseContainerExpirationCriterion.class);
+        if (expirationCriterion == null) {
+            log.debug("No expiration criterion given, returning null");
+            return null;
+        }
+        final ResolveEntityRequest resolveRequest = requestCriterion.getRequest();
+        final HttpGet httpRequest = new HttpGet(resolveRequest.getEndpointURI());
+        final List<NameValuePair> nvps = new ArrayList<>();
+        nvps.add(new BasicNameValuePair("sub", resolveRequest.getSubject()));
+        resolveRequest.getTrustAnchors()
+            .forEach(trustAnchor -> nvps.add(new BasicNameValuePair("trust_anchor", trustAnchor)));
+        resolveRequest.getEntityTypes()
+            .forEach(entityType -> nvps.add(new BasicNameValuePair("entity_type", entityType)));
+        final URI uri;
+        try {
+            uri = new URIBuilder(httpRequest.getUri()).addParameters(nvps).build();
+        } catch (final URISyntaxException e) {
+            log.error("Could not build endpoint URI for the resolve entity request", e);
+            return null;
+        }
+        log.debug("Using URI {} for fetching resolve entity API response", uri);
+        httpRequest.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED);
+        httpRequest.setUri(uri);
+
+        final HttpClientContext httpContext = buildHttpContext(httpRequest);
+        try (final ClassicHttpResponse response = httpClient.executeOpen(null, httpRequest, httpContext)) {
+            final String scheme = httpRequest.getUri().getScheme();
+            assert scheme != null;
+            HttpClientSecuritySupport.checkTLSCredentialEvaluated(httpContext, scheme);
+            if (response != null && response.getCode() == HttpStatus.SC_OK) {
+                final SignedJWT responseJwt = SignedJWT.parse(EntityUtils.toString(response.getEntity()));
+                final ResolveEntityResponse resolveResponse = new ResolveEntityResponse(responseJwt);
+                final Instant expirationTime = expirationCriterion.getExpirationInstant();
+                return new ResolveEntityResponseContainer(resolveResponse, resolveRequest, expirationTime);
+            } else {
+                log.debug("Unable to fetch resolve entity response from URI: {} (HTTP status {})", uri,
+                        response == null ? null : response.getCode());
+                //TODO: cache non-success results?
+                return null;
+            }
+        } catch (final ParseException | URISyntaxException | IOException | java.text.ParseException e) {
+            log.debug("Unable to fetch resolve entity response from URI: {}", uri, e);
+        }
+
+        return null;
+
+    } 
+
+    /**
+     * Build the {@link HttpClientContext} instance to be used by the HttpClient.
+     * 
+     * @param request the HTTP client request
+     * @return the client context instance
+     */
+    @Nonnull
+    protected HttpClientContext buildHttpContext(@Nonnull final ClassicHttpRequest request) {
+        final HttpClientContext clientContext = HttpClientContext.create();
+        assert clientContext != null;
+        HttpClientSecuritySupport.marshalSecurityParameters(clientContext, httpClientSecurityParameters, false);
+        HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(clientContext, request);
+        return clientContext;
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java
new file mode 100644
index 0000000..132fe9a
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java
@@ -0,0 +1,515 @@
+/*
+ * 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 net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatementHelper;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityRequestCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityResponseContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResponseContainerExpirationCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityStatementCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultPreSelectedTrustChainIDsLookupStrategy;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainIDsLookupStrategy;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Resolves trust chain, policy-enforced metadata and trust marks via configurable
+ * {@link #resolveEntityTrustChainMetadataCache}. The data is populated to the {@link RelyingPartyTrustChainContext}.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class CallResolveEntityApi extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(CallResolveEntityApi.class);
+
+    /** Strategy used to create the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextCreationStrategy;
+
+    /** Cache used to fetch the issuer entity configuration from. */
+    @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+
+    /** Cache containing responses from resolve entity APIs. */
+    @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> resolveEntityTrustChainMetadataCache;
+
+    /** Strategy used to obtain the client id value of the request. */
+    @NonnullAfterInit private Function<MessageContext, ClientID> clientIDLookupStrategy;
+
+    /** Strategy used to fetch the pre-selected trust chain entity IDs. */
+    @Nonnull private Function<ProfileRequestContext, List<String>> preSelectedTrustChainIdsLookupStrategy;
+
+    /** Strategy used to get entity IDs from a trust chain. */
+    @Nonnull private Function<List<EntityStatement>, List<String>> trustChainIDsLookupStrategy;
+
+    /** Strategy used to fetch entity configuration delivered to the trust chain cache. */
+    @Nonnull private Function<ProfileRequestContext, EntityStatement> entityConfigurationLookupStrategy;
+
+    /** Condition to require entity configuration via {@link #entityConfigurationLookupStrategy}. */
+    @Nonnull private Predicate<ProfileRequestContext> requireEntityConfigurationCondition;
+
+    /** Strategy used to get map of trusted entities for resolve entity APIs. */
+    @NonnullAfterInit private Function<ProfileRequestContext, Map<String,List<String>>> trustedEntitiesLookupStrategy;
+
+    /** Strategy used to fetch the entity types used in the resolve entity request. */
+    @Nonnull private Function<ProfileRequestContext, List<String>> entityTypesLookupStrategy;
+
+    /** Lookup function to supply cached response lifetime. */
+    @Nonnull private Function<ProfileRequestContext,Duration> cachedResponseLifetimeLookupStrategy;
+
+    /** JSON object mapper used for decoding JSON into Map. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /** OAuth2 client id. */
+    @NonnullBeforeExec private String clientId;
+
+    /** Trusted entities to use. */
+    @NonnullBeforeExec private Map<String,List<String>> trustedEntities;
+
+    /**
+     * Constructor.
+     */
+    public CallResolveEntityApi() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tccs =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class, true).compose(
+                        new InboundMessageContextLookup());
+        assert tccs != null;
+        trustChainContextCreationStrategy = tccs;
+        preSelectedTrustChainIdsLookupStrategy = new DefaultPreSelectedTrustChainIDsLookupStrategy();
+        trustChainIDsLookupStrategy = new DefaultTrustChainIDsLookupStrategy();
+        entityConfigurationLookupStrategy = FunctionSupport.constant(null);
+        requireEntityConfigurationCondition = PredicateSupport.alwaysFalse();
+        entityTypesLookupStrategy = FunctionSupport.constant(List.of("openid_relying_party"));
+        cachedResponseLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofMinutes(5));
+    }
+
+    /**
+     * Set the strategy used to create the trust chain context.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setTrustChainContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+        checkSetterPreconditions();
+        trustChainContextCreationStrategy =
+                Constraint.isNotNull(strategy, "TrustChainContextCreationStrategy cannot be null");
+    }
+
+    /**
+     * Set the cache used to fetch the issuer entity configuration from.
+     * 
+     * @param cache cache used to fetch the issuer entity configuration from
+     */
+    public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+        checkSetterPreconditions();
+        entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
+    }
+
+    public void setResolveEntityTrustChainMetadataCache(
+            @Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+        checkSetterPreconditions();
+        resolveEntityTrustChainMetadataCache =
+                Constraint.isNotNull(cache, "ResolveEntityTrustChainMetadataCache cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the client id of the request.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setClientIDLookupStrategy(@Nonnull final Function<MessageContext, ClientID> strategy) {
+        checkSetterPreconditions();
+        clientIDLookupStrategy =
+                Constraint.isNotNull(strategy, "ClientIDLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to fetch the pre-selected trust chain entity IDs.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setPreSelectedTrustChainIdsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+        checkSetterPreconditions();
+        preSelectedTrustChainIdsLookupStrategy = Constraint.isNotNull(strategy,
+                "PreSelectedTrustChainIdsLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to get entity IDs from a trust chain.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustChainIDsLookupStrategy(@Nonnull final Function<List<EntityStatement>, List<String>> strategy) {
+        checkSetterPreconditions();
+        trustChainIDsLookupStrategy = Constraint.isNotNull(strategy, "TrustChainIDsLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to fetch entity configuration delivered to the trust chain cache.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setEntityConfigurationLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, EntityStatement> strategy) {
+        checkSetterPreconditions();
+        entityConfigurationLookupStrategy = Constraint.isNotNull(strategy,
+                "EntityConfigurationLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the condition to require entity configuration via {@link #entityConfigurationLookupStrategy}.
+     * @param predicate condition
+     */
+    public void setRequireEntityConfigurationCondition(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        checkSetterPreconditions();
+        requireEntityConfigurationCondition =
+                Constraint.isNotNull(predicate, "RequireEntityConfigurationCondition cannot be null");
+    }
+
+    /**
+     * Set the strategy used to get map of trusted entities for resolve entity APIs.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustedEntitiesLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Map<String,List<String>>> strategy) {
+        checkSetterPreconditions();
+        trustedEntitiesLookupStrategy = Constraint.isNotNull(strategy, "TrustedEntitiesLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to fetch the entity types used in the resolve entity request.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setEntityTypesLookupStrategy(@Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+        checkSetterPreconditions();
+        entityTypesLookupStrategy = Constraint.isNotNull(strategy, "EntityTypesLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the JSON {@link ObjectMapper} used for decoding JSON into Map.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /**
+     * Set a lookup strategy for the cached response lifetime.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setCachedResponseLifetimeLookupStrategy(
+            @Nullable final Function<ProfileRequestContext,Duration> strategy) {
+        cachedResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (entityConfigurationCache == null) {
+            throw new ComponentInitializationException("Entity Configuration cache cannot be null");
+        }
+        if (resolveEntityTrustChainMetadataCache == null) {
+            throw new ComponentInitializationException("ResolveEntityTrustChainMetadataCache cannot be null");
+        }
+        if (clientIDLookupStrategy == null) {
+            throw new ComponentInitializationException("ClientIDLookupStrategy cannot be null");
+        }
+        if (trustedEntitiesLookupStrategy == null) {
+            throw new ComponentInitializationException("TrustedEntitiesLookupStrategy cannot be null");
+        }
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("ObjectMapper cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        final ClientID id = clientIDLookupStrategy.apply(profileRequestContext.getInboundMessageContext());
+        if (id == null) {
+            log.error("{} Unable to obtain client ID", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        clientId = id.getValue();
+        try {
+            new URL(clientId).toURI();
+        } catch (final URISyntaxException | MalformedURLException e) {
+            log.debug("{} The client ID {} is not a valid URL, nothing to do", getLogPrefix(), clientId);
+            return false;
+        }
+
+        trustedEntities = trustedEntitiesLookupStrategy.apply(profileRequestContext);
+        if (trustedEntities == null || trustedEntities.isEmpty()) {
+            log.warn("{} No trusted entities resolved for {} nothing to do", getLogPrefix(), clientId);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        log.debug("{} Resolving trust chain via resolve entity API for {}", getLogPrefix(), clientId);
+        assert clientId != null;
+        final CriteriaSet criteriaSet = new CriteriaSet(new SubjectEntityIDCriterion(clientId));
+        final EntityStatement entityConfiguration = entityConfigurationLookupStrategy.apply(profileRequestContext);
+        if (entityConfiguration != null) {
+            log.debug("{} Entity configuration resolved and included to the criteria set", getLogPrefix());
+            criteriaSet.add(new SubjectEntityStatementCriterion(entityConfiguration));
+        } else if (requireEntityConfigurationCondition.test(profileRequestContext)) {
+            log.error("{} Mandatory entity configuration could not be resolved", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+            return;
+        }
+        final Duration cachedLifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+        if (cachedLifetime == null) {
+            log.warn("{} Could not resolve lifetime for success responses", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        criteriaSet.add(new ResponseContainerExpirationCriterion(Instant.now().plus(cachedLifetime)));
+
+        final List<String> preSelectedChain =
+                Optional.ofNullable(preSelectedTrustChainIdsLookupStrategy.apply(profileRequestContext))
+                .orElse(CollectionSupport.emptyList());
+
+        for (final String trustedEntity : trustedEntities.keySet()) {
+            final List<String> trustAnchors = trustedEntities.get(trustedEntity);
+            if (trustAnchors == null || trustAnchors.isEmpty()) {
+                log.warn("{} No trust anchors defined for trusted entity {}", getLogPrefix(), trustedEntity);
+                continue;
+            }
+            final URI uri = fetchResolveEntityEndpoint(trustedEntity);
+            if (uri == null) {
+                log.warn("{} Could not fetch federation resolve endpoint for {}", getLogPrefix(), trustedEntity);
+                continue;
+            }
+            final ResolveEntityRequest entityRequest = new ResolveEntityRequest(
+                    uri, clientId, trustAnchors, List.of("openid_relying_party"));
+            criteriaSet.add(new ResolveEntityRequestCriterion(entityRequest));
+            final List<ResolveEntityResponseContainer> cacheResult;
+            try {
+                cacheResult = resolveEntityTrustChainMetadataCache.get(criteriaSet);
+            } catch (final MetadataCacheException e) {
+                log.warn("{} Could not resolve entity for {}", getLogPrefix(), clientId, e);
+                continue;
+            }
+            if (cacheResult.isEmpty()) {
+                log.debug("{} No data resolved for {}", getLogPrefix(), clientId);
+                continue;
+            }
+            if (cacheResult.get(0).getResponse() instanceof ResolveEntityResponse successResponse) {
+                final List<String> rawTrustChain;
+                final Map<String, Object> rawMetadata;
+                final List<Object> rawTrustMarks;
+                try {
+                    rawTrustChain = successResponse.getJWT().getJWTClaimsSet().getStringListClaim("trust_chain");
+                    rawMetadata = successResponse.getJWT().getJWTClaimsSet().getJSONObjectClaim("metadata");
+                    rawTrustMarks = successResponse.getJWT().getJWTClaimsSet().getListClaim("trust_marks");
+                } catch (final ParseException e) {
+                    log.error("{} Could not parse resolve entity response contents", getLogPrefix(), e);
+                    continue;
+                }
+                if (rawTrustChain == null || rawTrustChain.isEmpty() || rawMetadata == null || rawMetadata.isEmpty()) {
+                    log.warn("{} Could not parse mandatory parameters from the response", getLogPrefix());
+                    continue;
+                }
+                final List<EntityStatement> chain = rawTrustChain.stream()
+                        .filter(Objects::nonNull)
+                        .map(entry -> EntityStatementHelper.deserializeEntityStatement(entry))
+                        .toList();
+                if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
+                    log.debug("{} Ignored resolved trust chain that doesn't match with preselected chain", getLogPrefix());
+                    continue;
+                }
+                final Map<String, Map<String, Object>> metadata = rawMetadata.entrySet().stream()
+                        .filter(entry -> entry.getKey() instanceof String && entry.getValue() instanceof Map<?,?>)
+                        .collect(Collectors.toMap(entry -> (String) entry.getKey(),
+                                entry -> ((Map<?,?>)entry.getValue()).entrySet().stream()
+                                .filter(e -> e.getKey() instanceof String)
+                                .collect(Collectors.toMap(e -> (String) e.getKey(), e -> e.getValue()))));
+
+                final RelyingPartyTrustChainContext trustChainContext =
+                        trustChainContextCreationStrategy.apply(profileRequestContext);
+                final List<Pair<List<EntityStatement>, Map<String,Map<String,Object>>>> policyCompliantChains =
+                        new ArrayList<>();
+                policyCompliantChains.add(new Pair<>(chain, metadata));
+                trustChainContext.setPolicyCompliantTrustChains(policyCompliantChains);
+                log.debug("{} Populated policy compliant trust chains with {}", getLogPrefix(), policyCompliantChains);
+
+                final List<SignedJWT> trustMarks = rawTrustMarks == null ? null : rawTrustMarks
+                        .stream()
+                        .filter(Map.class::isInstance)
+                        .map(Map.class::cast)
+                        .map(map -> parseTrustMark(map.get("trust_mark")))
+                        .filter(Objects::nonNull)
+                        .toList();
+                if (trustMarks != null) {
+                    final Map<String, List<SignedJWT>> trustMarksByEntity = new HashMap<>();
+                    for (final String entity : chain.stream().map(entity -> entity.getEntityID().getValue()).toList()) {
+                        final List<SignedJWT> trustMarksForEntity = trustMarks.stream()
+                                .filter(jwt -> {
+                                    try {
+                                        return entity.equals(jwt.getJWTClaimsSet().getSubject());
+                                    } catch (final ParseException e1) {
+                                        return false;
+                                    }
+                                }).toList();
+                        if (!trustMarksForEntity.isEmpty()) {
+                            trustMarksByEntity.put(entity, trustMarksForEntity);
+                        }
+                    }
+                    if (!trustMarksByEntity.isEmpty()) {
+                        trustChainContext.setVerifiedTrustMarks(trustMarksByEntity);
+                        final Map<String, List<String>> trustMarkIds = trustMarksByEntity.entrySet().stream()
+                                .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().stream()
+                                        .map(jwt -> {
+                                            try {
+                                                return jwt.getJWTClaimsSet().getStringClaim("trust_mark_id");
+                                            } catch (final ParseException e1) {
+                                                return null;
+                                            }
+                                        })
+                                        .filter(Objects::nonNull)
+                                        .toList()));
+                        log.debug("{} The following trust marks are included: {}", getLogPrefix(), trustMarkIds);
+                        trustChainContext.setVerifiedTrustMarkIds(trustMarkIds);
+                        
+                    }
+                } else {
+                    log.debug("{} No trust marks included in the response", getLogPrefix());
+                }
+                return;
+            } else {
+                log.debug("{} The response was not a success response: {}", getLogPrefix(),
+                        cacheResult.get(0).getResponse());
+                continue;
+            }
+        }
+        log.debug("{} No previously rejected policy-compliant trust chains resolved", getLogPrefix());
+        ActionSupport.buildEvent(profileRequestContext, "CheckFallback");
+
+    }
+
+    /**
+     * Fetch the resolve entity endpoint via entity configuration from the metadata cache.
+     * 
+     * @param subject the entity ID
+     * @return the resolve entity endpoint, or null if could not be fetched
+     */
+    @Nullable protected URI fetchResolveEntityEndpoint(@Nonnull final String subject) {
+        final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(subject));
+        try {
+            final List<EntityStatement> result = entityConfigurationCache.get(criteria);
+            if (!result.isEmpty()) {
+                return Optional.ofNullable(
+                        EntityStatementHelper.parseMetadata(objectMapper, result.get(0)).get("federation_entity"))
+                        .filter(Map.class::isInstance)
+                        .map(map -> (Map<String,Object>) map)
+                        .map(map -> map.get("federation_resolve_endpoint"))
+                        .filter(String.class::isInstance)
+                        .map(String.class::cast)
+                        .map(URI::create)
+                        .orElse(null);
+            }
+        } catch (final MetadataCacheException e) {
+            log.debug("Error while fetching entity configuration for {}", subject, e);
+        }
+        log.warn("Could not fetch entity configuration for {}", subject);
+        return null;
+    }
+
+    /**
+     * Parses the trust mark.
+     * 
+     * @param trustMark trust mark to be verified, expected to be parseable from string
+     * @return trust mark JWT if valid, null otherwise
+     */
+    @Nullable private SignedJWT parseTrustMark(@Nullable final Object trustMark) {
+        if (trustMark instanceof String string) {
+            try {
+                return SignedJWT.parse(string);
+            } catch (final ParseException e) {
+                log.error("Could not parse the trust mark into a JWT", e);
+            }
+        }
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
index 530c3ca..a770c3f 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
@@ -199,21 +199,25 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
             return;
         }
-        final OIDCClientInformation clientInformation = new OIDCClientInformation(
-                new ClientID(selectedTrustChain.getFirst().get(0).getEntityID().getValue()), metadata);
-
+        final OIDCClientInformation clientInformation;
         final Map<String, MetadataPolicy> localMetadataPolicy =
                 localMetadataPolicyLookupStrategy.apply(profileRequestContext);
         if (localMetadataPolicy != null && !localMetadataPolicy.isEmpty()) {
             log.debug("{} Applying local metadata policy into the client metadata", getLogPrefix());
-            final OIDCClientInformation enforcedMetadata =
-                    localMetadataPolicyMergingStrategy.apply(clientInformation, localMetadataPolicy);
-            if (enforcedMetadata == null) {
+            clientInformation = localMetadataPolicyMergingStrategy.apply(
+                    new OIDCClientInformation(
+                            new ClientID(selectedTrustChain.getFirst().get(0).getEntityID().getValue()),
+                            metadata), localMetadataPolicy);
+            if (clientInformation == null) {
                 log.error("{} Could not apply the local metadata policy into the client metadata", getLogPrefix());
                 ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
                 return;
             }
             selectedTrustChain.setSecond(Map.of("openid_relying_party", clientInformation.toJSONObject()));
+        } else {
+            clientInformation = new OIDCClientInformation(
+                    new ClientID(selectedTrustChain.getFirst().get(0).getEntityID().getValue()), metadata);
+
         }
         final String clientId = clientInformation.getID().getValue();
         final List<String> mandatoryTrustMarks = mandatoryTrustMarksLookupStrategy.apply(profileRequestContext);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultLocalMetadataPolicyMergingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultLocalMetadataPolicyMergingStrategy.java
index 684aa0d..14eeb8f 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultLocalMetadataPolicyMergingStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultLocalMetadataPolicyMergingStrategy.java
@@ -79,7 +79,7 @@ public class DefaultLocalMetadataPolicyMergingStrategy extends AbstractIdentifia
         if (localPolicy == null || localPolicy.isEmpty()) {
             return inputMetadata;
         }
-        final JSONObject result = inputMetadata.getOIDCMetadata().toJSONObject();
+        final JSONObject result = inputMetadata.toJSONObject();
         boolean compliant = true;
         for (final String claim : localPolicy.keySet()) {
             final MetadataPolicy policy = localPolicy.get(claim);
@@ -102,6 +102,7 @@ public class DefaultLocalMetadataPolicyMergingStrategy extends AbstractIdentifia
         } else {
             log.debug("The requested metadata is compliant with the policy");
             try {
+                log.trace("Attempting to parse the metadata {}", result.toJSONString());
                 return OIDCClientInformation.parse(result);
             } catch (final ParseException e) {
                 log.error("Could not parse the metadata object", e);
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index e37696b..a94a38f 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -70,6 +70,13 @@
         </constructor-arg>
     </bean>
 
+    <bean id="shibboleth.oidfed.ResolveEntityTrustChainMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+        <constructor-arg>
+            <bean p:cacheId="DefaultResolveEntityTrustChainMetadataCache" parent="shibboleth.oidfed.ResolveEntityTrustChainMetadataCacheBuilderSpec"
+                p:cleanupTaskInterval="PT30S"/>
+        </constructor-arg>
+    </bean>
+
     <bean id="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" parent="shibboleth.oidc.CacheBuilder">
         <constructor-arg>
             <bean p:cacheId="DefaultLocalTrustAnchorsMetadataCache" parent="shibboleth.oidfed.LocalTrustAnchorsMetadataCacheBuilderSpec"/>
@@ -206,6 +213,43 @@
         </property>
     </bean>
 
+    <bean id="shibboleth.oidfed.ResolveEntityTrustChainMetadataCacheBuilderSpec"
+        class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
+        p:minCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT1S}"
+        p:maxCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT30S}">
+        <property name="criteriaToIdentifierStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityRequestCriteriaToIdentifierStrategy" />
+        </property>
+        <property name="identifierExtractionStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityResponseIdentifierExtractionStrategy" />
+        </property>
+        <property name="metadataExpirationTimeStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityResponseContainerExpirationTimeStrategy"/>
+        </property>
+        <property name="metadataFilterStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityResponseSignatureValidationFilterStrategy"
+                p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache">
+                <property name="trustEngine">
+                    <bean class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
+                        <constructor-arg index="0">
+                            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityConfigurationCredentialResolver" />
+                        </constructor-arg>
+                        <constructor-arg index="1">
+                            <bean class="net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+        <property name="fetchStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityTrustChainFetchingStrategy"
+                p:httpClient="#{getObject('shibboleth.oidfed.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
+                p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}"
+                p:localTrustAnchorsCache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache"
+                p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+        </property>
+    </bean>
+
     <util:map id="shibboleth.oidfed.DefaultFederationPolicyConstraints"
         value-type="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint">
         <entry key="max_path_length">
@@ -519,5 +563,10 @@
             <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_metadata" c:_1="Requested metadata is not compliant with the merged policy" c:_2="400" />
         </property>
     </bean>
-        
+
+    <alias alias="UseResolverApiCondition" name="%{idp.oidfed.trustchain.resolver.useResolverApiCondition:shibboleth.Conditions.FALSE}" />
+    <alias alias="FallbackToLocalResolutionCondition" name="%{idp.oidfed.trustchain.resolver.fallbackToLocalCondition:shibboleth.Conditions.TRUE}" />
+
+    <import resource="${idp.home}/conf/oidfed/oidfed-trustchain-resolver.xml"/>
+
 </beans>
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
index e16681a..4a4cacc 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
@@ -14,6 +14,21 @@
         </property>
     </bean>
 
+    <bean id="TrustChainCandidatesExist" parent="shibboleth.Conditions.Expression"
+        c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)) and #input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)).getPolicyCompliantTrustChains() != null and #input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainCont [...]
+
+    <bean id="CallResolveEntityApi"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.CallResolveEntityApi"
+        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')}">
+        <property name="clientIDLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultMetadataLookupExtensionContextClientIDLookupFunction" />
+        </property>
+    </bean>
+
     <bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
         scope="prototype"
         p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
@@ -113,6 +128,10 @@
     <bean id="ValidateAutomaticRegistrationProfileConfiguration"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateAutomaticRegistrationProfileConfiguration"
         scope="prototype">
+        <property name="localMetadataPolicyLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.LocalMetadataPolicyLookupFunction"
+                p:relyingPartyContextLookupStrategy-ref="AutomaticRegistrationRelyingPartyCreationStrategy"/>
+        </property>
         <property name="localMetadataPolicyMergingStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultLocalMetadataPolicyMergingStrategy"
                 p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"/>
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
index ddbff40..51d6133 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
@@ -7,17 +7,38 @@
             <set name="conversationScope.automaticallyRegistered" value="false" />
         </on-entry>
         <if test="AutomaticRegistrationCondition.test(opensamlProfileRequestContext) and !opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))"
+            then="ChooseResolutionMethod" else="proceed" />
+    </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="CheckIfDoAutomaticRegistration">
+            <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="proceed" />
     </decision-state>
 
     <action-state id="ResolveTrustChains">
         <evaluate expression="ResolveTrustChains" />
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="CheckIfDoAutomaticRegistration" />
+        <transition on="proceed" to="CheckIfDoAutomaticRegistration">
+            <set name="flowScope.transitionForReselectTrustChain" value="'DoAutomaticRegistration'" />
+        </transition>
     </action-state>
 
     <decision-state id="CheckIfDoAutomaticRegistration">
-        <if test="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)) and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)).getPolicyCompliantTrustChains() != null and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth. [...]
+        <if test="TrustChainCandidatesExist.test(opensamlProfileRequestContext)"
             then="DoAutomaticRegistration" else="proceed" />
     </decision-state>
 
@@ -29,7 +50,7 @@
         <evaluate expression="ValidateAutomaticRegistrationProfileConfiguration" />
         <evaluate expression="InitializeRelyingPartyContext" />
         <evaluate expression="'proceed'" />
-        <transition on="ReselectTrustChain" to="DoAutomaticRegistration" />
+        <transition on="ReselectTrustChain" to="#{transitionForReselectTrustChain}" />
         <transition on="proceed" to="proceed">
             <set name="conversationScope.automaticallyRegistered" value="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))" />
         </transition>
@@ -38,10 +59,12 @@
     <end-state id="proceed"/>
     <end-state id="InvalidMetadataPolicy"/>
     <end-state id="InvalidMetadataAgainstPolicy"/>
+    <end-state id="HandleError"/>
 
     <global-transitions>
         <transition on="InvalidMetadataPolicy" to="InvalidMetadataPolicy" />
         <transition on="InvalidMetadataAgainstPolicy" to="InvalidMetadataAgainstPolicy" />
+        <transition on="HandleError" to="HandleError" />
     </global-transitions>
 
 
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index b62fda2..e3e0353 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -19,7 +19,22 @@
 
     <bean id="OIDFED.AutomaticRegistration" parent="AbstractOIDFederationProfile" lazy-init="true"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationAutomaticRegistrationProfileConfiguration"
-        p:mandatoryTrustMarks="%{idp.oidfed.automaticRegistration.mandatoryTrustMarks:}" />
+        p:mandatoryTrustMarks="%{idp.oidfed.automaticRegistration.mandatoryTrustMarks:}">
+        <property name="localMetadataPolicyLookupStrategy">
+            <bean parent="shibboleth.Functions.Constant">
+                <constructor-arg name="target">
+                    <util:map>
+                        <entry key="scope">
+                            <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="openid" />
+                        </entry>
+                        <entry key="token_endpoint_auth_method">
+                            <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="private_key_jwt" />
+                        </entry>
+                    </util:map>
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
 
     <bean id="OIDFED.ExplicitRegistration" parent="AbstractOIDFederationProfile" lazy-init="true"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationExplicitRegistrationProfileConfiguration"
@@ -63,13 +78,12 @@
     <bean id="shibboleth.oidfed.DefaultSecurityConfiguration"
         class="net.shibboleth.oidc.profile.config.JSONSecurityConfiguration" c:clockSkew="%{idp.policy.clockSkew:PT1M}">
         <constructor-arg name="idGenerator">
-                <bean 
-                    class="net.shibboleth.shared.security.IdentifierGenerationStrategy" factory-method="getInstance">
+            <bean class="net.shibboleth.shared.security.IdentifierGenerationStrategy" factory-method="getInstance">
                 <constructor-arg>
                     <util:constant
                         static-field="net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType.SECURE" />
                 </constructor-arg>
-                </bean>
+            </bean>
         </constructor-arg>
         <property name="jwtSignatureSigningConfiguration">
             <ref bean="#{'%{idp.oidfed.signing.config:shibboleth.oidfed.SigningConfiguration}'.trim()}" />
diff --git a/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-trustchain-resolver.xml b/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-trustchain-resolver.xml
new file mode 100644
index 0000000..8a26100
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-trustchain-resolver.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:context="http://www.springframework.org/schema/context"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <!--
+        The default function for fetching the map of trusted entities for trust chain resolution via resolve entity API.
+        The map is keyed with entityId of trusted entities whose federation_resolve_endpoint is used as the API endpoint.
+        The entity configuration is used as a trusted source for the endpoint. The map value is a list of trust anchors
+        that will be used for the trust_anchor paramter(s) in the API request.
+        
+        The default function may be overridden via 'shibboleth.oidfed.TrustedEntitiesLookupStrategy' bean that must
+        implement Function<ProfileRequestContext, Map<String,List<String>>>.
+    -->
+    <bean id="shibboleth.oidfed.DefaultTrustedEntitiesLookupStrategy" parent="shibboleth.Functions.Constant">
+        <constructor-arg name="target">
+            <util:map>
+                <!-- 
+                    Example entry for a trusted entity 'https://trust-anchor.federation.local'
+                    its 'federation_resolve_endpoint' is exploited with the configured trust_anchor parameters:
+                    '...&trust_anchor=https://trust-anchor.federation.local'
+                -->
+                <!--
+                <entry key="https://trust-anchor.federation.local">
+                    <util:list value-type="java.lang.String">
+                        <value>https://trust-anchor.federation.local</value>
+                    </util:list>
+                </entry>
+                -->
+            </util:map>
+        </constructor-arg>
+    </bean>
+
+</beans>
diff --git a/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/module.properties b/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/module.properties
index c7ded79..54d11a0 100644
--- a/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/module.properties
+++ b/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/module.properties
@@ -15,3 +15,7 @@ idp.oidc.OP.oidfed.1.replace = false
 idp.oidc.OP.oidfed.2.src =  /net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-entity-configuration-metadata.json
 idp.oidc.OP.oidfed.2.dest = conf/oidfed/oidfed-entity-configuration-metadata.json
 idp.oidc.OP.oidfed.2.replace = false
+
+idp.oidc.OP.oidfed.3.src =  /net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-trustchain-resolver.xml
+idp.oidc.OP.oidfed.3.dest = conf/oidfed/oidfed-trustchain-resolver.xml
+idp.oidc.OP.oidfed.3.replace = false
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 a4924d5..9d44226 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
@@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.net.URLEncoder;
 import java.nio.charset.Charset;
 import java.security.KeyPair;
 import java.security.NoSuchAlgorithmException;
@@ -91,6 +92,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     final String intermediateIdPattern = "https://intermediate-authority%s.federation.local";
     final String anchorId = "https://trust-anchor.federation.local";
     final String anchorFetchEndpoint = anchorId + "/fetch";
+    final String anchorResolveEndpoint = anchorId + "/resolve";
     String issuer = "https://op.example.org";
 
     JWK rpKey;
@@ -170,6 +172,17 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         return fetchEndpoint + "?sub=" + subject;
     }
 
+    protected String resolveEntityUrl(final String resolveEndpoint, final String subject,
+            final String... trustAnchors) {
+        final StringBuilder builder = new StringBuilder(resolveEndpoint + "?sub=" + 
+            URLEncoder.encode(subject, Charset.forName("UTF-8")));
+        for (final String trustAnchor : trustAnchors) {
+            builder.append("&trust_anchor=" + URLEncoder.encode(trustAnchor, Charset.forName("UTF-8")));
+        }
+        builder.append("&entity_type=openid_relying_party");
+        return builder.toString();
+    }
+
     protected void mapResponse(final String requestUri, final ClassicHttpResponse classicResponse) throws IOException {
         when(federationHttpClient.executeOpen(any(), argThat(new RequestUriMatcher(requestUri)), any()))
             .thenReturn(classicResponse);
@@ -271,7 +284,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(trustedAnchorKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Map.of("federation_fetch_endpoint",
-                        anchorFetchEndpoint)))
+                        anchorFetchEndpoint, "federation_resolve_endpoint", anchorResolveEndpoint)))
                 .build();
         final EntityStatement anchorConfiguration =
                 TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet);
@@ -351,7 +364,27 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, issuerKey, claimsSet);
         return rpConfiguration.getSignedStatement().serialize();
     }
-    
+
+    protected String rpResolveEntityResponse(final String clientId, final Map<String, Object> metadata) {
+        final List<String> trustChain;
+        try {
+            trustChain = List.of(rpEntityConfiguration(clientId),
+                    subordinateStatement(clientId, Map.of("openid_relying_party",
+                            new OIDCClientMetadata().toJSONObject())),
+                    trustedAnchorConfiguration());
+        } catch (URISyntaxException e) {
+            Assert.fail("Could not build URI", e);
+            return null;
+        }
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(clientId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("trust_chain", trustChain)
+                .claim("metadata", metadata)
+                .build();
+        return TrustChainTestUtil.signedJwt(JWSAlgorithm.RS256, trustedAnchorKey, "application/resolve-response+jwt",
+                claimsSet).serialize();
+    }
     protected String uniqueClientId() {
         return String.format(clientIdPattern, clientIndex.getAndIncrement());
     }
@@ -420,6 +453,20 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         }
     }
 
+    protected void rpResolveEntityConfigureMockHttpClient(final String clientId) {
+        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) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+    }
+
     @SuppressWarnings("unchecked")
     protected void rpConfigureMockHttpClient(final String clientId, final Map<String, Object> testVector) {
         final String intermediateId = uniqueIntermediateId();
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 9c4d31c..c5ea9e0 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,6 +35,7 @@ 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;
@@ -58,6 +59,9 @@ 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;
@@ -65,6 +69,11 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public AuthorizeFlowAutomaticRegistrationTest() {
         super(AuthorizeFlowTest.FLOW_ID);
     }
+    
+    @BeforeMethod
+    public void removeHeaders() {
+        request.removeHeader(USE_CUSTOM_RESOLVER_API_CONDITION);
+    }
 
     @Test
     public void testWithValidTrustChain_noRequestObject()
@@ -116,6 +125,95 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
     }
 
+    @Test
+    public void testWithValidTrustChain_resolveApi_signedRequestObject()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        rpResolveEntityConfigureMockHttpClient(clientId);
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
+    }
+
+    @Test
+    public void testWithValidTrustChain_resolveApiFailsNoFaildback_signedRequestObject()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        rpConfigureMockHttpClient(clientId);
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+    }
+
+    @Test
+    public void testWithValidTrustChain_resolveApiFailsFailbackFails_signedRequestObject()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        request.addHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION, "true");
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+    }
+
+    @Test
+    public void testWithValidTrustChain_resolveApiFailsFailbackSuccess_signedRequestObject()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        rpConfigureMockHttpClient(clientId);
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        request.addHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION, "true");
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
+    }
+
     @Test
     public void testWithInvalidTrustChain_signedRequestObject_unmatchingRpEntityConfigurationSignature()
             throws IOException, UnsupportedOperationException, URISyntaxException {
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
index e11b63a..1589b62 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
@@ -89,4 +89,12 @@
         <constructor-arg value="#{T(org.apache.hc.client5.http.classic.HttpClient)}" />
     </bean>
 
+    <bean id="HeaderUseResolveApiCondition" parent="shibboleth.Conditions.Expression"
+        p:customObject-ref="shibboleth.HttpServletRequestSupplier"
+        c:expression="'true'.equals(#custom.get().getHeader('useCustomResolverApi'))" />
+
+    <bean id="HeaderFallbackToLocalCondition" parent="shibboleth.Conditions.Expression"
+        p:customObject-ref="shibboleth.HttpServletRequestSupplier"
+        c:expression="'true'.equals(#custom.get().getHeader('useCustomFallbackToLocal'))" />
+
 </beans>
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
index b996d9f..d663f4f 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
@@ -37,4 +37,6 @@ idp.authn.OAuth2Client.audit.format = %a|%T|%SP|%I|%s|%AF|%CV|%u|%tu|%AR|%UA|%is
 idp.oidfed.authorize.automaticRegistrationCondition = shibboleth.Conditions.TRUE
 idp.oidfed.par.automaticRegistrationCondition = shibboleth.Conditions.TRUE
 idp.oidfed.configuration.resolver.values = CustomEntityConfigurationValues
-idp.oidfed.configuration.MetadataSkaletonFile = src/test/resources/net/shibboleth/idp/module/conf/oidfed-entity-configuration-metadata.json
\ No newline at end of file
+idp.oidfed.configuration.MetadataSkaletonFile = src/test/resources/net/shibboleth/idp/module/conf/oidfed-entity-configuration-metadata.json
+idp.oidfed.trustchain.resolver.useResolverApiCondition = HeaderUseResolveApiCondition
+idp.oidfed.trustchain.resolver.fallbackToLocalCondition = HeaderFallbackToLocalCondition
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trustchain-resolver.xml b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trustchain-resolver.xml
new file mode 100644
index 0000000..e831bef
--- /dev/null
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trustchain-resolver.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:context="http://www.springframework.org/schema/context"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <bean id="shibboleth.oidfed.DefaultTrustedEntitiesLookupStrategy" parent="shibboleth.Functions.Constant">
+        <constructor-arg name="target">
+            <util:map>
+                <entry key="https://trust-anchor.federation.local">
+                    <util:list value-type="java.lang.String">
+                        <value>https://trust-anchor.federation.local</value>
+                    </util:list>
+                </entry>
+            </util:map>
+        </constructor-arg>
+    </bean>
+
+</beans>
\ No newline at end of file

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


More information about the commits mailing list