[java-oidfed-common] 03/05: Initial version of the 'oidfed/resolve-trust-chains' flow

Codeberg noreply at shibboleth.net
Fri May 22 10:52:50 UTC 2026


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

codeberg pushed a commit to branch main
in repository java-oidfed-common.

View the commit online:
https://codeberg.org/Shibboleth/java-oidfed-common/commit/9d1ae1d6788c0b766d08122b7fa617ce449331ad

commit 9d1ae1d6788c0b766d08122b7fa617ce449331ad
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri May 22 12:15:37 2026 +0300

    Initial version of the 'oidfed/resolve-trust-chains' flow
    
    - Imported and refactored required webflow actions and context from java-idp-plugin-oidc-op-oidfed
      - API: net.shibboleth.oidfed.profile.context
      - IMPL: net.shibboleth.oidfed.profile.impl: CallResolveEntityApi and ResolveTrustChains
        - Referred predicates/functions in net.shibboleth.oidfed.profile.navigate
---
 .../context/RelyingPartyTrustChainContext.java     | 280 +++++++++++
 .../oidfed/profile/context/VerifiedTrustChain.java |  76 +++
 .../resolve-trust-chains-beans.xml                 |  70 +++
 .../resolve-trust-chains-flow.xml                  |  52 ++
 .../impl/AbstractTrustChainResolutionAction.java   | 388 +++++++++++++++
 .../oidfed/profile/impl/CallResolveEntityApi.java  | 534 +++++++++++++++++++++
 .../oidfed/profile/impl/ResolveTrustChains.java    | 263 ++++++++++
 ...mbinedMetadataFromTrustChainLookupStrategy.java |  86 ++++
 .../DefaultMetadataValidationCondition.java        |  82 ++++
 .../DefaultTrustChainIDsLookupStrategy.java        |  43 ++
 ...ultTrustChainMetadataPolicyMergingStrategy.java | 145 ++++++
 11 files changed, 2019 insertions(+)

diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyTrustChainContext.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyTrustChainContext.java
new file mode 100644
index 0000000..5329012
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyTrustChainContext.java
@@ -0,0 +1,280 @@
+/*
+ * 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.oidfed.profile.context;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.profile.TrustedRemoteResolverEntity;
+import net.shibboleth.shared.annotation.constraint.Live;
+
+/**
+ * Subcontext carrying information for trust chains related to a relying party.
+ */
+public final class RelyingPartyTrustChainContext extends BaseContext {
+
+    /** Trust chain provided within the request. */
+    @Nullable private List<EntityStatement<?>> providedTrustChain;
+
+    /** All resolved trust chains for the relying party. */
+    @Nullable private List<List<EntityStatement<?>>> resolvedTrustChains;
+
+    /** Policy-compliant trust chains for the relying party. */
+    @Nullable private List<VerifiedTrustChain> policyCompliantTrustChains;
+
+    /** Selected trust chain for the relying party. */
+    @Nullable private VerifiedTrustChain selectedTrustChain;
+
+    /** Expiration instant for the selected metadata. */
+    @Nullable private Instant selectedMetadataExpiration;
+
+    /** Verified trust mark IDs for the selected trust chain. */
+    @Nullable private Map<String, List<String>> verifiedTrustMarkIds;
+
+    /** Verified trust marks for the selected trust chain. */
+    @Nullable private Map<String, List<SignedJWT>> verifiedTrustMarks;
+
+    /** Verified trust mark issuers. */
+    @Nonnull @Live private Map<String, EntityStatement<?>> verifiedTrustMarkIssuers;
+
+    /** All previously selected but rejected trust chains. */
+    @Nullable private List<List<EntityStatement<?>>> rejectedTrustChains;
+
+    /** All already attempted trusted remote resolver entities. */
+    @Nullable private List<TrustedRemoteResolverEntity> attemptedTrustedRemoteResolverEntities;
+
+    /**
+     * Constructor.
+     */
+    public RelyingPartyTrustChainContext() {
+        verifiedTrustMarkIssuers = new HashMap<>();
+    }
+
+    /**
+     * Get the trust chain provided within the request.
+     * 
+     * @return provided trust chain
+     */
+    @Nullable public List<EntityStatement<?>> getProvidedTrustChain() {
+        return providedTrustChain;
+    }
+
+    /**
+     * Set the trust chain provided within the request.
+     * 
+     * @param trustChain provided trust chain
+     * 
+     * @return this context
+     */
+    @Nonnull public RelyingPartyTrustChainContext setProvidedTrustChain(
+            @Nullable final List<EntityStatement<?>> trustChain) {
+        providedTrustChain = trustChain;
+        return this;
+    }
+
+    /**
+     * Get the resolved trust chains for the relying party.
+     * 
+     * @return the trust chains
+     */
+    @Nullable public List<List<EntityStatement<?>>> getResolvedTrustChains() {
+        return resolvedTrustChains;
+    }
+
+    /**
+     * Set the resolved trust chains for the relying party.
+     * 
+     * @param trustChains the trust chains
+     * 
+     * @return this context
+     */
+    @Nonnull public RelyingPartyTrustChainContext setResolvedTrustChains(
+            @Nullable final List<List<EntityStatement<?>>> trustChains) {
+        resolvedTrustChains = trustChains;
+        return this;
+    }
+
+    /**
+     * Get the policy-compliant trust chains for the relying party.
+     * 
+     * @return the trust chains
+     */
+    @Nullable public List<VerifiedTrustChain> getPolicyCompliantTrustChains() {
+        return policyCompliantTrustChains;
+    }
+
+    /**
+     * Set the policy-compliant trust chains for the relying party.
+     * 
+     * @param chains the trust chains and client informations
+     * 
+     * @return this context
+     */
+    @Nonnull public RelyingPartyTrustChainContext setPolicyCompliantTrustChains(
+            @Nullable final List<VerifiedTrustChain> chains) {
+        policyCompliantTrustChains = chains;
+        return this;
+    }
+
+    /**
+     * Get the selected trust chain for the relying party.
+     * 
+     * @return the trust chain
+     */
+    @Nullable public VerifiedTrustChain getSelectedTrustChain() {
+        return selectedTrustChain;
+    }
+
+    /**
+     * Set the selected trust chain for the relying party.
+     * 
+     * @param chain the selected trust chain
+     * 
+     * @return this context
+     */
+    @Nonnull public RelyingPartyTrustChainContext setSelectedTrustChains(
+            @Nullable final VerifiedTrustChain chain) {
+        selectedTrustChain = chain;
+        return this;
+    }
+
+    /**
+     * Get the expiration instant for the selected metadata.
+     * 
+     * @return the expiration instant
+     */
+    @Nullable public Instant getSelectedMetadataExpiration() {
+        return selectedMetadataExpiration;
+    }
+
+    /**
+     * Set the expiration instant for the selected metadata.
+     * 
+     * @param expiration the expiration instant
+     * 
+     * @return this context
+     */
+    @Nonnull public RelyingPartyTrustChainContext setSelectedMetadataExpiration(@Nullable final Instant expiration) {
+        selectedMetadataExpiration = expiration;
+        return this;
+    }
+
+    /**
+     * Get the verified trust mark IDs for the selected trust chain.
+     * 
+     * @return verified trust mark IDs
+     */
+    @Nullable public Map<String, List<String>> getVerifiedTrustMarkIds() {
+        return verifiedTrustMarkIds;
+    }
+
+    /**
+     * Set the verified trust mark IDs for the selected trust chain.
+     * 
+     * @param ids verified trust mark IDs
+     * 
+     * @return this context
+     */
+    @Nonnull public RelyingPartyTrustChainContext setVerifiedTrustMarkIds(
+            @Nullable final Map<String, List<String>> ids) {
+        verifiedTrustMarkIds = ids;
+        return this;
+    }
+
+    /**
+     * Get the verified trust marks for the selected trust chain.
+     * 
+     * @return verified trust marks
+     */
+    @Nullable public Map<String, List<SignedJWT>> getVerifiedTrustMarks() {
+        return verifiedTrustMarks;
+    }
+
+    /**
+     * Set the verified trust marks for the selected trust chain.
+     * 
+     * @param trustMarks verified trust marks
+     * 
+     * @return this context
+     */
+    @Nonnull public RelyingPartyTrustChainContext setVerifiedTrustMarks(
+            @Nullable final Map<String, List<SignedJWT>> trustMarks) {
+        verifiedTrustMarks = trustMarks;
+        return this;
+    }
+
+    /**
+     * Get the verified trust mark issuers.
+     * 
+     * @return verified trust mark issuers
+     */
+    @Nonnull @Live public Map<String, EntityStatement<?>> getVerifiedTrustMarkIssuers() {
+        return verifiedTrustMarkIssuers;
+    }
+
+    /**
+     * Get the previously selected but rejected trust chains for the relying party.
+     * 
+     * @return the trust chains
+     */
+    @Nullable public List<List<EntityStatement<?>>> getRejectedTrustChains() {
+        return rejectedTrustChains;
+    }
+
+    /**
+     * Set the previously selected but rejected trust chains for the relying party.
+     * 
+     * @param trustChains the trust chains
+     * 
+     * @return this context
+     */
+    @Nonnull public RelyingPartyTrustChainContext setRejectedTrustChains(
+            @Nullable final List<List<EntityStatement<?>>> trustChains) {
+        rejectedTrustChains = trustChains;
+        return this;
+    }
+
+    /**
+     * Get the attempted remote resolver entities for the relying party.
+     * 
+     * @return the trust chains
+     */
+    @Nullable public List<TrustedRemoteResolverEntity> getAttemptedTrustedRemoteResolverEntities() {
+        return attemptedTrustedRemoteResolverEntities;
+    }
+
+    /**
+     * Set the attempted remote resolver entities for the relying party.
+     * 
+     * @param attemptedEntities the attempted remote resolver entities
+     * 
+     * @return this context
+     */
+    @Nonnull public RelyingPartyTrustChainContext setAttemptedTrustedRemoteResolverEntities(
+            @Nullable final List<TrustedRemoteResolverEntity> attemptedEntities) {
+        attemptedTrustedRemoteResolverEntities = attemptedEntities;
+        return this;
+    }
+}
\ No newline at end of file
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/VerifiedTrustChain.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/VerifiedTrustChain.java
new file mode 100644
index 0000000..3562109
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/VerifiedTrustChain.java
@@ -0,0 +1,76 @@
+/*
+ * 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.oidfed.profile.context;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A wrapper for a verified trust chain containing the trust chain and its policy-enforced metadata. The trust chain
+ * length is verified to equal to three or more.
+ */
+public class VerifiedTrustChain {
+
+    /** Verified trust chain. */
+    @Nonnull @NotEmpty private final List<EntityStatement<?>> trustChain;
+
+    /** Policy-enforced metadata. */
+    @Nonnull private Metadata metadata;
+
+    /**
+     * Constructor.
+     *
+     * @param chain verified trust chain
+     * @param data policy-enforced metadata
+     */
+    public VerifiedTrustChain(@Nonnull @NotEmpty final List<EntityStatement<?>> chain, @Nonnull final Metadata data) {
+        trustChain = Constraint.isNotNull(chain, "Trust chain cannot be null");
+        Constraint.isTrue(chain.size() > 2, "Trust chain cannot be shorter than three");
+        metadata = Constraint.isNotNull(data, "Metadata cannot be null");
+    }
+
+    /**
+     * Get the verified trust chain.
+     * 
+     * @return verified trust chain
+     */
+    @Nonnull @NotEmpty public List<EntityStatement<?>> getTrustChain() {
+        return trustChain;
+    }
+
+    /**
+     * Get the policy-enforced metadata.
+     * 
+     * @return policy-enforced metadata
+     */
+    @Nonnull public Metadata getMetadata() {
+        return metadata;
+    }
+
+    /**
+     * Set the policy-enforced metadata.
+     * 
+     * @param data policy-enforced metadata
+     */
+    public void setMetadata(@Nonnull Metadata data) {
+        metadata = Constraint.isNotNull(data, "Metadata cannot be null");
+    }
+}
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-beans.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-beans.xml
new file mode 100644
index 0000000..cc5e1ab
--- /dev/null
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-beans.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" 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="CallResolveEntityApi"
+        class="net.shibboleth.oidfed.profile.impl.CallResolveEntityApi"
+        p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
+        p:resolveEntityTrustChainMetadataCache-ref="shibboleth.oidfed.ResolveEntityTrustChainMetadataCache"
+        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"
+        p:trustedEntitiesLookupStrategy="#{getObject('shibboleth.oidfed.TrustedRemoteResolverEntitiesLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultTrustedRemoteResolverEntitiesLookupStrategy')}"
+        p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
+        p:entityIDLookupStrategy="#{getObject('shibboleth.oidfed.trust-chain-resolver.EntityIDLookupStrategy') ?: getObject('shibboleth.oidfed.shibboleth.oidfed.trust-chain-resolver.DefaultEntityIDLookupStrategy')}"/>
+
+    <bean id="ResolveTrustChains" class="net.shibboleth.oidfed.profile.impl.ResolveTrustChains"
+        scope="prototype"
+        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
+        p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
+        p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
+        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"
+        p:metadataValidationCondition-ref="#{'%{idp.oidfed.MetadataValidationCondition:DefaultMetadataValidationCondition}'.trim()}"
+        p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}"
+        p:entityIDLookupStrategy="#{getObject('shibboleth.oidfed.trust-chain-resolver.EntityIDLookupStrategy') ?: getObject('shibboleth.oidfed.shibboleth.oidfed.trust-chain-resolver.DefaultEntityIDLookupStrategy')}">
+        <property name="metadataLookupStrategy">
+            <bean class="net.shibboleth.oidfed.profile.navigate.DefaultCombinedMetadataFromTrustChainLookupStrategy" />
+        </property>
+    </bean>
+
+    <bean id="DefaultMetadataValidationCondition"
+        class="net.shibboleth.oidfed.profile.navigate.DefaultMetadataValidationCondition" />
+
+    <bean id="DefaultMetadataPolicyEnforcer"
+        class="net.shibboleth.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEnforcer"
+        p:metadataPolicyOperators-ref="#{'%{idp.oidfed.authorize.MetadataPolicyOperatorsy:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
+
+    <bean id="DefaultTrustChainMetadataPolicyMergingStrategy" 
+        class="net.shibboleth.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
+        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.MetadataPolicMergingyStrategy:MetadataPolicMergingyStrategy}'.trim()}"
+        p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.authorize.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"/>
+
+    <bean id="MetadataPolicMergingyStrategy"
+        class="net.shibboleth.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyMergingStrategy"
+        p:metadataPolicyOperators-ref="#{'%{idp.oidfed.authorize.MetadataPolicyOperatorsy:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
+
+    <bean id="DefaultLocalMetadataPolicyStrategy"
+        parent="shibboleth.Functions.Constant">
+        <constructor-arg name="target">
+            <util:map>
+                <entry key="scope">
+                    <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy">
+                        <property name="defaultValue">
+                            <util:list value-type="java.lang.String">
+                                <value>openid</value>
+                            </util:list>
+                        </property>
+                    </bean>
+                </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>
+
+</beans>
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-flow.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-flow.xml
new file mode 100644
index 0000000..0776003
--- /dev/null
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-flow.xml
@@ -0,0 +1,52 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    abstract="true">
+
+    <decision-state id="ChooseResolutionMethod">
+        <on-entry>
+            <evaluate expression="flowRequestContext.getFlowScope().get('transitionAfterTrustChainResolution') != null ? flowRequestContext.getFlowScope().get('transitionAfterTrustChainResolution') : 'BuildResponseMessage'" result="flowScope.transitionAfterTrustChainResolution"/>
+            <evaluate expression="flowRequestContext.getFlowScope().get('transitionOnNoTrustChainsResolved') != null ? flowRequestContext.getFlowScope().get('transitionAfterTrustChainResolution') : 'NoTrustChainsResolved'" result="flowScope.transitionOnNoTrustChainsResolved"/>
+        </on-entry>
+        <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="#{transitionAfterTrustChainResolution}">
+            <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="#{transitionOnNoTrustChainsResolved}" />
+    </decision-state>
+
+    <action-state id="ResolveTrustChains">
+        <evaluate expression="ResolveTrustChains" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="#{transitionAfterTrustChainResolution}"/>
+    </action-state>
+
+    <end-state id="proceed"/>
+    <end-state id="InvalidMessageContext"/>
+    <end-state id="InvalidMetadataPolicy"/>
+    <end-state id="InvalidMetadataAgainstPolicy"/>
+    <end-state id="NoTrustChainsResolved" />
+    <end-state id="InvalidTrustChainAgainstConstraints" />
+
+    <global-transitions>
+        <transition on="InvalidMessageContext" to="InvalidMessageContext" />
+        <transition on="InvalidMetadataPolicy" to="InvalidMetadataPolicy" />
+        <transition on="InvalidMetadataAgainstPolicy" to="InvalidMetadataAgainstPolicy" />
+        <transition on="NoTrustChainsResolved" to="#{transitionOnNoTrustChainsResolved}" />
+        <transition on="InvalidTrustChainAgainstConstraints" to="InvalidTrustChainAgainstConstraints" />
+    </global-transitions>
+
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-beans.xml" />
+
+</flow>
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/AbstractTrustChainResolutionAction.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/AbstractTrustChainResolutionAction.java
new file mode 100644
index 0000000..50856dd
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/AbstractTrustChainResolutionAction.java
@@ -0,0 +1,388 @@
+/*
+ * 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.oidfed.profile.impl;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+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 net.minidev.json.JSONObject;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.constraints.FederationPolicyConstraint;
+import net.shibboleth.oidfed.metadata.constraints.FederationPolicyConstraintHelper;
+import net.shibboleth.oidfed.metadata.impl.EntityConfigurationImpl;
+import net.shibboleth.oidfed.metadata.payload.claim.impl.MetadataImpl;
+import net.shibboleth.oidfed.metadata.payload.impl.EntityConfigurationPayloadImpl;
+import net.shibboleth.oidfed.metadata.policy.FederationMetadataPolicyHelper;
+import net.shibboleth.oidfed.profile.OidFederationEventIds;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidfed.profile.context.VerifiedTrustChain;
+import net.shibboleth.shared.annotation.constraint.Live;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+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.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+
+/**
+ * Base action for actions initializing {@link RelyingPartyTrustChainContext} and performing metadata and metadata
+ * policy related operations.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(AbstractTrustChainResolutionAction.class);
+
+    /** Strategy used to create the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextCreationStrategy;
+
+    /** Strategy used to get combined entity metadata from trust chain. */
+    @NonnullAfterInit private Function<List<EntityStatement<?>>,Map<String,Map<String,Object>>> metadataLookupStrategy;
+
+    /** Strategy used to merge metadata policies in trust chain for specific entity type. */
+    @NonnullAfterInit private BiFunction<List<EntityStatement<?>>,String,Map<String, MetadataPolicy>>
+        metadataPolicyMergingStrategy;
+
+    /** Enforcer function for applying metadata policy for an item. */
+    @NonnullAfterInit private BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> metadataPolicyEnforcer;
+
+    /** Condition used to validate metadata for an entity type. */
+    @NonnullAfterInit private BiPredicate<String, Map<String, Object>> metadataValidationCondition;
+
+    /** List of claim names who are transformed from a space-separated String into a List. */
+    @Nonnull private List<String> arraysAsSpaceSeparatedList;
+
+    /** Map of supported federation policy constraints. */
+    @NonnullAfterInit private Map<String, FederationPolicyConstraint> federationPolicyConstraints;
+
+    /**
+     * Constructor.
+     */
+    public AbstractTrustChainResolutionAction() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tccs =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class, true).compose(
+                        new InboundMessageContextLookup());
+        assert tccs != null;
+        trustChainContextCreationStrategy = tccs;
+        arraysAsSpaceSeparatedList = CollectionSupport.listOf("scope");
+    }
+
+    /**
+     * 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");
+    }
+
+    /**
+     * Get the strategy used to create the trust chain context.
+     * 
+     * @return creation strategy
+     */
+    @Nonnull
+    public Function<ProfileRequestContext, RelyingPartyTrustChainContext> getTrustChainContextCreationStrategy() {
+        checkComponentActive();
+        return trustChainContextCreationStrategy;
+    }
+
+    /**
+     * Set the strategy used to get combined entity metadata from trust chain.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setMetadataLookupStrategy(
+            @Nonnull final Function<List<EntityStatement<?>>,Map<String,Map<String,Object>>> strategy) {
+        checkSetterPreconditions();
+        metadataLookupStrategy =
+                Constraint.isNotNull(strategy, "MetadataLookupStrategy cannot be null");
+    }
+
+    /**
+     * Get the strategy used to get combined entity metadata from trust chain.
+     * 
+     * @return lookup strategy
+     */
+    @NonnullAfterInit
+    public Function<List<EntityStatement<?>>,Map<String,Map<String,Object>>> getMetadataLookupStrategy() {
+        checkComponentActive();
+        return metadataLookupStrategy;
+    }
+
+    /**
+     * Set the strategy used to merge metadata policies in trust chain for specific entity type.
+     * 
+     * @param strategy merging strategy
+     */
+    public void setMetadataPolicyMergingStrategy(@Nonnull final
+            BiFunction<List<EntityStatement<?>>,String,Map<String, MetadataPolicy>> strategy) {
+        checkSetterPreconditions();
+        metadataPolicyMergingStrategy =
+                Constraint.isNotNull(strategy, "MetadataPolicyMergingStrategy cannot be null");
+    }
+
+    /**
+     * Get the strategy used to merge metadata policies in trust chain for specific entity type.
+     * 
+     * @return merging strategy
+     */
+    @Nonnull
+    public BiFunction<List<EntityStatement<?>>,String,Map<String, MetadataPolicy>> getMetadataPolicyMergingStrategy() {
+        checkComponentActive();
+        assert metadataPolicyMergingStrategy != null;
+        return metadataPolicyMergingStrategy;
+    }
+
+    /**
+     * Set the enforcer function for applying metadata policy for an item.
+     * 
+     * @param enforcer policy enforcer
+     */
+    public void setMetadataPolicyEnforcer(
+            @Nonnull final BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> enforcer) {
+        checkSetterPreconditions();
+        metadataPolicyEnforcer = Constraint.isNotNull(enforcer, "Metadata policy enforcer cannot be null");
+    }
+
+    /**
+     * Get the enforcer function for applying metadata policy for an item.
+     * 
+     * @return policy enforcer
+     */
+    @Nonnull public BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> getMetadataPolicyEnforcer() {
+        checkComponentActive();
+        assert metadataPolicyEnforcer != null;
+        return metadataPolicyEnforcer;
+    }
+
+    /**
+     * Set the list of claim names who are transformed from a space-separated String into a List.
+     * 
+     * @param list list of claim names
+     */
+    public void setArraysAsSpaceSeparatedList(@Nonnull final List<String> list) {
+        checkSetterPreconditions();
+        arraysAsSpaceSeparatedList = Constraint.isNotNull(list, "ArraysAsSpaceSeparatedList cannot be null");
+    }
+
+    /**
+     * Get the list of claim names who are transformed from a space-separated String into a List.
+     * 
+     * @return list of claim names
+     */
+    @Nonnull public List<String> getArraysAsSpaceSeparatedList() {
+        checkComponentActive();
+        return arraysAsSpaceSeparatedList;
+    }
+
+    /**
+     * Set the condition used to validate metadata for an entity type.
+     * 
+     * @param condition validation condition
+     */
+    public void setMetadataValidationCondition(@Nonnull final BiPredicate<String,Map<String,Object>> condition) {
+        checkSetterPreconditions();
+        metadataValidationCondition = Constraint.isNotNull(condition, "MetadataValidationCondition cannot be null");
+    }
+
+    /**
+     * Set the map of supported federation policy constraints.
+     * 
+     * @param constraints map of supported federation policy constraints.
+     */
+    public void setFederationPolicyConstraints(@Nonnull final Map<String, FederationPolicyConstraint> constraints) {
+        checkSetterPreconditions();
+        federationPolicyConstraints = Constraint.isNotNull(constraints, "Map of policy constraints cannot be null");
+    }
+
+    /**
+     * Get the map of supported federation policy constraints.
+     * 
+     * @return map of supported federation policy constraints.
+     */
+    @Nonnull public Map<String, FederationPolicyConstraint> getFederationPolicyConstraints() {
+        checkComponentActive();
+        assert federationPolicyConstraints != null;
+        return federationPolicyConstraints;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (metadataLookupStrategy == null) {
+            throw new ComponentInitializationException("MetadataLookupStrategy cannot be null");
+        }
+        if (metadataPolicyMergingStrategy == null) {
+            throw new ComponentInitializationException("MetadataPolicyMergingStrategy cannot be null");
+        }
+        if (metadataPolicyEnforcer == null) {
+            throw new ComponentInitializationException("MetadataPolicyEnforcer cannot be null");
+        }
+        if (metadataValidationCondition == null) {
+            throw new ComponentInitializationException("MetadataValidationCondition cannot be null");
+        }
+        if (federationPolicyConstraints == null) {
+            throw new ComponentInitializationException("Map of policy constraints cannot be null");
+        }
+    }
+
+    /**
+     * Populates the given policy compliant trust chains with the given trust chain if its metadata is policy compliant.
+     * 
+     * @param candidate the trust chain to be evaluated
+     * @param policyCompliantChains the list of policy-compliant trust chains to be populated
+     * @return error event ID if metadata policy merging or enforcement failed, null otherwise
+     */
+    @Nullable protected String populatePolicyComplaintChains(@Nonnull final List<EntityStatement<?>> candidate,
+            @Nonnull @Live List<VerifiedTrustChain> policyCompliantChains) {
+        final Map<String,Map<String,Object>> candidateMetadata = getMetadataLookupStrategy().apply(candidate);
+        log.trace("{} Metadata resolved via lookup strategy: {}", getLogPrefix(), candidateMetadata);
+        if (candidateMetadata != null) {
+            final List<EntityStatement<?>> chain = new ArrayList<>(candidate);
+            final EntityConfiguration updatedLeaf = updateEntityConfiguration(candidate.get(0), candidateMetadata);
+            if (updatedLeaf == null) {
+                log.error("Could not update the leaf entity configuration for {}", candidate.get(0).getSubject());
+                return EventIds.INVALID_MSG_CTX;
+            }
+            chain.set(0, updatedLeaf);
+            if (!FederationPolicyConstraintHelper.verifyPolicyConstraints(
+                    chain, getFederationPolicyConstraints())) {
+                return OidFederationEventIds.INVALID_TRUST_CHAIN_AGAINST_CONSTRAINTS;
+            }
+            final Map<String,Map<String,Object>> metadata =
+                    Optional.ofNullable(getMetadataLookupStrategy().apply(chain)).orElse(CollectionSupport.emptyMap());
+            log.trace("{} Constrained metadata resolved via lookup strategy: {}", getLogPrefix(), metadata);
+            final Map<String,Map<String,Object>> verifiedMetadata = new HashMap<>();
+            for (final String entityType : metadata.keySet()) {
+                final Map<String, MetadataPolicy> mergedPolicies;
+                try {
+                    mergedPolicies = getMetadataPolicyMergingStrategy().apply(chain, entityType);
+                    log.debug("{} Merged policy for type {} for chain {}", getLogPrefix(), entityType, mergedPolicies);
+                } catch (final ConstraintViolationException e) {
+                    log.warn("{} Could not merge metadata policies", getLogPrefix(), e);
+                    return OidFederationEventIds.INVALID_METADATA_POLICY;
+                }
+
+                final JSONObject requestMetadata = new JSONObject(metadata.get(entityType));
+                for (final String claim : mergedPolicies.keySet()) {
+                    assert claim != null;
+                    final MetadataPolicy policy = mergedPolicies.get(claim);
+                    try {
+                        final Object enforcedValue = enforceValue(claim, requestMetadata.get(claim), policy);
+                        if (enforcedValue != null) {
+                            requestMetadata.put(claim, enforcedValue);
+                        } else {
+                            log.debug("{} Enforcer returned null for entity {} claim {}", getLogPrefix(), entityType,
+                                    claim);
+                        }
+                    } catch (final ConstraintViolationException e) {
+                        log.warn("{} The requested metadata is not compliant with the policy", getLogPrefix());
+                        return OidFederationEventIds.INVALID_METADATA_AGAINST_POLICY;
+                    }
+                }
+
+                log.debug("{} The requested metadata is compliant with the policy", getLogPrefix());
+                if (metadataValidationCondition.test(entityType, requestMetadata)) {
+                    verifiedMetadata.put(entityType, requestMetadata);
+                    log.debug("{} Policy-enforced metadata {}", getLogPrefix(), requestMetadata.toJSONString());
+                } else {
+                    log.warn("{} Metadata validation failed for {} for entity type {}", getLogPrefix(),
+                            chain.get(0).getSubject(), entityType);
+                }
+            }
+            policyCompliantChains.add(new VerifiedTrustChain(chain, new MetadataImpl(verifiedMetadata)));
+            log.debug("{} Policy-enforced metadata {}", getLogPrefix(), verifiedMetadata);
+            return null;
+        }
+        return EventIds.INVALID_MSG_CTX;
+    }
+
+    /**
+     * Enforces the given value with the given metadata policy.
+     * 
+     * @param claim name of the claim to be enforced
+     * @param value value of the claim
+     * @param policy the metadata policy to be used for enforcing
+     * @return the enforced value
+     * @throws ConstraintViolationException if the operation was not successful
+     */
+    @Nullable protected Object enforceValue(@Nonnull final String claim, @Nullable final Object value,
+            @Nullable final MetadataPolicy policy) throws ConstraintViolationException {
+        log.debug("{} Claim {} set in policy included in the request: {}", getLogPrefix(), claim,
+                value == null);
+        final Object enforcerInput = FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
+                arraysAsSpaceSeparatedList, claim, value);
+
+        final Pair<Object,Boolean> mergeResult = getMetadataPolicyEnforcer().apply(enforcerInput, policy);
+        final Boolean enforcerResult = mergeResult != null ? mergeResult.getSecond() : null;
+        if (enforcerResult == null || !enforcerResult.booleanValue()) {
+            throw new ConstraintViolationException("Metadata claim " + claim + " is not compliant with the policy");
+        }
+        log.trace("{} Validation result is OK for claim {}", getLogPrefix(), claim);
+        return Optional.ofNullable(mergeResult)
+                .map(pair -> pair.getFirst())
+                .map(result -> FederationMetadataPolicyHelper.transformListIntoSpaceSeparatedString(
+                        arraysAsSpaceSeparatedList,claim, result))
+                .orElse(null);
+    }
+
+    /**
+     * Updates the given entity configuration to contain given metadata.
+     * 
+     * @param entityStatement entity configuration source
+     * @param metadata metadata to be included in the updated entity configuration
+     * @return updated entity configuration or null if the source was unexpected
+     */
+    @Nullable protected EntityConfiguration updateEntityConfiguration(
+            @Nullable final EntityStatement<?> entityStatement,
+            @Nonnull final Map<String, Map<String, Object>> metadata) {
+        if (entityStatement instanceof EntityConfiguration entityConfiguration) {
+            final EntityConfigurationPayloadImpl payload =
+                    new EntityConfigurationPayloadImpl(entityConfiguration.getParsedPayload());
+            payload.setMetadata(new MetadataImpl(metadata));
+            return new EntityConfigurationImpl(entityStatement.getJwt(), payload);
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/CallResolveEntityApi.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/CallResolveEntityApi.java
new file mode 100644
index 0000000..fd49db6
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/CallResolveEntityApi.java
@@ -0,0 +1,534 @@
+/*
+ * 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.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 net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.ResolveEntityResponse;
+import net.shibboleth.oidfed.metadata.cache.FederationEndpointEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.ResponseContainerExpirationCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer;
+import net.shibboleth.oidfed.metadata.cache.resolver.ResolveEntityCacheContainerIdentifier;
+import net.shibboleth.oidfed.metadata.cache.resolver.ResolveEntityCacheIdentifierCriterion;
+import net.shibboleth.oidfed.metadata.cache.resolver.ResolveEntityResponseContainer;
+import net.shibboleth.oidfed.metadata.payload.ResolveEntityResponsePayload;
+import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
+import net.shibboleth.oidfed.metadata.util.EntityStatementHelper;
+import net.shibboleth.oidfed.profile.TrustedRemoteResolverEntity;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidfed.profile.context.VerifiedTrustChain;
+import net.shibboleth.oidfed.profile.navigate.DefaultTrustChainIDsLookupStrategy;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+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.primitive.NonnullSupplier;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+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<EntityConfigurationContainer> entityConfigurationCache;
+
+    /** Cache containing responses from resolve entity APIs. */
+    @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> resolveEntityTrustChainMetadataCache;
+
+    /** Strategy used to obtain the entity id value of the request. */
+    @NonnullAfterInit private Function<ProfileRequestContext, String> entityIDLookupStrategy;
+
+    /** Strategy used to fetch the pre-selected trust chain entity IDs. */
+    @NonnullAfterInit 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, EntityConfiguration> 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, List<TrustedRemoteResolverEntity>> 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;
+
+    /** Entity id. */
+    @NonnullBeforeExec private String entityId;
+
+    /** Trusted entities to use. */
+    @NonnullBeforeExec private List<TrustedRemoteResolverEntity> trustedEntities;
+
+    /**
+     * Constructor.
+     */
+    public CallResolveEntityApi() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tccs =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class, true).compose(
+                        new InboundMessageContextLookup());
+        assert tccs != null;
+        trustChainContextCreationStrategy = tccs;
+        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<EntityConfigurationContainer> 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 entity id of the request.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setEntityIDLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        entityIDLookupStrategy =
+                Constraint.isNotNull(strategy, "EntityIDLookupStrategy 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, EntityConfiguration> 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, List<TrustedRemoteResolverEntity>> 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 (entityIDLookupStrategy == null) {
+            throw new ComponentInitializationException("EntityIDLookupStrategy cannot be null");
+        }
+        if (trustedEntitiesLookupStrategy == null) {
+            throw new ComponentInitializationException("TrustedEntitiesLookupStrategy cannot be null");
+        }
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("ObjectMapper cannot be null");
+        }
+        if (preSelectedTrustChainIdsLookupStrategy == null) {
+            throw new ComponentInitializationException("PreSelectedTrustChainIdsLookupStrategy cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        entityId = entityIDLookupStrategy.apply(profileRequestContext);
+        if (entityId == null) {
+            log.error("{} Unable to obtain entity ID", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        try {
+            new URL(entityId).toURI();
+        } catch (final URISyntaxException | MalformedURLException e) {
+            log.debug("{} The entity ID {} is not a valid URL, nothing to do", getLogPrefix(), entityId);
+            return false;
+        }
+
+        trustedEntities = trustedEntitiesLookupStrategy.apply(profileRequestContext);
+        if (trustedEntities == null || trustedEntities.isEmpty()) {
+            log.warn("{} No trusted entities resolved for {} nothing to do", getLogPrefix(), entityId);
+            ActionSupport.buildEvent(profileRequestContext, "CheckFallback");
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        log.debug("{} Resolving trust chain via resolve entity API for {}", getLogPrefix(), entityId);
+        final String nonNullEntityId = entityId;
+        assert nonNullEntityId != null;
+        final CriteriaSet baseCriteriaSet = new CriteriaSet(new SubjectEntityIDCriterion(nonNullEntityId));
+        final EntityConfiguration entityConfiguration = entityConfigurationLookupStrategy.apply(profileRequestContext);
+        if (entityConfiguration != null) {
+            log.debug("{} Entity configuration resolved and included to the criteria set", getLogPrefix());
+            baseCriteriaSet.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;
+        }
+        final Instant expirationTime = Instant.now().plus(cachedLifetime);
+        assert expirationTime != null;
+        baseCriteriaSet.add(new ResponseContainerExpirationCriterion(expirationTime));
+
+        final List<String> preSelectedChain =
+                Optional.ofNullable(preSelectedTrustChainIdsLookupStrategy.apply(profileRequestContext))
+                .orElse(CollectionSupport.emptyList());
+
+        final RelyingPartyTrustChainContext trustChainContext =
+                trustChainContextCreationStrategy.apply(profileRequestContext);
+
+        for (final TrustedRemoteResolverEntity trustedEntity : trustedEntities) {
+            if (trustedEntity == null) {
+                log.warn("{} Ignoring null trusted entity entry", getLogPrefix());
+                continue;
+            }
+            final List<TrustedRemoteResolverEntity> alreadyAttemptedEntities =
+                    Optional.ofNullable(trustChainContext.getAttemptedTrustedRemoteResolverEntities())
+                    .map(list -> new ArrayList<>(list))
+                    .orElseGet(NonnullSupplier.of(new ArrayList<>()));
+            if (alreadyAttemptedEntities.contains(trustedEntity)) {
+                log.debug("{} Trusted entity {} has already been attempted", getLogPrefix(), trustedEntity);
+                continue;
+            } else {
+                alreadyAttemptedEntities.add(trustedEntity);
+                trustChainContext.setAttemptedTrustedRemoteResolverEntities(
+                        CollectionSupport.copyToList(alreadyAttemptedEntities));
+            }
+            final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(trustedEntity.getEntityId()),
+                    new ResponseContainerExpirationCriterion(expirationTime));
+
+            final List<EntityConfigurationContainer> configurationCacheResult;
+            try {
+                configurationCacheResult = entityConfigurationCache.get(criteria);
+            } catch (final MetadataCacheException e) {
+                log.warn("{} Could not resolve entity configuration for {}", getLogPrefix(), trustedEntity, e);
+                continue;
+            }
+            if (configurationCacheResult.isEmpty()) {
+                log.warn("{} Could not resolve entity configuration for {}", getLogPrefix(), trustedEntity);
+                continue;
+            }
+            final EntityConfiguration configuration =
+                    Optional.ofNullable(configurationCacheResult.get(0).getStatement())
+                    .orElse(null);
+            if (configuration == null) {
+                log.warn("{} Could not resolve entity configuration for {}", getLogPrefix(), trustedEntity);
+                continue;
+            }
+            final URI uri = Optional.of(configuration.getParsedPayload().getMetadata())
+                        .map(metadata -> metadata.getFederationEntityMetadata())
+                        .map(entityMetadata -> entityMetadata.get("federation_resolve_endpoint"))
+                        .map(endpoint -> URI.create((String) endpoint))
+                        .orElse(null);
+            if (uri == null) {
+                log.warn("{} Could not fetch federation resolve endpoint for {}", getLogPrefix(), trustedEntity);
+                continue;
+            }
+            final String uriValue = uri.toString();
+            assert uriValue != null;
+            final ResolveEntityCacheContainerIdentifier entityRequest =
+                    new ResolveEntityCacheContainerIdentifier(
+                            uriValue, nonNullEntityId, CollectionSupport.copyToList(trustedEntity.getTrustAnchors()),
+                            entityTypesLookupStrategy.apply(profileRequestContext));
+            final CriteriaSet criteriaSet = new CriteriaSet();
+            baseCriteriaSet.forEach(c -> criteriaSet.add(c));
+            criteriaSet.add(new ResolveEntityCacheIdentifierCriterion(entityRequest));
+            criteriaSet.add(new FederationEndpointEntityStatementCriterion(configuration));
+            final List<ResolveEntityResponseContainer> cacheResult;
+            try {
+                cacheResult = resolveEntityTrustChainMetadataCache.get(criteriaSet);
+            } catch (final MetadataCacheException e) {
+                log.warn("{} Could not resolve entity for {} from {}", getLogPrefix(), entityId, trustedEntity, e);
+                continue;
+            }
+            if (cacheResult.isEmpty()) {
+                log.debug("{} No data resolved for {} from {}", getLogPrefix(), entityId, trustedEntity);
+                continue;
+            }
+            final ResolveEntityResponse statement = cacheResult.get(0).getStatement();
+            if (statement != null) {
+                final ResolveEntityResponsePayload payload = statement.getParsedPayload();
+                final List<String> rawTrustChain = payload.getTrustChain();
+                final Metadata metadata = payload.getMetadata();
+                final List<Map<String,String>> rawTrustMarks = payload.getTrustMarks();
+                if (rawTrustChain == null || rawTrustChain.isEmpty() || metadata == null) {
+                    log.warn("{} Could not parse mandatory parameters from the response from {}", getLogPrefix(),
+                            trustedEntity);
+                    continue;
+                }
+                assert objectMapper != null;
+                final List<EntityStatement<?>> chain =
+                        EntityStatementHelper.deserializeTrustChain(rawTrustChain, objectMapper);
+                if (chain == null) {
+                    log.warn("Could not parse the trust chain into list of entity statements");
+                    continue;
+                }
+                if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
+                    log.debug("{} Ignored resolved trust chain that doesn't match with preselected chain",
+                            getLogPrefix());
+                    continue;
+                }
+                final List<VerifiedTrustChain> policyCompliantChains = new ArrayList<>();
+                policyCompliantChains.add(new VerifiedTrustChain(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.getSubject()).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_type");
+                                            } 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 from {}", getLogPrefix(), trustedEntity);
+                }
+                return;
+            } else {
+                log.debug("{} The cached response from {} was not containing statement", getLogPrefix(),
+                        trustedEntity);
+                continue;
+            }
+        }
+        log.debug("{} No previously not attempted policy-compliant trust chains resolved", getLogPrefix());
+        ActionSupport.buildEvent(profileRequestContext, "CheckFallback");
+
+    }
+
+    /**
+     * 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", getLogPrefix(), e);
+            }
+        }
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ResolveTrustChains.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ResolveTrustChains.java
new file mode 100644
index 0000000..6868c38
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ResolveTrustChains.java
@@ -0,0 +1,263 @@
+/*
+ * 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.oidfed.profile.impl;
+
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.trustchain.TrustChainsContainer;
+import net.shibboleth.oidfed.profile.OidFederationEventIds;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidfed.profile.context.VerifiedTrustChain;
+import net.shibboleth.oidfed.profile.navigate.DefaultTrustChainIDsLookupStrategy;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+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.profile.action.ActionSupport;
+
+/**
+ * Resolves metadata policy-compliant trust chains from the configurable trust chain cache, metadata policy merging
+ * strategy and enforcer. The data is populated to the {@link RelyingPartyTrustChainContext}.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ResolveTrustChains.class);
+
+    /** Metadata cache for trust chains. */
+    @NonnullAfterInit private MetadataCache<TrustChainsContainer> trustChainCache;
+
+    /** Strategy used to obtain the entity id value of the request. */
+    @NonnullAfterInit private Function<ProfileRequestContext, String> entityIDLookupStrategy;
+
+    /** Strategy used to fetch the pre-selected trust chain entity IDs. */
+    @NonnullAfterInit 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, EntityConfiguration> entityConfigurationLookupStrategy;
+
+    /** Condition to require entity configuration via {@link #entityConfigurationLookupStrategy}. */
+    @Nonnull private Predicate<ProfileRequestContext> requireEntityConfigurationCondition;
+
+    /** Entity id. */
+    @NonnullBeforeExec private String entityId;
+
+    /**
+     * Constructor.
+     */
+    public ResolveTrustChains() {
+        super();
+        trustChainIDsLookupStrategy = new DefaultTrustChainIDsLookupStrategy();
+        entityConfigurationLookupStrategy = FunctionSupport.constant(null);
+        requireEntityConfigurationCondition = PredicateSupport.alwaysFalse();
+    }
+
+    /**
+     * Set the metadata cache for trust chains.
+     * 
+     * @param cache metadata cache
+     */
+    public void setTrustChainCache(@Nonnull final MetadataCache<TrustChainsContainer> cache) {
+        checkSetterPreconditions();
+        trustChainCache = Constraint.isNotNull(cache, "TrustChainCache cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the entity id of the request.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setEntityIDLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        entityIDLookupStrategy =
+                Constraint.isNotNull(strategy, "EntityIDLookupStrategy 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, EntityConfiguration> 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");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (trustChainCache == null) {
+            throw new ComponentInitializationException("TrustChainCache cannot be null");
+        }
+        if (entityIDLookupStrategy == null) {
+            throw new ComponentInitializationException("EntityIDLookupStrategy cannot be null");
+        }
+        if (preSelectedTrustChainIdsLookupStrategy == null) {
+            throw new ComponentInitializationException("PreSelectedTrustChainIdsLookupStrategy cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        entityId = entityIDLookupStrategy.apply(profileRequestContext);
+        if (entityId == null) {
+            log.error("{} Unable to obtain entity ID", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        try {
+            new URL(entityId).toURI();
+        } catch (final URISyntaxException | MalformedURLException e) {
+            log.debug("{} The entity ID {} is not a valid URL, nothing to do", getLogPrefix(), entityId);
+            return false;
+        }
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        log.debug("{} Resolving trust chain for {}", getLogPrefix(), entityId);
+        assert entityId != null;
+        final CriteriaSet criteriaSet = new CriteriaSet(new SubjectEntityIDCriterion(entityId));
+        final EntityConfiguration 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 List<TrustChainsContainer> cacheResult;
+        try {
+            cacheResult = trustChainCache.get(criteriaSet);
+        } catch (final MetadataCacheException e) {
+            log.warn("{} Could fetch trust chains for {}", getLogPrefix(), entityId);
+            ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.NO_TRUST_CHAINS_RESOLVED);
+            return;
+        }
+        if (cacheResult.isEmpty()) {
+            log.debug("{} No trust chains resolved for {}", getLogPrefix(), entityId);
+            ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.NO_TRUST_CHAINS_RESOLVED);
+            return;
+        }
+        final List<String> preSelectedChain =
+                Optional.ofNullable(preSelectedTrustChainIdsLookupStrategy.apply(profileRequestContext))
+                .orElse(CollectionSupport.emptyList());
+
+        final RelyingPartyTrustChainContext trustChainContext =
+                getTrustChainContextCreationStrategy().apply(profileRequestContext);
+        trustChainContext.setResolvedTrustChains(cacheResult.get(0).getTrustChains());
+        final List<VerifiedTrustChain> policyCompliantChains = new ArrayList<>();
+
+        String errorEventId = null;
+        for (final List<EntityStatement<?>> chain : cacheResult.get(0).getTrustChains()) {
+            assert chain != null;
+            if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
+                log.debug("{} Ignored resolved trust chain that doesn't match with preselected chain", getLogPrefix());
+                continue;
+            }
+            errorEventId = populatePolicyComplaintChains(chain, policyCompliantChains);
+        }
+
+        if (policyCompliantChains.isEmpty()) {
+            if (errorEventId == null) {
+                ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.NO_TRUST_CHAINS_RESOLVED);
+            } else {
+                ActionSupport.buildEvent(profileRequestContext, errorEventId);
+            }
+            return;
+        }
+        log.debug("{} Setting the policy compliant trust chains into the context: {}", getLogPrefix(),
+                policyCompliantChains);
+        trustChainContext.setPolicyCompliantTrustChains(policyCompliantChains);
+    }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultCombinedMetadataFromTrustChainLookupStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultCombinedMetadataFromTrustChainLookupStrategy.java
new file mode 100644
index 0000000..381e5ff
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultCombinedMetadataFromTrustChainLookupStrategy.java
@@ -0,0 +1,86 @@
+/*
+ * 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.oidfed.profile.navigate;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default strategy to combine metadata claim contents from the trust chain by exploiting both entity configuration
+ * and subordinate statement issued by the immediate superior.
+ */
+ at ThreadSafeAfterInit
+public class DefaultCombinedMetadataFromTrustChainLookupStrategy extends AbstractIdentifiableInitializableComponent
+    implements Function<List<EntityStatement<?>>,Map<String,Map<String,Object>>> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultCombinedMetadataFromTrustChainLookupStrategy.class);
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public Map<String,Map<String,Object>> apply(@Nullable final List<EntityStatement<?>> chain) {
+        if (chain == null || chain.size() < 3) {
+            log.warn("Unexpected trust chain input: {}", chain == null ? null : "size = " + chain.size());
+            return null;
+        }
+        final Map<String,Map<String,Object>> configurationMetadata =
+                Optional.ofNullable(chain.get(0).getParsedPayload().getMetadata())
+                .map(metadata -> metadata.getAllClaims()).orElse(null);
+        final Map<String,Map<String,Object>> subordinateMetadata =
+                Optional.ofNullable(chain.get(1).getParsedPayload().getMetadata())
+                .map(metadata -> metadata.getAllClaims()).orElse(null);
+
+        if (configurationMetadata == null || configurationMetadata.isEmpty()) {
+            log.debug("Entity configuration for {} doesn't contain metadata", chain.get(0).getSubject());
+            return null;
+        }
+
+        final Map<String,Map<String,Object>> result = new HashMap<>();
+        for (final String entityType : configurationMetadata.keySet()) {
+            final Map<String, Object> configurationClaims = configurationMetadata.get(entityType);
+            final Map<String, Object> subordinateClaims = new HashMap<>(Optional.ofNullable(subordinateMetadata)
+                    .map(metadata -> metadata.get(entityType))
+                    .orElse(CollectionSupport.emptyMap()));
+
+            for (final String configurationClaim : configurationClaims.keySet()) {
+                if (!subordinateClaims.containsKey(configurationClaim)) {
+                    log.trace("Including metadata claim {} from the entity configuration", configurationClaim);
+                    subordinateClaims.put(configurationClaim, configurationClaims.get(configurationClaim));
+                } else {
+                    log.trace("Keeping metadata claim {} from the subordinate configuration", configurationClaim);
+                }
+            }
+
+            result.put(entityType, subordinateClaims);
+        }
+
+        return result;
+    }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultMetadataValidationCondition.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultMetadataValidationCondition.java
new file mode 100644
index 0000000..da8c6cf
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultMetadataValidationCondition.java
@@ -0,0 +1,82 @@
+/*
+ * 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.oidfed.profile.navigate;
+
+import java.util.Map;
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
+import com.nimbusds.oauth2.sdk.client.ClientMetadata;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Default validation condition for metadata entries by entity type. Validation is done by parsing the entity type
+ * into a corresponding Nimbus object.
+ */
+public class DefaultMetadataValidationCondition implements BiPredicate<String, Map<String,Object>> {
+
+    /** Class logger. */
+    @Nonnull private final static Logger log = LoggerFactory.getLogger(DefaultMetadataValidationCondition.class);
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(@Nullable final String key, @Nullable final Map<String, Object> metadata) {
+        if (StringSupport.trimOrNull(key) == null || metadata == null) {
+            log.error("Invalid input to the validation condition key={}, metadata={}", key, metadata);
+            return false;
+        }
+        try {
+            assert key != null;
+            switch (key) {
+                case "federation_entity":
+                    log.debug("Ignoring validation of {}", key);
+                    return true;
+                case "openid_provider":
+                    OIDCProviderMetadata.parse(new JSONObject(metadata));
+                    return true;
+                case "openid_relying_party":
+                    OIDCClientMetadata.parse(new JSONObject(metadata));
+                    return true;
+                case "oauth_authorization_server":
+                    AuthorizationServerMetadata.parse(new JSONObject(metadata));
+                    return true;
+                case "oauth_client":
+                    ClientMetadata.parse(new JSONObject(metadata));
+                    return true;
+                case "oauth_resource":
+                    log.debug("Ignoring validation of {}", key);
+                    return true;
+                default:
+                    log.debug("Ignoring validation of {}", key);
+                    return true;
+            }
+        } catch (final ParseException e) {
+            log.warn("Could not parse entity_type {}", key, e);
+        }
+        return false;
+    }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java
new file mode 100644
index 0000000..7661f81
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java
@@ -0,0 +1,43 @@
+/*
+ * 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.oidfed.profile.navigate;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+
+import javax.annotation.Nullable;
+
+import net.shibboleth.oidfed.metadata.EntityStatement;
+
+/**
+ * Default strategy for looking up the entity IDs of a trust chain.
+ */
+public class DefaultTrustChainIDsLookupStrategy implements Function<List<EntityStatement<?>>,List<String>> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public List<String> apply(@Nullable final List<EntityStatement<?>> input) {
+        return Optional.ofNullable(input)
+                .filter(chain -> chain != null && !chain.isEmpty())
+                .map(chain -> IntStream.range(0, chain.size())
+                        .mapToObj(i -> chain.get(i))
+                        .map(statement -> statement.getSubject())
+                        .distinct()
+                        .toList())
+                .orElse(null);
+    }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java
new file mode 100644
index 0000000..3e60d04
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java
@@ -0,0 +1,145 @@
+/*
+ * 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.oidfed.profile.navigate;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.payload.SubordinateStatementPayload;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Default strategy to merge the metadata policies for the given entity type from the given trust chain. Finally,
+ * a configurable local metadata policy is merged to the resulting map of metadata policies.
+ */
+ at ThreadSafeAfterInit
+public class DefaultTrustChainMetadataPolicyMergingStrategy extends AbstractIdentifiableInitializableComponent
+        implements BiFunction<List<EntityStatement<?>>,String, Map<String, MetadataPolicy>> {
+
+    /** The strategy used for merging two metadata policies. */
+    @NonnullAfterInit private BiFunction<Map<String, MetadataPolicy>, Map<String, MetadataPolicy>,
+        Pair<Map<String, MetadataPolicy>, Boolean>> metadataPolicyMergingStrategy;
+
+    /** The strategy used for local (additional) metadata policy. */
+    @NonnullAfterInit private Function<List<EntityStatement<?>>, Map<String, MetadataPolicy>>
+        localMetadataPolicyStrategy;
+
+    /**
+     * Set the strategy used for merging two metadata policies.
+     * 
+     * @param strategy What to set.
+     */
+    public void setMetadataPolicyMergingStrategy(@Nonnull final BiFunction<Map<String,MetadataPolicy>,
+            Map<String,MetadataPolicy>, Pair<Map<String, MetadataPolicy>, Boolean>> strategy) {
+        checkSetterPreconditions();
+        metadataPolicyMergingStrategy = Constraint.isNotNull(strategy,
+                "Metadata policy merging strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used for local (additional) metadata policy.
+     * 
+     * @param strategy What to set.
+     */
+    public void setLocalMetadataPolicyStrategy(
+            @Nonnull final Function<List<EntityStatement<?>>, Map<String, MetadataPolicy>> strategy) {
+        checkSetterPreconditions();
+        localMetadataPolicyStrategy = Constraint.isNotNull(strategy,
+                "Local metadata policy strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (metadataPolicyMergingStrategy == null) {
+            throw new ComponentInitializationException("Metadata policy merging strategy cannot be null");
+        }
+        if (localMetadataPolicyStrategy == null) {
+            throw new ComponentInitializationException("Local metadata policy strategy cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nonnull
+    public Map<String, MetadataPolicy> apply(@Nullable final List<EntityStatement<?>> trustChain,
+            @Nullable final String entityType) throws ConstraintViolationException {
+        checkComponentActive();
+        if (trustChain == null || trustChain.isEmpty()) {
+            return CollectionSupport.emptyMap();
+        }
+        final Map<String, MetadataPolicy> result = new HashMap<>();
+        for (int i = trustChain.size(); i > 0; i--) {
+            final EntityStatement<?> entityStatement = trustChain.get(i - 1);
+            if (entityStatement.getParsedPayload() instanceof SubordinateStatementPayload ssp) {
+                final Map<String, Map<String, MetadataPolicy>> policy = ssp.getMetadataPolicy();
+                if (policy == null || policy.isEmpty()) {
+                    continue;
+                }
+
+                if (StringSupport.trimOrNull(entityType) != null) {
+                    try {
+                        assert entityType != null;
+                        result.putAll(mergePolicies(result, policy.get(entityType)));
+                    } catch (final ConstraintViolationException e) {
+                        throw new ConstraintViolationException("Could not merge federation metadata policies");
+                    }
+                }
+            }
+        }
+        try {
+            result.putAll(mergePolicies(result, localMetadataPolicyStrategy.apply(trustChain)));
+        } catch (final ConstraintViolationException e) {
+            throw new ConstraintViolationException("Could not merge local metadata policy in the federation policy");
+        }
+        return result;
+    }
+
+    /**
+     * Merges the two maps of metadata policies.
+     * 
+     * @param first the first map of policies
+     * @param second the second map of policies
+     * @return the map containing merged policies
+     * @throws ConstraintViolationException if the merging fails
+     */
+    @Nonnull
+    protected Map<String, MetadataPolicy> mergePolicies(@Nullable final Map<String, MetadataPolicy> first,
+            @Nullable final Map<String, MetadataPolicy> second) throws ConstraintViolationException {
+        final Pair<Map<String, MetadataPolicy>, Boolean> result = metadataPolicyMergingStrategy.apply(first, second);
+        if (result != null && Boolean.TRUE.equals(result.getSecond())) {
+            final Map<String, MetadataPolicy> mergedMap = result.getFirst();
+            return mergedMap == null ? CollectionSupport.emptyMap() : mergedMap;
+        }
+        throw new ConstraintViolationException("Merge failed");
+    }
+
+}

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


More information about the commits mailing list