[java-oidfed-common] branch main updated: Imported 'resolve-entity' flow from the OP's OIDFED plugin

Codeberg noreply at shibboleth.net
Tue Sep 22 15:43:19 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/b95418629127f77d9bca13b13e9e039e2ec4c95a

The following commit(s) were added to refs/heads/main by this push:
     new b954186  Imported 'resolve-entity' flow from the OP's OIDFED plugin
b954186 is described below

commit b95418629127f77d9bca13b13e9e039e2ec4c95a
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue Sep 22 18:42:08 2026 +0300

    Imported 'resolve-entity' flow from the OP's OIDFED plugin
    
    - Changed the features to not require OP's codebase
    - Similar set of flow tests
---
 .../context/RelyingPartyConfigurationSupport.java  |  61 +++
 .../context/logic/TrustAnchorIdPredicate.java      |  98 +++++
 .../navigate/TrustAnchorIdLookupFunction.java      |  94 +++++
 oidfed-common-conf-impl/pom.xml                    |   5 +
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  69 +++-
 .../oidfed/resolve-entity/resolve-entity-beans.xml | 361 ++++++++++++++++
 .../oidfed/resolve-entity/resolve-entity-flow.xml  | 213 ++++++++++
 .../idp/service/relying-party/postconfig.xml       |  81 +++-
 .../oidfed/flow/AbstractFederationFlowTest.java    |   1 -
 .../oidfed/flow/ResolveEntityFlowTest.java         | 455 +++++++++++++++++++++
 .../src/test/resources/credentials/htpasswd.txt    |   2 +
 .../oauth2client-authn-config.xml}                 |  33 +-
 .../shibboleth/idp/module/conf/relying-party.xml   |  16 +
 .../decoding/impl/ResolveEntityRequestDecoder.java | 110 +++++
 .../BuildResolveEntityErrorResponseFromEvent.java  | 269 ++++++++++++
 .../profile/impl/BuildResolveEntityResponse.java   | 204 +++++++++
 .../impl/FormOutboundResolveEntityResponse.java    | 228 +++++++++++
 .../impl/LookupCachedResolveEntityResponse.java    | 157 +++++++
 .../ValidateResolveEntityProfileConfiguration.java | 231 +++++++++++
 .../profile/impl/ValidateResolveEntityRequest.java | 186 +++++++++
 .../profile/impl/ValidateSelectedTrustChain.java   | 231 +++++++++++
 .../navigate/DefaultEntityTypesLookupFunction.java |  46 +++
 ...ormationFederationEntityCredentialResolver.java | 128 ------
 ...ustChainFederationEntityCredentialResolver.java | 124 ++++++
 .../oidfed/conf/oidfed/oidfed.properties           |  17 +
 25 files changed, 3270 insertions(+), 150 deletions(-)

diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyConfigurationSupport.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyConfigurationSupport.java
new file mode 100644
index 0000000..7fc7431
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyConfigurationSupport.java
@@ -0,0 +1,61 @@
+/*
+ * 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.Collection;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.oidfed.profile.context.logic.TrustAnchorIdPredicate;
+import net.shibboleth.profile.relyingparty.BasicRelyingPartyConfiguration;
+import net.shibboleth.profile.relyingparty.RelyingPartyConfiguration;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Support functions for building {@link RelyingPartyConfiguration} objects with activation conditions.
+ */
+public class RelyingPartyConfigurationSupport {
+
+    /**
+     * A shorthand method for constructing a {@link BasicRelyingPartyConfiguration} with an activation condition
+     * based on one or more trust anchor IDs.
+     * 
+     * <p>If a single ID is supplied, then the ID is also set as the identifier for the configuration.</p>
+     * 
+     * @param trustAnchorIds the trust anchors for which the configuration should be active
+     * 
+     * @return  a default-constructed configuration with the appropriate condition set
+     */
+    @Nonnull
+    public static BasicRelyingPartyConfiguration byTrustAnchor(@Nonnull final Collection<String> trustAnchorIds) {
+
+        Constraint.isNotNull(trustAnchorIds, "Trust Anchor ID list cannot be null");
+
+        final BasicRelyingPartyConfiguration config = new BasicRelyingPartyConfiguration(); 
+        config.setActivationCondition(new TrustAnchorIdPredicate(trustAnchorIds));
+        
+        final StringBuffer name = new StringBuffer("TrustAnchorIDs[");
+        for (final String taId: trustAnchorIds) {
+            name.append(taId).append(',');
+            
+        }
+        name.append(']');
+        final String id = name.toString();
+        assert id != null;
+        config.setId(id);
+        return config;
+    }
+
+}
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/logic/TrustAnchorIdPredicate.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/logic/TrustAnchorIdPredicate.java
new file mode 100644
index 0000000..4ff2a99
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/logic/TrustAnchorIdPredicate.java
@@ -0,0 +1,98 @@
+/*
+ * 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.logic;
+
+import java.util.Collection;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidfed.profile.context.navigate.TrustAnchorIdLookupFunction;
+
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.StrategyIndirectedPredicate;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Predicate that evaluates a {@link ProfileRequestContext} by looking for a trust anchor ID that matches one of a
+ * designated set, or a generic predicate.
+ */
+public class TrustAnchorIdPredicate extends StrategyIndirectedPredicate<ProfileRequestContext,String> {
+
+    /**
+     * Constructor.
+     * 
+     * @param candidates hardwired set of values to check against
+     */
+    public TrustAnchorIdPredicate(@Nonnull @ParameterName(name="candidates") final Collection<String> candidates) {
+        super(new TrustAnchorIdLookupFunction(), StringSupport.normalizeStringCollection(candidates));
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param candidate a single value to check against
+     */
+    public TrustAnchorIdPredicate(@Nonnull @NotEmpty @ParameterName(name="candidate") final String candidate) {
+        this(CollectionSupport.singleton(candidate));
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param pred generalized predicate
+     */
+    public TrustAnchorIdPredicate(@Nonnull @ParameterName(name="pred") final Predicate<String> pred) {
+        super(new TrustAnchorIdLookupFunction(), pred);
+    }
+    
+    /**
+     * Workaround for Spring type conversion ambiguities.
+     * 
+     * @param candidates hardwired set of values to check against
+     * 
+     * @return the predicate
+     */
+    @Nonnull public static TrustAnchorIdPredicate fromCandidates(@Nonnull final Collection<String> candidates) {
+        return new TrustAnchorIdPredicate(candidates);
+    }
+    
+    /**
+     * Workaround for Spring type conversion ambiguities.
+     * 
+     * @param candidate a single value to check against
+     * 
+     * @return the predicate
+     */
+    @Nonnull public static TrustAnchorIdPredicate fromCandidate(@Nonnull @NotEmpty final String candidate) {
+        return new TrustAnchorIdPredicate(candidate);
+    }
+
+    /**
+     * Workaround for Spring type conversion ambiguities.
+     * 
+     * @param pred generalized predicate
+     * 
+     * @return the predicate
+     */
+    @Nonnull public static TrustAnchorIdPredicate fromPredicate(@Nonnull final Predicate<String> pred) {
+        return new TrustAnchorIdPredicate(pred);
+    }
+
+}
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/navigate/TrustAnchorIdLookupFunction.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/navigate/TrustAnchorIdLookupFunction.java
new file mode 100644
index 0000000..8faef10
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/navigate/TrustAnchorIdLookupFunction.java
@@ -0,0 +1,94 @@
+/*
+ * 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.navigate;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidfed.support.ClientInformationExtensionSupport;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A function that primarily returns {@link ClientInformationExtensionSupport#KEY_VALIDATED_TRUST_ANCHOR} if found from
+ * the client information resolved via {@link OIDCMetadataContext} and secondarily takes the subject of the last entity
+ * statement in the {@link RelyingPartyTrustChainContext#getSelectedTrustChain()} that is expected to be the anchor of
+ * the selected trust chain.
+ * 
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+ at ThreadSafe
+public class TrustAnchorIdLookupFunction implements Function<ProfileRequestContext, String> {
+
+    /** Strategy used to lookup the OIDC metadata context. */
+    @Nonnull private Function<ProfileRequestContext, OIDCMetadataContext> oidcMetadataContextLookupStrategy;
+
+    /** Strategy used to lookup the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public TrustAnchorIdLookupFunction() {
+        final Function<ProfileRequestContext,OIDCMetadataContext> omcls =
+                new ChildContextLookup<>(OIDCMetadataContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert omcls != null;
+        oidcMetadataContextLookupStrategy = omcls;
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param oidcMetadataStrategy strategy used to lookup the OIDC metadata context
+     * @param trustChainStrategy strategy used to lookup the trust chain context
+     */
+    public TrustAnchorIdLookupFunction(
+            @Nonnull final Function<ProfileRequestContext, OIDCMetadataContext> oidcMetadataStrategy,
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainStrategy) {
+        oidcMetadataContextLookupStrategy = Constraint.isNotNull(oidcMetadataStrategy,
+                "OIDC metadata context lookup strategy cannot be null");
+        trustChainContextLookupStrategy = Constraint.isNotNull(trustChainStrategy,
+                "Trust chain context lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public String apply(@Nullable final ProfileRequestContext profileRequestContext) {
+        return Optional.ofNullable(oidcMetadataContextLookupStrategy.apply(profileRequestContext))
+                .map(oidcContext -> oidcContext.getClientInformation())
+                .map(clientInfo -> clientInfo != null ?
+                        ClientInformationExtensionSupport.parseValidatedTrustAnchor(clientInfo) : null)
+                .orElse(Optional.ofNullable(trustChainContextLookupStrategy.apply(profileRequestContext))
+                        .map(trustChainContext -> trustChainContext.getSelectedTrustChain())
+                        .map(verifiedChain -> verifiedChain.getTrustChain())
+                        .map(trustChain -> trustChain.get(trustChain.size() - 1))
+                        .map(anchorStatement -> anchorStatement.getSubject())
+                        .orElse(null));
+    }
+}
diff --git a/oidfed-common-conf-impl/pom.xml b/oidfed-common-conf-impl/pom.xml
index cc3d193..847a518 100644
--- a/oidfed-common-conf-impl/pom.xml
+++ b/oidfed-common-conf-impl/pom.xml
@@ -70,6 +70,11 @@
             <artifactId>oidc-common-profile-impl</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${oidc-common.groupId}</groupId>
+            <artifactId>oidc-common-conf-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>${idp.groupId}</groupId>
             <artifactId>idp-admin-api</artifactId>
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index d0e5188..42bd010 100644
--- a/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -322,7 +322,7 @@
                             p:defaultSupportedOperators-ref="#{'%{idp.oidfed.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}">
                             <property name="supportedOperators">
                                 <util:map>
-                                    <entry key="net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest" value-ref="#{'%{idp.oidfed.resolve-entity.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}" />
+                                    <entry key="net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest" value-ref="#{'%{idp.oidfed.resolveEntity.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}" />
                                     <entry key="net.shibboleth.oidfed.messaging.impl.ExplicitClientRegistrationRequest" value-ref="#{'%{idp.oidfed.register.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}" />
                                 </util:map>
                             </property>
@@ -1118,6 +1118,73 @@
         class="net.shibboleth.oidfed.profile.impl.BiConsumerEntityConfigurationMetadataDecorator"
         abstract="true" />
 
+    <bean id="shibboleth.oidfed.DefaultApiMappedErrors"
+            class="org.springframework.beans.factory.config.MapFactoryBean">
+        <property name="sourceMap">
+            <map value-type="com.nimbusds.oauth2.sdk.ErrorObject">
+                <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).AUTHN_EXCEPTION}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+                <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).SUBJECT_C14N_ERROR}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+                <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).UNKNOWN_USERNAME}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+                <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).NO_CREDENTIALS}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+                <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).INVALID_CREDENTIALS}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+                <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).ACCOUNT_LOCKED}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+                <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).ACCOUNT_ERROR}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+                <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).RESELECT_FLOW}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+                <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).NO_POTENTIAL_FLOW}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+                <entry key="#{T(net.shibboleth.idp.profile.IdPEventIds).INVALID_PROFILE_CONFIG}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).UNAUTHORIZED_CLIENT}" />
+                <entry key="#{T(org.opensaml.profile.action.EventIds).ACCESS_DENIED}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).ACCESS_DENIED}" />
+            </map>
+        </property>
+    </bean>
+
+    <bean id="shibboleth.oidc.DefaultResolveEntityApiMappedErrors"
+            parent="shibboleth.oidfed.DefaultApiMappedErrors"
+            class="org.springframework.beans.factory.config.MapFactoryBean">
+        <property name="sourceMap">
+            <map merge="true" value-type="com.nimbusds.oauth2.sdk.ErrorObject">
+                <entry>
+                    <key>
+                        <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MSG_CTX"/>
+                    </key>
+                    <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="server_error" c:_1="Internal server error" c:_2="500" />
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.oidfed.profile.OidFederationEventIds.INVALID_TRUST_ANCHOR"/>
+                    </key>
+                    <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_trust_anchor" c:_1="Trust anchor in the request is invalid" c:_2="404" />
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.oidfed.profile.OidFederationEventIds.INVALID_SUBJECT"/>
+                    </key>
+                    <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_subject" c:_1="Subject in the request is invalid" c:_2="404" />
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.oidfed.profile.OidFederationEventIds.INVALID_METADATA"/>
+                    </key>
+                    <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_metadata" c:_1="Metadata is invalid or not found for the requested entity types" c:_2="400" />
+                </entry>
+            </map>
+        </property>
+    </bean>
+
+    <!-- Property-based definition of login flows for the resolve-entity endpoint. -->    
+    <bean id="shibboleth.oidfed.resolve-entity.PotentialFlows" class="org.springframework.beans.factory.config.ListFactoryBean"
+        p:sourceList="#{getObject('shibboleth.AuthenticationFlowDescriptorManager').getComponents().?[id matches 'authn/(' + '%{idp.oidfed.resolveEntity.authn.flows:OAuth2Client}'.trim() + ')']}" />
+
     <import resource="${idp.home}/conf/oidfed/oidfed-trustchain-resolver.xml"/>
 
 </beans>
\ No newline at end of file
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
new file mode 100644
index 0000000..5c34d90
--- /dev/null
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
@@ -0,0 +1,361 @@
+<?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="shibboleth.oidfed.profileId" class="java.lang.String"
+        c:_0="#{T(net.shibboleth.oidfed.profile.config.OIDFederationResolveEntityProfileConfiguration).PROFILE_ID}" />
+
+    <bean id="shibboleth.oidfed.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.oidfedresolve:OIDFED.ResolveEntity}" />
+
+    <bean id="shibboleth.oidfed.browserProfile" class="java.lang.Boolean" c:_0="false" />
+
+    <alias alias="shibboleth.oidfed.trust-chain-resolver.UseResolverApiCondition"
+        name="%{idp.oidfed.resolveEntity.trustchain.resolver.useResolverApiCondition:shibboleth.Conditions.FALSE}" />
+
+    <alias alias="shibboleth.oidfed.trust-chain-resolver.FallbackToLocalResolutionCondition"
+        name="%{idp.oidfed.resolveEntity.trustchain.resolver.fallbackToLocalCondition:shibboleth.Conditions.FALSE}" />
+
+    <alias alias="ResolveTrustChainsCondition"
+        name="%{idp.oidfed.resolveEntity.resolveTrustChainsCondition:DefaultResolveTrustChainsCondition}" />
+
+    <bean id="DefaultResolveTrustChainsCondition" parent="shibboleth.Conditions.Expression"
+        c:expression="#input.getInboundMessageContext().getMessage().getClientAuthentication() instanceof T(com.nimbusds.oauth2.sdk.auth.PrivateKeyJWT)" />
+
+    <bean id="TrustChainCandidatesExist" parent="shibboleth.Conditions.Expression"
+        c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext)) and #input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext)).getPolicyCompliantTrustChains() != null and #input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext)).getPolicyCompliantTrustChains().size() > 0"/>
+
+    <bean id="InitializeProfileRequestContext"
+        class="net.shibboleth.idp.profile.impl.InitializeProfileRequestContext" scope="prototype"
+        p:profileId-ref="shibboleth.oidfed.profileId"
+        p:loggingId-ref="shibboleth.oidfed.loggingId"
+        p:browserProfile-ref="shibboleth.oidfed.browserProfile" />
+
+    <bean id="CallInboundMessageHandler"
+        class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+        c:executionDirection="INBOUND">
+        <constructor-arg name="messageHandler">
+            <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
+                <property name="functionLookupStrategy">
+                    <bean class="net.shibboleth.oidc.profile.config.navigate.MessageHandlerLookupFunction" />
+                </property>
+            </bean>
+        </constructor-arg>
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
+        </property>
+    </bean>
+
+    <bean id="PopulateMetricContext"
+        class="org.opensaml.profile.action.impl.PopulateMetricContext" scope="prototype"
+        p:counterName="#{getObject('shibboleth.metrics.ProfileCounter')}"
+        p:metricStrategy="#{getObject('shibboleth.metrics.MetricStrategy')}" />
+
+    <util:constant id="shibboleth.metrics.ProfileCounter"
+        static-field="net.shibboleth.oidfed.profile.config.impl.DefaultOIDFederationResolveEntityProfileConfiguration.PROFILE_COUNTER" />
+
+    <bean id="DecodeMessage" class="org.opensaml.profile.action.impl.DecodeMessage" scope="prototype">
+        <constructor-arg>
+            <bean class="net.shibboleth.oidfed.decoding.impl.ResolveEntityRequestDecoder" scope="prototype"
+                p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+                <!-- TODO: support custom parsing? p:customRequestParser="#{getObject('%{idp.oidfed.requestParser.ResolveEntityRequest:}'.trim())}"/>-->
+        </constructor-arg>
+    </bean>
+
+    <bean id="InitializeAuthenticationContext"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeAuthenticationContext" scope="prototype" />
+
+    <bean id="shibboleth.oidfed.ResolveEntityResponseMetadataCache" parent="shibboleth.oidfed.CacheBuilder">
+        <constructor-arg>
+            <bean p:cacheId="DefaultResolveEntityResponseMetadataCache" parent="shibboleth.oidfed.ResolveEntityResponseMetadataCacheBuilderSpec"
+                p:cleanupTaskInterval="PT30S"/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="SelectRelyingPartyConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype"
+        p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyResolverService" />
+
+    <bean id="SelectProfileConfiguration" class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration"
+        scope="prototype" />
+
+    <bean id="PopulateInboundInterceptContext"
+            class="net.shibboleth.idp.profile.interceptor.impl.PopulateProfileInterceptorContext" scope="prototype"
+            p:availableFlows="#{@'shibboleth.ProfileInterceptorFlowDescriptorManager'.getComponents()}"
+            p:loggingLabel="inbound">
+        <property name="activeFlowsLookupStrategy">
+            <bean class="net.shibboleth.idp.profile.config.navigate.InboundFlowsLookupFunction" />
+        </property>
+    </bean>
+
+    <bean id="ValidateResolveEntityProfileConfiguration"
+        class="net.shibboleth.oidfed.profile.impl.ValidateResolveEntityProfileConfiguration"
+        scope="prototype">
+        <property name="subjectEntityIdLookupStrategy">
+            <bean parent="shibboleth.Functions.Expression"
+                c:expression="#input.getInboundMessageContext()?.getMessage().getClientAuthentication().getClientID()?.getValue()" />
+        </property>
+    </bean>
+
+    <bean id="shibboleth.oidfed.trust-chain-resolver.PreSelectedTrustChainIDsLookupStrategy"
+        parent="shibboleth.Functions.Expression" c:expression="#null" />
+
+    <bean id="shibboleth.oidfed.ResolveEntityResponseMetadataCacheBuilderSpec"
+        class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
+        p:minCacheDuration="%{idp.oidfed.cache.resolveEntity.minRefreshDelay:PT1S}"
+        p:maxCacheDuration="%{idp.oidfed.cache.resolveEntity.maxRefreshDelay:PT30S}">
+        <property name="criteriaToIdentifierStrategy">
+            <bean parent="shibboleth.Functions.Expression"
+                c:expression="#input?.get(T(net.shibboleth.oidfed.metadata.cache.local.ResolveEntityRequestCriterion))?.getRequest().toString()"/>
+        </property>
+        <property name="identifierExtractionStrategy">
+            <bean parent="shibboleth.Functions.Expression"
+                c:expression="#input?.getRequest()?.toString()"/>
+        </property>
+        <property name="metadataExpirationTimeStrategy">
+            <bean class="net.shibboleth.oidfed.metadata.cache.local.DefaultNimbusResponseContainerExpirationTimeStrategy"/>
+        </property>
+        <property name="metadataFilterStrategy">
+            <bean parent="shibboleth.BiFunctions.Expression" c:expression="#input1"/>
+        </property>
+        <property name="fetchStrategy">
+            <bean class="net.shibboleth.oidfed.metadata.cache.local.DefaultResolveEntityResponseFetchingStrategy" />
+        </property>
+    </bean>
+
+    <bean id="ValidateRequest" class="net.shibboleth.oidfed.profile.impl.ValidateResolveEntityRequest"
+        scope="prototype"
+        p:localTrustAnchorsCache-ref="#{'%{idp.oidfed.resolveEntity.LocalTrustAnchorsMetadataCache:shibboleth.oidfed.LocalTrustAnchorsMetadataCache}'.trim()}" />
+
+    <bean id="LookupCachedResolveEntityResponse"
+        class="net.shibboleth.oidfed.profile.impl.LookupCachedResolveEntityResponse"
+        scope="prototype"
+        p:responseCache-ref="shibboleth.oidfed.ResolveEntityResponseMetadataCache" />
+
+    <bean id="shibboleth.oidfed.trust-chain-resolver.EntityIDLookupStrategy"
+        parent="shibboleth.Functions.Expression"
+        c:expression="'true'.equals(#input.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))?.getAuthenticationStateMap()?.get('idp.resolve-entity.resolveForAuthentication')) ? #input.getInboundMessageContext()?.getMessage().getClientAuthentication()?.getClientID()?.getValue() : #input.getInboundMessageContext()?.getMessage()?.getSubject()"/>
+
+    <bean id="DefaultMetadataPolicyEnforcer"
+        class="net.shibboleth.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEnforcer"
+        p:metadataPolicyOperators-ref="#{'%{idp.oidfed.resolveEntity.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
+
+    <bean id="DefaultTrustChainMetadataPolicyMergingStrategy" 
+        class="net.shibboleth.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
+        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.resolveEntity.MetadataPolicyMergingStrategy:MetadataPolicyMergingStrategy}'.trim()}"
+        p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.resolveEntity.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"/>
+
+    <bean id="MetadataPolicyMergingStrategy"
+        class="net.shibboleth.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyMergingStrategy"
+        p:metadataPolicyOperators-ref="#{'%{idp.oidfed.resolveEntity.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
+
+    <bean id="DefaultLocalMetadataPolicyStrategy"
+        parent="shibboleth.Functions.Constant">
+        <constructor-arg name="target">
+            <util:map/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="SelectTrustChain" class="net.shibboleth.oidfed.profile.impl.SelectTrustChain"
+        scope="prototype">
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.Expression"
+                c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext))" />
+        </property>
+    </bean>
+
+    <bean id="ValidateSelectedTrustChain" class="net.shibboleth.oidfed.profile.impl.ValidateSelectedTrustChain"
+        scope="prototype"
+        p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"/>
+
+    <bean id="ResolveTrustMarks" class="net.shibboleth.oidfed.profile.impl.ResolveTrustMarks"
+        scope="prototype"
+        p:trustChainCache-ref="#{'%{idp.oidfed.resolveEntity.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:trustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.TrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}"
+        p:delegatedTrustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.DelegatedTrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultDelegatedTrustMarkClaimsValidationLookupStrategy')}"
+        p:trustEngine-ref="shibboleth.oidfed.DefaultEntityConfigurationTrustEngine"
+        p:delegationTrustEngine-ref="shibboleth.oidfed.DefaultDelegatedTrustMarkTrustEngine">
+        <property name="trustChainTrustMarksParsingStrategy">
+            <bean class="net.shibboleth.oidfed.profile.navigate.DefaultTrustChainTrustMarksParsingStrategy"
+                p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"/>
+        </property>
+        <property name="trustedTrustMarkIssuersLookupStrategy">
+            <bean class="net.shibboleth.oidfed.profile.navigate.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"/>
+        </property>
+        <property name="trustedTrustMarkOwnersLookupStrategy">
+            <bean class="net.shibboleth.oidfed.profile.navigate.DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy"/>
+        </property>
+    </bean>
+
+    <bean id="ValidateTrustMarks" class="net.shibboleth.oidfed.profile.impl.ValidateTrustMarks"
+        scope="prototype"
+        p:trustMarkStatusCache-ref="#{'%{idp.oidfed.resolveEntity.TrustMarkStatusMetadataCache:shibboleth.oidfed.TrustMarkStatusMetadataCache}'.trim()}">
+    </bean>
+
+    <bean id="PopulateResolveResponseSignatureSigningParameters"
+        class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters" scope="prototype"
+        c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
+        p:securityParametersContextLookupStrategy-ref="ResolveResponseSecurityParametersContextLookupStrategy">
+        <property name="configurationLookupStrategy">
+            <bean lazy-init="true"
+                class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+        </property>
+        <property name="signatureSigningParametersResolver">
+            <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationSignatureSigningParametersResolver">
+                <constructor-arg name="signatureAlgorithmLookupStrategy">
+                    <bean parent="shibboleth.Functions.Constant" c:target="" />
+                </constructor-arg>
+                <constructor-arg name="defaultAlgorithmValue" value="%{idp.oidfed.resolveEntity.sigalg:RS256}" />
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="ResolveResponseSecurityParametersContextLookupStrategy" parent="shibboleth.Functions.Compose"
+            c:g-ref="shibboleth.oidfed.ChildLookupOrCreate.JWTSecurityParameters"
+            c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+
+    <bean id="shibboleth.oidfed.ChildLookupOrCreate.JWTSecurityParameters"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.oidc.security.jose.context.SecurityParametersContext) }"
+        c:createContext="true" />
+
+    <bean id="ResolveResponseSecurityParametersCreationViaMessageContextStrategy" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g" ref="ResolveResponseSecurityParametersContextLookupStrategy" />
+        <constructor-arg name="f">
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input.getParent()" />
+        </constructor-arg>
+    </bean>
+
+    <bean id="BuildResolveResponse"
+        class="net.shibboleth.oidfed.profile.impl.BuildResolveEntityResponse" scope="prototype"
+        p:identifierGeneratorLookupStrategy-ref="shibboleth.oidfed.DefaultIdentifierGenerationStrategy"
+        p:objectMapper-ref="#{'%{idp.oidfed.logging.objectMapper:shibboleth.oidfed.JWTPayloadJSONObjectMapper}'.trim()}">
+        <property name="subjectLookupStrategy">
+            <bean parent="shibboleth.Functions.Expression"
+                c:expression="#input.ensureInboundMessageContext().getMessage().getSubject()" />
+        </property>    
+    </bean>
+
+    <bean id="SignResolveResponse" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+            scope="prototype" c:executionDirection="OUTBOUND ">
+        <constructor-arg name="messageHandler">
+            <bean id="SignResolveResponseHandler"
+                class="net.shibboleth.oidc.security.impl.SignJWTHandler" scope="prototype" p:logName="Resolve Response"
+                p:securityParametersLookupStrategy-ref="ResolveResponseSecurityParametersCreationViaMessageContextStrategy"
+                p:typeHeader="resolve-response+jwt">
+                <property name="claimsToSignLookupStrategy">
+                     <bean
+                        class="net.shibboleth.oidfed.profile.impl.JWTClaimsSetFromEntityStatementLookupFunction" />
+                </property>
+                <property name="jwtUpdateConsumer">
+                    <bean
+                        class="net.shibboleth.oidfed.profile.impl.EntityStatementUpdateStrategy" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+
+    <bean id="FormOutboundMessage"
+        class="net.shibboleth.oidfed.profile.impl.FormOutboundResolveEntityResponse" scope="prototype"
+        p:responseCache-ref="shibboleth.oidfed.ResolveEntityResponseMetadataCache" />
+
+    <bean id="BuildErrorResponseFromEvent"
+        class="net.shibboleth.oidfed.profile.impl.BuildResolveEntityErrorResponseFromEvent" scope="prototype"
+        p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier"
+        p:mappedErrors="#{getObject('shibboleth.oidfed.resolve-entity.MappedErrors') ?: getObject('shibboleth.oidc.DefaultResolveEntityApiMappedErrors')}"
+        p:responseCache-ref="shibboleth.oidfed.ResolveEntityResponseMetadataCache">
+        <property name="eventContextLookupStrategy">
+            <bean class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
+        </property>
+    </bean>
+
+    <bean id="LogEvent" class="org.opensaml.profile.action.impl.LogEvent" scope="prototype"
+        p:suppressedEvents="#{getObject('shibboleth.SuppressedEvents') ?: getObject('shibboleth.DefaultSuppressedEvents')}">
+        <property name="eventContextLookupStrategy">
+            <bean class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
+        </property>
+    </bean>
+
+    <bean id="FlowStartPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.FlowStartAuditExtractors') ?: getObject('shibboleth.DefaultFlowStartAuditExtractors')}" />
+
+    <bean id="PostDecodePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.oidfed.resolve-entity-PostDecodeAuditExtractors') ?: getObject('shibboleth.oidfed.resolve-entity.DefaultPostDecodeAuditExtractors')}" />
+
+    <bean id="PostLookupPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.oidfed.resolve-entity.PostLookupAuditExtractors') ?: getObject('shibboleth.oidfed.resolve-entity.DefaultPostLookupAuditExtractors')}" />
+
+    <bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.oidfed.resolve-entity.PostResponseAuditExtractors') ?: getObject('shibboleth.oidfed.resolve-entity.DefaultPostResponseAuditExtractors')}" />
+
+    <bean id="shibboleth.oidfed.resolve-entity.DefaultPostDecodeAuditExtractors"
+        class="org.springframework.beans.factory.config.MapFactoryBean">
+        <property name="sourceMap">
+            <map/>
+        </property>
+    </bean>
+
+    <bean id="shibboleth.oidfed.resolve-entity.DefaultPostLookupAuditExtractors"
+        class="org.springframework.beans.factory.config.MapFactoryBean">
+        <property name="sourceMap">
+            <map/>
+        </property>
+    </bean>
+
+    <bean id="shibboleth.oidfed.resolve-entity.DefaultPostResponseAuditExtractors"
+        class="org.springframework.beans.factory.config.MapFactoryBean">
+        <property name="sourceMap">
+            <map/>
+        </property>
+    </bean>
+
+    <bean id="PopulateOutboundInterceptContext"
+            class="net.shibboleth.idp.profile.interceptor.impl.PopulateProfileInterceptorContext" scope="prototype"
+            p:availableFlows="#{@'shibboleth.ProfileInterceptorFlowDescriptorManager'.getComponents()}"
+            p:loggingLabel="outbound">
+        <property name="activeFlowsLookupStrategy">
+            <bean class="net.shibboleth.idp.profile.config.navigate.OutboundFlowsLookupFunction" />
+        </property>
+    </bean>
+
+    <bean id="CallOutboundMessageHandler"
+        class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+        c:executionDirection="OUTBOUND">
+        <constructor-arg name="messageHandler">
+            <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
+                <property name="functionLookupStrategy">
+                    <bean class="net.shibboleth.oidc.profile.config.navigate.MessageHandlerLookupFunction" />
+                </property>
+            </bean>
+        </constructor-arg>
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
+        </property>
+    </bean>
+
+    <bean id="WriteAuditLog" class="net.shibboleth.idp.profile.audit.impl.WriteAuditLog" scope="prototype"
+        p:activationCondition-ref="shibboleth.ProfileAuditingCondition"
+        p:formattingMap-ref="shibboleth.AuditFormattingMap"
+        p:dateTimeFormat="#{getObject('shibboleth.AuditDateTimeFormat')}"
+        p:useDefaultTimeZone="#{getObject('shibboleth.AuditDefaultTimeZone') ?: false}"
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+
+    <bean id="EncodeMessage" class="org.opensaml.profile.action.impl.EncodeMessage" scope="prototype"
+        p:messageEncoderFactory-ref="oidc.messageEncoderFactory"
+        p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier" />
+
+    <bean id="oidc.messageEncoderFactory"
+        class="net.shibboleth.oidc.profile.encoding.impl.OIDCResponseEncoderFactory"
+        p:messageEncoder-ref="oidc.nimbusEncoder" scope="prototype" />
+
+    <bean id="oidc.nimbusEncoder" class="net.shibboleth.oidc.profile.encoding.impl.SimpleNimbusResponseEncoder"
+        scope="prototype" p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier" init-method=""/>
+
+    <bean id="RecordResponseComplete" class="net.shibboleth.idp.profile.impl.RecordResponseComplete"
+        scope="prototype" />
+
+</beans>
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
new file mode 100644
index 0000000..06d81be
--- /dev/null
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
@@ -0,0 +1,213 @@
+<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"
+    parent="oidfed/resolve-trust-chains">
+
+    <action-state id="InitializeProfileRequestContext">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="PopulateMetricContext" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="InitializeMandatoryContexts" />
+    </action-state>
+
+    <action-state id="InitializeMandatoryContexts">
+        <on-entry>
+            <evaluate expression="opensamlProfileRequestContext.ensureInboundMessageContext()"/>
+            <evaluate expression="opensamlProfileRequestContext.ensureOutboundMessageContext()"/>
+            <evaluate expression="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.profile.context.RelyingPartyContext))" />
+        </on-entry>
+        <evaluate expression="FlowStartPopulateAuditContext" />
+        <evaluate expression="DecodeMessage" />
+        <evaluate expression="PostDecodePopulateAuditContext" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="CheckIfContainsAuthentication" />
+    </action-state>
+
+    <decision-state id="CheckIfContainsAuthentication">
+        <on-entry>
+            <set name="flowScope.skipOAuth2ClientAuth" value="opensamlProfileRequestContext.getInboundMessageContext().getMessage().getClientAuthentication() == null" />
+        </on-entry>
+        <if test="flowScope.skipOAuth2ClientAuth"
+            then="SelectConfiguration" else="CheckIfResolveTrustChains" />
+    </decision-state>
+
+    <decision-state id="CheckIfResolveTrustChains">
+        <on-entry>
+            <evaluate expression="InitializeAuthenticationContext" />
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).getAuthenticationStateMap().put('idp.resolve-entity.resolveForAuthentication', 'true')"/>
+            <set name="conversationScope.automaticallyRegistered" value="false" />
+            <evaluate expression="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.profile.context.RelyingPartyContext)).setRelyingPartyId(opensamlProfileRequestContext.getInboundMessageContext()?.getMessage().getClientAuthentication().getClientID()?.getValue())" />
+            <evaluate expression="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.profile.context.RelyingPartyContext)).setVerified(true)" />
+        </on-entry>
+        <if test="ResolveTrustChainsCondition.test(opensamlProfileRequestContext)"
+            then="InitializeResolveTrustChains" else="SelectConfiguration" />
+    </decision-state>
+
+    <action-state id="InitializeResolveTrustChains">
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="ChooseResolutionMethod">
+            <set name="flowScope.transitionAfterTrustChainResolution" value="'CheckIfContinueTrustChainResolution'" />
+            <set name="flowScope.transitionOnNoTrustChainsResolved" value="'NoTrustChainsResolved'" />
+            <set name="flowScope.transitionForReselectTrustChain" value="'SelectFederationConfiguration'" />
+        </transition>
+    </action-state>
+
+    <decision-state id="CheckIfContinueTrustChainResolution">
+        <if test="TrustChainCandidatesExist.test(opensamlProfileRequestContext)"
+            then="SelectFederationConfiguration" else="SelectConfiguration" />
+    </decision-state>
+
+    <action-state id="SelectFederationConfiguration">
+        <evaluate expression="SelectTrustChain" />
+        <evaluate expression="ResolveTrustMarks" />
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+        <evaluate expression="SelectProfileConfiguration" />
+        <evaluate expression="ValidateTrustMarks" />
+        <evaluate expression="ValidateResolveEntityProfileConfiguration" />
+        <evaluate expression="'proceed'" />
+        <transition on="ReselectTrustChain" to="#{transitionForReselectTrustChain}" />
+        <transition on="proceed" to="SelectConfiguration">
+            <set name="conversationScope.automaticallyRegistered" value="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))" />
+        </transition>
+    </action-state>
+
+    <action-state id="SelectConfiguration">
+        <on-entry>
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))?.getAuthenticationStateMap()?.put('idp.resolve-entity.resolveForAuthentication', 'false')"/>
+        </on-entry>
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+        <evaluate expression="SelectProfileConfiguration" />
+        <evaluate expression="CallInboundMessageHandler" />
+        <evaluate expression="PostLookupPopulateAuditContext" />
+        <evaluate expression="PopulateInboundInterceptContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckInboundInterceptContext" />
+    </action-state>
+
+    <decision-state id="CheckInboundInterceptContext">
+        <on-entry>
+            <evaluate expression="CallInboundMessageHandler" />
+        </on-entry>
+        <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+            then="#{skipOAuth2ClientAuth ? 'ResumeAfterAuthentication' : 'DoAuthenticationSubflow'}" else="DoInboundInterceptSubflow" />
+    </decision-state>
+
+    <subflow-state id="DoInboundInterceptSubflow" subflow="intercept">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="#{skipOAuth2ClientAuth ? 'ResumeAfterAuthentication' : 'DoAuthenticationSubflow'}" />
+    </subflow-state>
+
+    <subflow-state id="DoAuthenticationSubflow" subflow="authn">
+        <input name="calledAsSubflow" value="true" />
+        <input name="bypassSessionActions" value="true" />
+        <input name="potentialFlows" value="getActiveFlow().getApplicationContext().getBean('shibboleth.oidfed.resolve-entity.PotentialFlows')" />
+        <transition on="proceed" to="ResumeAfterAuthentication" />
+        <transition on="RestartAuthentication" to="DoAuthenticationSubflow" />
+    </subflow-state>
+
+    <!-- Authentication subflow happens here. -->
+
+    <action-state id="ResumeAfterAuthentication">
+        <evaluate expression="ValidateRequest" />
+        <evaluate expression="LookupCachedResolveEntityResponse" />
+        <evaluate expression="'proceed'" />
+        <transition on="CachedResponseFound" to="BuildResponseMessage" />
+        <transition on="proceed" to="ResolveSubjectTrustChains" />
+    </action-state>
+
+    <action-state id="ResolveSubjectTrustChains">
+        <evaluate expression="ResolveTrustChains" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="SelectSubjectTrustChain" />
+    </action-state>
+
+    <action-state id="SelectSubjectTrustChain">
+        <evaluate expression="SelectTrustChain" />
+        <evaluate expression="ValidateSelectedTrustChain" />
+        <evaluate expression="ResolveTrustMarks" />
+        <evaluate expression="ValidateTrustMarks" />
+        <evaluate expression="'proceed'" />
+        <transition on="ReselectTrustChain" to="SelectSubjectTrustChain" />
+        <transition on="proceed" to="BuildResponse" />
+    </action-state>
+
+    <action-state id="BuildResponse">
+        <evaluate expression="PopulateResolveResponseSignatureSigningParameters" />
+        <evaluate expression="BuildResolveResponse" />
+        <evaluate expression="SignResolveResponse" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="BuildResponseMessage" />
+    </action-state>
+
+
+
+    <action-state id="HandleError">
+        <on-entry>
+            <evaluate
+                expression="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.SpringRequestContext)).setRequestContext(flowRequestContext)" />
+            <evaluate expression="LogEvent" />
+        </on-entry>
+        <evaluate expression="BuildErrorResponseFromEvent" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="PopulateOutboundInterceptContext"/>
+    </action-state>
+
+    <action-state id="BuildResponseMessage">
+        <evaluate expression="FormOutboundMessage" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="PopulateOutboundInterceptContext" />
+    </action-state>
+
+    <action-state id="PopulateOutboundInterceptContext">
+        <evaluate expression="PopulateOutboundInterceptContext" />
+         <evaluate expression="'proceed'" />
+         <transition on="proceed" to="CheckOutboundInterceptContext" />
+     </action-state>
+
+    <decision-state id="CheckOutboundInterceptContext">
+        <on-entry>
+            <evaluate expression="flowRequestContext.getFlowScope().get('transitionAfterOutboundIntercept') != null ? flowRequestContext.getFlowScope().get('transitionAfterOutboundIntercept') : 'CommitResponse'" result="flowScope.postOutboundInterceptTransition"/>
+            <evaluate expression="PopulateOutboundInterceptContext" />
+        </on-entry>
+        <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+            then="#{postOutboundInterceptTransition}" else="DoOutboundInterceptSubflow" />
+    </decision-state>
+
+    <subflow-state id="DoOutboundInterceptSubflow" subflow="intercept">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="#{postOutboundInterceptTransition}" />
+        <transition to="HandleError" />
+    </subflow-state>
+
+    <!-- Passthrough state if an exception is thrown. -->
+    <action-state id="LogRuntimeException">
+        <on-entry>
+            <evaluate
+                expression="T(org.slf4j.LoggerFactory).getLogger('net.shibboleth.idp.plugin.oidc.op.profile').error('Uncaught runtime exception', flowExecutionException.getCause())" />
+        </on-entry>
+        <evaluate expression="'RuntimeException'" />
+        <transition to="HandleError" />
+    </action-state>
+    
+    <!-- end state -->
+    <end-state id="CommitResponse">
+        <on-entry>
+            <evaluate expression="CallOutboundMessageHandler" />
+            <evaluate expression="EncodeMessage" />
+            <evaluate expression="PostResponsePopulateAuditContext" />
+            <evaluate expression="WriteAuditLog" />
+            <evaluate expression="RecordResponseComplete" />
+        </on-entry>
+    </end-state>
+
+    <!-- all unhandled non proceed results are turned into errors -->
+    <global-transitions>
+        <transition on-exception="java.lang.RuntimeException" to="LogRuntimeException" />
+        <transition on="#{!'proceed'.equals(currentEvent.id)}" to="HandleError" />
+    </global-transitions>
+
+    <bean-import resource="resolve-entity-beans.xml" />
+
+</flow>
\ No newline at end of file
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index d947aa6..428d517 100644
--- a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -19,6 +19,29 @@
         p:authorityHints="%{idp.oidfed.entityConfiguration.authorityHints:https://example.org}"
         p:optionalClaimsLookupStrategies-ref="shibboleth.oidfed.EntityConfigurationClaimsLookupStrategies" />
 
+    <bean id="OIDFED.ResolveEntity" parent="AbstractOIDFederationProfile" lazy-init="true"
+        class="net.shibboleth.oidfed.profile.config.impl.DefaultOIDFederationResolveEntityProfileConfiguration"
+        p:issuer-ref="shibboleth.oidfed.entityId"
+        p:tokenEndpointAuthMethods="%{idp.oidfed.resolveEntity.endpointAuthMethods:private_key_jwt}"
+        p:useTargetedEndpointAsJWTAudience="%{idp.oidfed.resolveEntity.targetedEndpointAsJWTAudience:true}"
+        p:requireSingleJWTAudience="%{idp.oidfed.resolveEntity.requireSingleJWTAudience:true}">
+        <property name="claimsValidator">
+            <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator">
+                <property name="claimValidators">
+                    <util:list value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+                        <ref bean="OIDFedClientAuthenticationExpiryClaimsValidator" />
+                        <ref bean="OIDFedClientAuthenticationNotBeforeClaimsValidator" />
+                        <ref bean="OIDFedClientAuthenticationIssuedAtClaimsValidator" />
+                        <ref bean="OIDFedClientAuthenticationIssuerClaimsValidator" />
+                        <ref bean="OIDFedClientAuthenticationSubjectClaimsValidator" />
+                        <ref bean="OIDFedClientAuthenticationDefaultAuthenticationAudienceClaimsValidator" />
+                        <ref bean="OIDFedClientAuthenticationJWTIdentifierClaimsValidator" />
+                    </util:list>
+                </property>
+            </bean>
+        </property>
+    </bean>
+
     <bean id="shibboleth.oidfed.SigningConfiguration"
         parent="shibboleth.oidfed.BasicSignatureSigningConfiguration"
         p:signingCredentials-ref="shibboleth.oidfed.SigningCredentialsFactory">
@@ -62,8 +85,7 @@
                         class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
                         <constructor-arg>
                             <list>
-                                <bean class="net.shibboleth.oidfed.security.credential.ClientInformationFederationEntityCredentialResolver"
-                                    c:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper">
+                                <bean class="net.shibboleth.oidfed.security.credential.SelectedTrustChainFederationEntityCredentialResolver">
                                     <constructor-arg name="entityConfigurationCredentialResolver">
                                         <bean class="net.shibboleth.oidfed.security.credential.DefaultEntityConfigurationCredentialResolver" />
                                     </constructor-arg>
@@ -112,6 +134,61 @@
         class="net.shibboleth.oidfed.profile.navigate.DefaultEntityConfigurationTrustMarksLookupStrategy"
         p:trustMarkLookupStrategies-ref="shibboleth.oidfed.DefaultTrustMarkLookupStrategies"/>
 
+    <bean id="OIDFedClientAuthenticationExpiryClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+    <bean id="OIDFedClientAuthenticationNotBeforeClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.NotBeforeClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+        
+    <bean id="OIDFedClientAuthenticationIssuedAtClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.IssuedAtClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+        p:messageLifetime="%{idp.policy.messageLifetime:PT1M}"
+        p:requiredRule="false" />
+
+    <bean id="OIDFedClientAuthenticationIssuerClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+        p:claimName="iss">
+        <property name="valueToMatchLookupStrategy">
+            <bean class="net.shibboleth.shared.logic.BiFunctionSupport"
+                factory-method="forFunctionOfFirstArg"
+                c:_0-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+        </property>
+    </bean>
+
+    <bean id="OIDFedClientAuthenticationSubjectClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+        p:claimName="sub">
+        <property name="valueToMatchLookupStrategy">
+            <bean class="net.shibboleth.shared.logic.BiFunctionSupport"
+                factory-method="forFunctionOfFirstArg"
+                c:_0-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+        </property>
+    </bean>
+
+    <bean id="OIDFedClientAuthenticationDefaultAuthenticationAudienceClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.JWTAuthenticationAudienceClaimsValidator">
+        <property name="audienceLookupStrategy">
+            <bean parent="shibboleth.BiFunctions.Expression"
+                c:expression="#custom.get().getRequestURL().toString()"
+                p:customObject-ref="shibboleth.HttpServletRequestSupplier" />
+        </property>
+        <property name="extraAudienceValidationCondition">
+            <bean class="net.shibboleth.oidc.profile.config.navigate.RequireSingleJWTAudienceLookupFunction"/>
+        </property>
+    </bean>
+
+    <bean id="OIDFedClientAuthenticationJWTIdentifierClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+        p:replayCache-ref="shibboleth.ReplayCache" />
+
+
+    <bean id="RelyingPartyByTrustAnchor" abstract="true" parent="RelyingParty"
+        class="net.shibboleth.oidfed.profile.context.RelyingPartyConfigurationSupport" factory-method="byTrustAnchor" />
+
     <import resource="${idp.home}/conf/oidfed/oidfed-entity-configuration-claims.xml"/>
 
 </beans>
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java
index 38b2ed1..3cf3410 100644
--- a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java
@@ -723,7 +723,6 @@ public class AbstractFederationFlowTest extends AbstractFlowTest {
 
     protected ErrorResponse parseErrorResponse(final FlowExecutionResult result, final String message) {
         final Response response = parseResponse(result);
-        Assert.assertFalse(response.indicatesSuccess(), message);
         Assert.assertTrue(response instanceof ErrorResponse, message);
         return (ErrorResponse) response;
     }
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/ResolveEntityFlowTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/ResolveEntityFlowTest.java
new file mode 100644
index 0000000..ef70d12
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/ResolveEntityFlowTest.java
@@ -0,0 +1,455 @@
+/*
+ * 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.flow;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.time.Instant;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.codec.binary.Base64;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.StorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ErrorResponse;
+import com.nimbusds.oauth2.sdk.Response;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.impl.SubordinateStatementImpl;
+import net.shibboleth.oidfed.testing.FederationJwtSupport;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.minidev.json.JSONObject;
+
+/**
+ * Flow tests for the OpenID federation resolve entity flow.
+ */
+public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
+    
+    public static final String FLOW_ID = "oidfed/resolve-entity";
+
+    @Autowired
+    @Qualifier("shibboleth.StorageService")
+    StorageService storageService;
+
+    public ResolveEntityFlowTest() {
+        super(FLOW_ID);
+    }
+    
+    @Test
+    public void testInvalidMethod() throws Exception {
+        setJsonRequest("POST", "{}");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_request");
+    }
+
+    @Test
+    public void testNoContentType() throws Exception {
+        request.setMethod("GET");
+        request.setQueryString("sub=mockClientId&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_request");
+    }
+
+    @Test
+    public void testInvalidSubject() throws Exception {
+        request.setMethod("GET");
+        request.setContentType("application/x-www-form-urlencoded");
+        request.setQueryString("sub=mockClientId&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_subject");
+    }
+
+    @Test
+    public void testUntrustedAnchor() throws Exception {
+        request.setMethod("GET");
+        final String clientId = uniqueClientId();
+        rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        request.setQueryString("sub=" + clientId + "&trust_anchor=mockAnchors&entity_type=openid_relying_party");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_trust_anchor");
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor() throws Exception {
+        request.setMethod("GET");
+        final String clientId = uniqueClientId();
+        rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final ResolveEntityResponse parsedResponse =
+                parseSuccessResponse(result, ResolveEntityResponse.class);
+        final SignedJWT response = parsedResponse.getJWT();
+        Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+        Assert.assertEquals(response.getJWTClaimsSet().getSubject(), clientId);
+        Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+        Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor_validTrustMark() throws Exception {
+        request.setMethod("GET");
+        final String clientId = uniqueClientId();
+        final String trustMark = FederationJwtSupport.trustMark(JWSAlgorithm.RS256, trustMarkIssuerKey, trustMarkIssuerId,
+                clientId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        final String rpEntityConfiguration = rpEntityConfiguration(clientId, metadata, List.of(Map.of(
+                "trust_mark_type", "https://example.org/email-allowing-trust-mark",
+                "trust_mark", trustMark)), leafKey);
+        rpConfigureMockHttpClient(clientId, rpEntityConfiguration);
+        try {
+            mapResponse(entityConfigurationUrl(trustMarkIssuerId),
+                    mockResponse(trustMarkIssuerConfiguration(trustMarkIssuerId)));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, trustMarkIssuerId),
+                    mockResponse(subordinateStatement(trustMarkIssuerId,
+                            Map.of("federation_entity", CollectionSupport.emptyMap()), trustMarkIssuerKey)));
+            mapResponse(trustMarkStatusEndpoint, mockResponse(200, "application/trust-mark-status-response+jwt",
+                    trustMarkStatusResponse(trustMarkIssuerId, trustMark, "active", trustMarkIssuerKey)));
+        } catch (UnsupportedOperationException | IOException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+        request.setContentType("application/x-www-form-urlencoded");
+        request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final ResolveEntityResponse parsedResponse =
+                parseSuccessResponse(result, ResolveEntityResponse.class);
+        final SignedJWT response = parsedResponse.getJWT();
+        Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+        Assert.assertEquals(response.getJWTClaimsSet().getSubject(), clientId);
+        Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+        Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
+        final Map<String,Object> trustMarks = response.getJWTClaimsSet().getJSONObjectClaim("trust_marks");
+        Assert.assertNotNull(trustMarks, "Could not find trust marks for client " + clientId);
+        Assert.assertEquals(trustMarks.size(), 1);
+        Assert.assertEquals(trustMarks.get("https://example.org/email-allowing-trust-mark"), trustMark);
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor_subordinateKeyNotMatchingEntityConfiguration() throws Exception {
+        request.setMethod("GET");
+        final String clientId = uniqueClientId();
+        rpConfigureMockHttpClient(clientId, initializeNewJwk("RSA", 2048, "mockNewLeafKey"));
+        request.setContentType("application/x-www-form-urlencoded");
+        request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_request");
+        assertErrorDescriptionContains(result, "NoTrustChainsResolved", "");
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchorInvalidMetadata() throws Exception {
+        request.setMethod("GET");
+        final String clientId = uniqueClientId();
+        rpConfigureMockHttpClient(clientId, new JSONObject(Map.of("response_types", "invalid")));
+        request.setContentType("application/x-www-form-urlencoded");
+        request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_metadata");
+    }
+
+    @Test
+    public void testOPWithTrustedTrustAnchor() throws Exception {
+        request.setMethod("GET");
+        final String entityId = uniqueClientId();
+        opConfigureMockHttpClient(entityId);
+        request.setContentType("application/x-www-form-urlencoded");
+        request.setQueryString("sub=" + entityId + "&trust_anchor=" + anchorId + "&entity_type=openid_provider");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final ResolveEntityResponse parsedResponse =
+                parseSuccessResponse(result, ResolveEntityResponse.class);
+        final SignedJWT response = parsedResponse.getJWT();
+        Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+        Assert.assertEquals(response.getJWTClaimsSet().getSubject(), entityId);
+        Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+        Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
+    }
+
+    @Test
+    public void testOPWithTrustedTrustAnchorInvalidMetadata() throws Exception {
+        request.setMethod("GET");
+        final String entityId = uniqueClientId();
+        opConfigureMockHttpClient(entityId, new JSONObject(Map.of("issuer", List.of("unexpected", "values"))));
+        request.setContentType("application/x-www-form-urlencoded");
+        request.setQueryString("sub=" + entityId + "&trust_anchor=" + anchorId + "&entity_type=openid_provider");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_metadata");
+    }
+
+    @Test
+    public void testOPWithTrustedTrustAnchor_emptyMetadataPolicyCrit() throws Exception {
+        request.setMethod("GET");
+        final String entityId = uniqueClientId();
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+                .claim("metadata", Map.of("openid_provider", emptyOpMetadata(entityId).toJSONObject()))
+                .build();
+        final ObjectMapper objectMapper = payloadObjectMapper;
+        assert objectMapper != null;
+        final SignedJWT jwt = FederationJwtSupport.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet);
+        assert jwt != null;
+        final EntityStatement<?> subordinateStatement = SubordinateStatementImpl.parse(jwt, objectMapper);
+        try {
+            mapResponse(entityConfigurationUrl(entityId), mockResponse(opEntityConfiguration(entityId)));
+            mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                    mockResponse(subordinateStatement.getJwt().serialize()));
+        } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+        request.setContentType("application/x-www-form-urlencoded");
+        request.setQueryString("sub=" + entityId + "&trust_anchor=" + anchorId + "&entity_type=openid_provider");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final ResolveEntityResponse parsedResponse =
+                parseSuccessResponse(result, ResolveEntityResponse.class);
+        final SignedJWT response = parsedResponse.getJWT();
+        Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+        Assert.assertEquals(response.getJWTClaimsSet().getSubject(), entityId);
+        Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+        Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor_jwtAuth_successWithLeafKey() throws Exception {
+        request.setMethod("POST");
+        final String requestingClientId = uniqueClientId();
+        final String clientId = uniqueClientId();
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .issuer(requestingClientId)
+                .subject(requestingClientId)
+                .audience(issuer)
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .jwtID(idGenerator.generateIdentifier())
+                .build();
+
+        final SignedJWT jwt = createPrivateKeyJWT(claimsSet, leafKey.toRSAKey().toRSAPrivateKey());
+        final Map<String, String> requestParams = new HashMap<>();
+        requestParams.put("sub", clientId);
+        requestParams.put("trust_anchor", anchorId);
+        requestParams.put("entity_type", "openid_relying_party");
+        populateClientAssertionParams(requestParams, jwt);
+        rpConfigureMockHttpClient(requestingClientId);
+        rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        setHttpFormRequest(request, "POST", requestParams);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final ResolveEntityResponse parsedResponse =
+                parseSuccessResponse(result, ResolveEntityResponse.class);
+        final SignedJWT response = parsedResponse.getJWT();
+        Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+        Assert.assertEquals(response.getJWTClaimsSet().getSubject(), clientId);
+        Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+        Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor_basicAuth_successForLocalClient() throws Exception {
+        request.setMethod("POST");
+        final String registeredClientId = "localResolveEntityClient";
+        final String secret = "mockClientSecret";
+        final String clientId = uniqueClientId();
+        final Map<String, String> requestParams = new HashMap<>();
+        requestParams.put("sub", clientId);
+        requestParams.put("trust_anchor", anchorId);
+        requestParams.put("entity_type", "openid_relying_party");
+        setBasicAuth(registeredClientId, secret);
+
+        rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        setHttpFormRequest(request, "POST", requestParams);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final ResolveEntityResponse parsedResponse =
+                parseSuccessResponse(result, ResolveEntityResponse.class);
+        final SignedJWT response = parsedResponse.getJWT();
+        Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+        Assert.assertEquals(response.getJWTClaimsSet().getSubject(), clientId);
+        Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+        Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor_basicAuth_failForLocalClientWithDefaultConfig() throws Exception {
+        request.setMethod("POST");
+        final String registeredClientId = "localDefaultClient";
+        final String secret = "mockClientSecret";
+        final String clientId = uniqueClientId();
+        final Map<String, String> requestParams = new HashMap<>();
+        requestParams.put("sub", clientId);
+        requestParams.put("trust_anchor", anchorId);
+        requestParams.put("entity_type", "openid_relying_party");
+        setBasicAuth(registeredClientId, secret);
+
+        rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        setHttpFormRequest(request, "POST", requestParams);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "unauthorized_client");
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor_basicAuth_failWithGet() throws Exception {
+        request.setMethod("GET");
+        final String registeredClientId = "localResolveEntityClient";
+        final String secret = "mockClientSecret";
+        final String clientId = uniqueClientId();
+        request.setContentType("application/x-www-form-urlencoded");
+        request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+        setBasicAuth(registeredClientId, secret);
+
+        rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_request");
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor_jwtAuth_failWithRpfKey() throws Exception {
+        request.setMethod("POST");
+        final String requestingClientId = uniqueClientId();
+        final String clientId = uniqueClientId();
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .issuer(requestingClientId)
+                .subject(requestingClientId)
+                .audience(issuer)
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .jwtID(idGenerator.generateIdentifier())
+                .build();
+
+        final SignedJWT jwt = createPrivateKeyJWT(claimsSet, rpKey.toRSAKey().toRSAPrivateKey());
+        final Map<String, String> requestParams = new HashMap<>();
+        requestParams.put("sub", clientId);
+        requestParams.put("trust_anchor", anchorId);
+        requestParams.put("entity_type", "openid_relying_party");
+        populateClientAssertionParams(requestParams, jwt);
+        rpConfigureMockHttpClient(requestingClientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        setHttpFormRequest(request, "POST", requestParams);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_client");
+    }
+
+    protected Response parseResponse(final FlowExecutionResult result) {
+        assertFlowExecutionOutcome(result.getOutcome(), END_STATE_ID);
+        final ProfileRequestContext prc = retrieveProfileRequestContext(result);
+        Assert.assertNotNull(prc);
+        assert prc != null;
+        Assert.assertNotNull(prc.ensureOutboundMessageContext());
+        final Object responseMessage = prc.ensureOutboundMessageContext().getMessage();
+        Assert.assertNotNull(responseMessage);
+        Assert.assertTrue(responseMessage instanceof Response);
+        return (Response) responseMessage;
+    }
+    
+    protected <AResponseType extends Response> AResponseType parseSuccessResponse(final FlowExecutionResult result,
+            final Class<AResponseType> clazz) {
+        final Response response = parseResponse(result);
+        if (response.indicatesSuccess()) {
+            Assert.assertTrue(clazz.isInstance(response));
+            return clazz.cast(response);
+        }
+        return null;
+    }
+
+    protected void assertErrorCode(final FlowExecutionResult result, final String errorCode) {
+        assertErrorCode(result, errorCode, null);
+    }
+
+    protected void assertErrorCode(final FlowExecutionResult result, final String errorCode,
+            final String message) {
+        final Response response = parseResponse(result);
+        Assert.assertTrue(response instanceof ErrorResponse);
+        final ErrorResponse errorResponse = (ErrorResponse) response;
+        Assert.assertEquals(errorResponse.getErrorObject().getCode(), errorCode, message);
+    }
+
+    protected void setJsonRequest(final String method, final String body) {
+        setRequest(method, body, "application/json");
+    }
+    
+    protected void setHttpFormRequest(final String method, final Map<String, String> parameters) {
+        setHttpFormRequest(request, method, parameters);
+    }
+
+    protected static void setHttpFormRequest(final MockHttpServletRequest request, final String method, final Map<String, String> parameters) {
+        setRequest(request, method, "", "application/x-www-form-urlencoded");
+        request.setParameters(parameters);
+    }
+
+    protected void setBasicAuth(final String username, final String password) {
+        request.removeHeader("Authorization");
+        request.addHeader("Authorization",
+                "Basic " + new String(Base64.encodeBase64(new String(username + ":" + password).getBytes())));
+    }
+    
+    protected void setRequest(final String method, final String body, final String contentType) {
+        setRequest(request, method, body, contentType);
+    }
+
+    protected static void setRequest(final MockHttpServletRequest request, final String method, final String body, final String contentType) {
+        request.setMethod(method);
+        request.setContentType(contentType);
+        request.setContent(body.getBytes());   
+    }
+
+    protected static SignedJWT createPrivateKeyJWT(final JWTClaimsSet claimsSet, final RSAPrivateKey rsaPrivateKey)
+            throws JOSEException {
+        return createPrivateKeyJWT(claimsSet, rsaPrivateKey, JWSAlgorithm.RS256);
+    }    
+
+    protected static SignedJWT createPrivateKeyJWT(final JWTClaimsSet claimsSet, final RSAPrivateKey rsaPrivateKey,
+            final JWSAlgorithm algorithm) throws JOSEException {
+        final SignedJWT jwt = new SignedJWT(new JWSHeader(algorithm), claimsSet);
+        final RSASSASigner signer = new RSASSASigner(rsaPrivateKey);
+        jwt.sign(signer);
+        return jwt;
+    }    
+
+    protected static SignedJWT createPrivateKeyJWT(final JWTClaimsSet claimsSet, final ECPrivateKey ecPrivateKey,
+            final JWSAlgorithm algorithm) throws JOSEException {
+        final SignedJWT jwt = new SignedJWT(new JWSHeader(algorithm), claimsSet);
+        final ECDSASigner signer = new ECDSASigner(ecPrivateKey);
+        jwt.sign(signer);
+        return jwt;
+    }
+
+}
diff --git a/oidfed-common-conf-impl/src/test/resources/credentials/htpasswd.txt b/oidfed-common-conf-impl/src/test/resources/credentials/htpasswd.txt
new file mode 100644
index 0000000..882b85e
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/credentials/htpasswd.txt
@@ -0,0 +1,2 @@
+localDefaultClient:$apr1$2dj74pi3$XKh7CjYBTUxzeMbhRSxlr1
+localResolveEntityClient:$apr1$503cv9z7$4ogERgiLlrAALMa4xDaKg/
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/authn/oauth2client-authn-config.xml
similarity index 57%
copy from oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
copy to oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/authn/oauth2client-authn-config.xml
index 9ab8124..032594b 100644
--- a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/authn/oauth2client-authn-config.xml
@@ -11,23 +11,20 @@
                            
        default-init-method="initialize"
        default-destroy-method="destroy">
-
-    <bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
-        <property name="profileConfigurations">
-            <list>
-                <bean parent="OIDFED.Configuration" p:cachedSuccessResponseLifetime="PT2S" />
-            </list>
-        </property>
-    </bean>
-
-    <bean id="shibboleth.DefaultRelyingParty" parent="RelyingParty.MDDriven">
-        <property name="profileConfigurations">
-            <list>
-            </list>
-        </property>
-    </bean>
-
-    <util:list id="shibboleth.RelyingPartyOverrides">
+    
+    <!-- Ordered list of CredentialValidators to apply to a request. -->
+    <util:list id="shibboleth.authn.OAuth2Client.Validators">
+        <ref bean="shibboleth.OIDCClientInfoValidator" />
+        <ref bean="shibboleth.JWTValidator" />
+        <bean parent="shibboleth.HTPasswdValidator">
+            <property name="resource">
+                <bean class="net.shibboleth.shared.spring.resource.ConditionalResource">
+                    <constructor-arg>
+                        <bean class="org.springframework.core.io.ClassPathResource" c:path="/credentials/htpasswd.txt" />
+                     </constructor-arg>
+                </bean>
+            </property>
+        </bean>
     </util:list>
-
+    
 </beans>
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 9ab8124..4ca29ed 100644
--- a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -16,6 +16,7 @@
         <property name="profileConfigurations">
             <list>
                 <bean parent="OIDFED.Configuration" p:cachedSuccessResponseLifetime="PT2S" />
+                <bean parent="OIDFED.ResolveEntity" />
             </list>
         </property>
     </bean>
@@ -28,6 +29,21 @@
     </bean>
 
     <util:list id="shibboleth.RelyingPartyOverrides">
+        <bean parent="RelyingPartyByTrustAnchor" c:trustAnchorIds="https://trust-anchor.federation.local">
+            <property name="profileConfigurations">
+                <list>
+                    <!-- Enabled for federation-authenticated clients -->
+                    <bean parent="OIDFED.ResolveEntity" />
+                </list>
+            </property>
+        </bean>
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="localResolveEntityClient">
+            <property name="profileConfigurations">
+                <list>
+                    <bean parent="OIDFED.ResolveEntity" p:tokenEndpointAuthMethods="client_secret_basic"/>
+                </list>
+            </property>
+        </bean>
     </util:list>
 
 </beans>
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/decoding/impl/ResolveEntityRequestDecoder.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/decoding/impl/ResolveEntityRequestDecoder.java
new file mode 100644
index 0000000..ea07356
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/decoding/impl/ResolveEntityRequestDecoder.java
@@ -0,0 +1,110 @@
+/*
+ * 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.decoding.impl;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.opensaml.messaging.decoder.servlet.AbstractHttpServletRequestMessageDecoder;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.http.JakartaServletUtils;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Message decoder decoding OpenID Federation Resolve Entity request {@link ResolveEntityRequest}.
+ */
+public class ResolveEntityRequestDecoder extends AbstractHttpServletRequestMessageDecoder {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ResolveEntityRequestDecoder.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doDecode() throws MessageDecodingException {
+        final HttpServletRequest request = getHttpServletRequest();
+        assert request != null;
+        if (!"GET".equalsIgnoreCase(request.getMethod()) && !"POST".equalsIgnoreCase(request.getMethod())) {
+            throw new MessageDecodingException("This message decoder only supports the HTTP GET and POST methods");
+        }
+        if (!"application/x-www-form-urlencoded".equals(request.getContentType())) {
+            throw new MessageDecodingException("Invalid content type: " + request.getContentType());
+        }
+        try {
+            final HTTPRequest httpRequest = JakartaServletUtils.createHTTPRequest(request);
+            //TODO: protocol logging once base-class moved to oidc-common
+            //getProtocolMessageLogger().trace("Inbound request {}", RequestUtil.toString(httpRequest, null));
+            final URI uri = httpRequest.getURI();
+            if (uri == null) {
+                throw new MessageDecodingException("Could not parse request URI");
+            }
+            final ClientAuthentication clientAuthentication = ClientAuthentication.parse(httpRequest);
+            if (clientAuthentication != null && !"POST".equalsIgnoreCase(request.getMethod())) {
+                throw new MessageDecodingException(
+                        "This message decoder requires use of POST method when client authentication is involved");
+            }
+            if (clientAuthentication == null && !"GET".equalsIgnoreCase(request.getMethod())) {
+                throw new MessageDecodingException(
+                        "This message decoder requires use of GET method when client authentication is not involved");
+            }
+            final Map<String, List<String>> parameters;
+            if ("GET".equalsIgnoreCase(request.getMethod())) {
+                parameters = httpRequest.getQueryStringParameters();
+            } else {
+                parameters = httpRequest.getBodyAsFormParameters();
+            }
+            final String subject = Optional.ofNullable(parameters.get("sub"))
+                    .filter(Objects::nonNull)
+                    .filter(list -> list.size() == 1)
+                    .map(list -> list.get(0))
+                    .orElse(null);
+            if (subject == null) {
+                throw new MessageDecodingException("No single sub value in the request");
+            }
+            final List<String> trustAnchors = Optional.ofNullable(parameters.get("trust_anchor"))
+                    .filter(Objects::nonNull)
+                    .filter(list -> list.size() > 0)
+                    .orElse(null);
+            if (trustAnchors == null) {
+                throw new MessageDecodingException("No trust_anchor included in the request");
+            }
+            final ResolveEntityRequest requestMessage =
+                    new ResolveEntityRequest(uri, subject, trustAnchors, parameters.get("entity_type"),
+                            clientAuthentication);
+            log.debug("Successfully built request message object {}", requestMessage);
+            final MessageContext messageContext = new MessageContext();
+            messageContext.setMessage(requestMessage);
+            setMessageContext(messageContext);
+        } catch (final IOException | ParseException e) {
+            log.error("Could not create HTTP request from the request", e);
+            throw new MessageDecodingException(e);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java
new file mode 100644
index 0000000..20f6113
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java
@@ -0,0 +1,269 @@
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.EventContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.CurrentOrPreviousEventLookup;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ErrorObject;
+import com.nimbusds.oauth2.sdk.ErrorResponse;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.profile.messaging.JSONErrorResponse;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.oidfed.metadata.cache.ResponseContainerExpirationCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.NimbusResponseCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.ResolveEntityRequestCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.ResolveEntityResponseContainer;
+import net.shibboleth.oidfed.profile.config.navigate.CachedErrorResponseLifetimeLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * This action reads an event from the configured {@link EventContext} lookup strategy, constructs a JSON error response
+ * message and attaches it as the outbound message. If {@link RelyingPartyCachedMessageContext} is found, it's exploited
+ * for storing the response message in the configured {@link #responseCache}.
+ */
+public class BuildResolveEntityErrorResponseFromEvent extends AbstractProfileAction {
+
+    /** Default value for the error code in the error response messages. */
+    public static final String DEFAULT_ERROR_CODE = "invalid_request";
+    
+    /** Default value for the HTTP response status code in the HTTP responses. */
+    public static final int DEFAULT_HTTP_STATUS_CODE = HTTPResponse.SC_BAD_REQUEST;
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(BuildResolveEntityErrorResponseFromEvent.class);
+
+    /** Strategy function for access to {@link EventContext} to check. */
+    @Nonnull
+    private Function<ProfileRequestContext, EventContext> eventContextLookupStrategy;
+
+    /** Map of eventIds to pre-configured error objects. */
+    private Map<String, ErrorObject> mappedErrors;
+
+    /** The status code for unmapped events. */
+    private int defaultStatusCode;
+
+    /** The code for unmapped events. */
+    private String defaultCode;
+
+    /** Metadata cache for cached response containers. */
+    @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> responseCache;
+
+    /** Strategy used to locate the lifetime for the cached response record. */
+    @Nonnull private Function<ProfileRequestContext,Duration> cachedResponseLifetimeLookupStrategy;
+
+    /** Strategy used to locate the resolve entity context. */
+    @Nonnull
+    private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> resolveEntityContextLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public BuildResolveEntityErrorResponseFromEvent() {
+        eventContextLookupStrategy = new CurrentOrPreviousEventLookup();
+        mappedErrors = new HashMap<>();
+        defaultStatusCode = DEFAULT_HTTP_STATUS_CODE;
+        defaultCode = DEFAULT_ERROR_CODE;
+        cachedResponseLifetimeLookupStrategy = new CachedErrorResponseLifetimeLookupFunction();
+        final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> recls =
+                new ChildContextLookup<>(RelyingPartyCachedMessageContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert recls != null;
+        resolveEntityContextLookupStrategy = recls;
+
+    }
+
+    /**
+     * Set the status code for unmapped events.
+     * 
+     * @param code The default status code for unmapped events.
+     */
+    public void setDefaultStatusCode(final int code) {
+        defaultStatusCode = code;
+    }
+
+    /**
+     * Set the code for unmapped events.
+     * 
+     * @param code The default status code for unmapped events.
+     */
+    public void setDefaultCode(@Nonnull final String code) {
+        defaultCode = Constraint.isNotNull(code, "Default code cannot be null");
+    }
+
+    /**
+     * Set lookup strategy for {@link EventContext} to check.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setEventContextLookupStrategy(@Nonnull final Function<ProfileRequestContext, EventContext> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+
+        eventContextLookupStrategy = Constraint.isNotNull(strategy, "EventContext lookup strategy cannot be null");
+    }
+
+    /**
+     * Set map of eventIds to pre-configured error objects.
+     * 
+     * @param errors map of eventIds to pre-configured error objects.
+     */
+    public void setMappedErrors(@Nonnull final Map<String, ErrorObject> errors) {
+        ifInitializedThrowUnmodifiabledComponentException();
+
+        mappedErrors = Constraint.isNotNull(errors, "Mapped errors cannot be null");
+    }
+
+    /**
+     * Set the metadata cache for cached response containers.
+     * 
+     * @param cache What to set.
+     */
+    public void setResponseCache(@Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+        checkSetterPreconditions();
+        responseCache = Constraint.isNotNull(cache, "Response metadata cache cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the lifetime for the cached response record.
+     * 
+     * @param strategy What to set.
+     */
+    public void setCachedResponseLifetimeLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, Duration> strategy) {
+        checkSetterPreconditions();
+        cachedResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the resolve entity context
+     * 
+     * @param strategy What to set.
+     */
+    public void setResolveEntityContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyCachedMessageContext> strategy) {
+        checkSetterPreconditions();
+        resolveEntityContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (responseCache == null) {
+            throw new ComponentInitializationException("Response metadata cache cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        if (profileRequestContext.getOutboundMessageContext() == null) {
+            log.debug("{} No outbound message context initialized, nothing to do", getLogPrefix());
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final EventContext eventCtx = eventContextLookupStrategy.apply(profileRequestContext);
+        if (eventCtx == null || eventCtx.getEvent() == null) {
+            log.error("{} No event to be included in the response, nothing to do", getLogPrefix());
+            return;
+        }
+        assert eventCtx != null;
+        final Object event = eventCtx.getEvent();
+        assert event != null;
+        final String eventValue = event.toString();
+        final ErrorObject error;
+        if (mappedErrors.containsKey(eventValue)) {
+            log.debug("{} Found mapped event for {}", getLogPrefix(), eventValue);
+            error = mappedErrors.get(eventValue);
+        } else {
+            log.debug("{} No mapped event found for {}, creating general {}", getLogPrefix(), eventValue, defaultCode);
+            error = new ErrorObject(defaultCode, eventValue, defaultStatusCode);
+        }
+        assert error != null;
+        final ErrorResponse errorResponse = buildErrorResponse(error, profileRequestContext);
+        if (errorResponse != null) {
+            profileRequestContext.ensureOutboundMessageContext().setMessage(errorResponse);
+            log.debug("{} ErrorResponse successfully set as the outbound message", getLogPrefix());
+        } else {
+            log.debug("{} Error response not formed", getLogPrefix());
+        }
+    }
+
+    protected JSONErrorResponse buildErrorResponse(@Nonnull final ErrorObject error,
+            @Nonnull final ProfileRequestContext profileRequestContext) {
+        final JSONErrorResponse response = new JSONErrorResponse(error);
+        final RelyingPartyCachedMessageContext resolveEntityContext =
+                resolveEntityContextLookupStrategy.apply(profileRequestContext);
+
+        final Duration cachedResponseLifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+        if (resolveEntityContext != null && cachedResponseLifetime != null &&
+                resolveEntityContext.getValidatedRequest() instanceof ResolveEntityRequest resolveEntityRequest) {
+            final NimbusResponseCriterion responseCriterion = new NimbusResponseCriterion(response);
+            final Instant expiration = Instant.now().plus(cachedResponseLifetime);
+            assert expiration != null;
+            final ResponseContainerExpirationCriterion expirationCriterion =
+                    new ResponseContainerExpirationCriterion(expiration);
+            final ResolveEntityRequestCriterion requestCriterion =
+                    new ResolveEntityRequestCriterion(resolveEntityRequest);
+            final CriteriaSet criteria = new CriteriaSet(requestCriterion, responseCriterion, expirationCriterion);
+            try {
+                final List<ResolveEntityResponseContainer> result = responseCache.get(criteria);
+                if (result.size() != 1) {
+                    log.error("{} Unexpected result (size={}) when storing response record into the metadata cache",
+                            getLogPrefix(), result.size());
+                } else {
+                    log.debug("{} Response stored into the cache", getLogPrefix());
+                }
+            } catch (final MetadataCacheException e) {
+                log.error("{} Could not store the response record into the metadata cache", getLogPrefix(), e);
+            }
+        }
+
+        return response;
+    }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityResponse.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityResponse.java
new file mode 100644
index 0000000..b5c45e6
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityResponse.java
@@ -0,0 +1,204 @@
+/*
+ * 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.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+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.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.TrustMark;
+import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
+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.DefaultEntityTypesLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action that uses the information from {@link RelyingPartyTrustChainContext} for creating a new JWT to be used for
+ * creating a response to OpenID Federation Resolve Entity API.
+ */
+public class BuildResolveEntityResponse extends AbstractBuildEntityStatementAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(BuildResolveEntityResponse.class);
+
+    /** Strategy used to lookup the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /** Strategy used to lookup the entity types included in the response metadata. */
+    @Nonnull private Function<ProfileRequestContext, List<String>> entityTypesLookupStrategy;
+
+    /** Trust chain context to operate on. */
+    @NonnullBeforeExec private RelyingPartyTrustChainContext trustChainContext;
+
+    /** Constructor. */
+    public BuildResolveEntityResponse() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+        entityTypesLookupStrategy = new DefaultEntityTypesLookupFunction();
+    }
+    
+    /**
+     * Set the strategy used to lookup the trust chain context.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustChainContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+        checkSetterPreconditions();
+        trustChainContextLookupStrategy =
+                Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup the entity types included in the response metadata.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setEntityTypesLookupStrategy(@Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+        checkSetterPreconditions();
+        entityTypesLookupStrategy = Constraint.isNotNull(strategy, "EntityTypesLookupStrategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        trustChainContext = trustChainContextLookupStrategy.apply(profileRequestContext);
+        if (trustChainContext == null) {
+            log.error("{} Unable to locate trust chain context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean populateClaimsSetBuilder(@Nonnull final JWTClaimsSet.Builder builder,
+            @Nonnull final ProfileRequestContext profileRequestContext) {
+        final VerifiedTrustChain selectedTrustChain = trustChainContext.getSelectedTrustChain();
+        if (selectedTrustChain == null) {
+            log.debug("{} No selected trust chain found form the context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_METADATA);
+            return false;                                    
+        }
+        final Metadata metadata = selectedTrustChain.getMetadata();
+        final List<String> entityTypes = entityTypesLookupStrategy.apply(profileRequestContext);
+        log.trace("{} The following entity types were requested: {}", getLogPrefix(), entityTypes);
+        if (entityTypes != null && !entityTypes.isEmpty()) {
+            final Map<String,Object> filteredMetadata = metadata.getAllClaims().entrySet()
+                    .stream()
+                    .filter(entry -> entityTypes.contains(entry.getKey()))
+                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+            if  (filteredMetadata.isEmpty()) {
+                log.warn("{} No metadata for entity types {} found for the selected trust chain", getLogPrefix(),
+                        entityTypes);
+                ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_METADATA);
+                return false;
+            }
+            builder.claim("metadata", filteredMetadata);
+        } else {
+            builder.claim("metadata", metadata);
+        }
+
+        final List<EntityStatement<?>> trustChain = selectedTrustChain.getTrustChain();
+        builder.claim("trust_chain", trustChain.stream()
+                .map(statement -> statement.getJwt().serialize())
+                .toList());
+
+        final Instant expirationTime = resolveTrustChainExpiration(trustChain);
+        if (expirationTime == null) {
+            log.error("{} Coud not resolve expiration time from the selected trust chain context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        builder.expirationTime(Date.from(expirationTime));
+
+        final String entityId = trustChain.get(0).getSubject();
+        assert entityId != null;
+        final Map<String, String> trustMarks = buildTrustMarks(entityId, trustChainContext.getVerifiedTrustMarks());
+        if (trustMarks != null && !trustMarks.isEmpty()) {
+            builder.claim("trust_marks", trustMarks);
+        }
+        return true;
+    }
+
+    /**
+     * Builds value for the trust_marks claim in the resolve entity response entity statement.
+     * 
+     * @param entityId the subject entity ID
+     * @param trustMarks trust marks for the selected trust chain
+     * @return map of trust marks, keyed with trust mark IDs
+     */
+    @Nullable private Map<String, String> buildTrustMarks(@Nonnull final String entityId,
+            @Nullable final Map<String, List<TrustMark>> trustMarks) {
+        return Optional.ofNullable(trustMarks)
+            .map(marks -> marks.get(entityId))
+            .filter(Objects::nonNull)
+            .map(list -> list.stream()
+                    .map(trustMark -> new Pair<String, String>(
+                            trustMark.getParsedPayload().getTrustMarkType(), trustMark.getJwt().serialize()))
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toMap(pair -> pair.getFirst(), pair -> pair.getSecond())))
+            .orElse(null);
+    }
+
+    /**
+     * Resolve expiration time for the given trust chain.
+     * 
+     * @param trustChain trust chain
+     * @return expiration time
+     */
+    @Nullable private Instant resolveTrustChainExpiration(@Nonnull final List<EntityStatement<?>> trustChain) {
+        Instant metadataExpiration = null;
+        for (final EntityStatement<?> statement : trustChain) {
+            final Instant statementExpiration = statement.getParsedPayload().getExpiration();
+            metadataExpiration = metadataExpiration == null ? statementExpiration : 
+                statementExpiration.isBefore(metadataExpiration) ? statementExpiration : metadataExpiration;
+        }
+        return metadataExpiration;
+    }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/FormOutboundResolveEntityResponse.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/FormOutboundResolveEntityResponse.java
new file mode 100644
index 0000000..5309dfd
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/FormOutboundResolveEntityResponse.java
@@ -0,0 +1,228 @@
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.oidfed.metadata.cache.ResponseContainerExpirationCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.NimbusResponseCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.ResolveEntityRequestCriterion;
+import net.shibboleth.oidfed.metadata.cache.resolver.ResolveEntityResponseContainer;
+import net.shibboleth.oidfed.profile.config.navigate.CachedSuccessResponseLifetimeLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * This action builds a response for the OpenID federation resolve entity request. The response contains an
+ * {@link SignedJWT}. The response is put in the {@link #responseCache} using the lifetime resolved via
+ * {@link #cachedResponseLifetimeLookupStrategy}.
+ */
+public class FormOutboundResolveEntityResponse extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(FormOutboundResolveEntityResponse.class);
+
+    /** Metadata cache for cached response containers. */
+    @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> responseCache;
+
+    /** Strategy used to locate the resolve entity context. */
+    @Nonnull
+    private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> cachedMessageContextLookupStrategy;
+
+    /** Strategy used to locate the subcontext to hold the statement. */
+    @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextLookupStrategy;
+
+    /** Strategy used to locate the lifetime for the cached response record. */
+    @Nonnull private Function<ProfileRequestContext,Duration> cachedResponseLifetimeLookupStrategy;
+
+    /** JWT used to build entity statement. */
+    @NonnullBeforeExec private SignedJWT jwt;
+
+    /** The resolve entity context to operate on. */
+    @NonnullBeforeExec private RelyingPartyCachedMessageContext cachedMessageContext;
+
+    /**
+     * Constructor.
+     */
+    public FormOutboundResolveEntityResponse() {
+        final Function<ProfileRequestContext,EntityStatementContext> escls =
+                new ChildContextLookup<>(EntityStatementContext.class).compose(
+                        new OutboundMessageContextLookup());
+        assert escls != null;
+        entityStatementContextLookupStrategy = escls;
+        final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> recls =
+                new ChildContextLookup<>(RelyingPartyCachedMessageContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert recls != null;
+        cachedMessageContextLookupStrategy = recls;
+        cachedResponseLifetimeLookupStrategy = new CachedSuccessResponseLifetimeLookupFunction();
+    }
+
+    /**
+     * Set the strategy used to locate the subcontext to hold the statement
+     * 
+     * @param strategy What to set.
+     */
+    public void setEntityStatementContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
+        checkSetterPreconditions();
+        entityStatementContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+    }
+
+    /**
+     * Set the strategy used to locate the cached message context
+     * 
+     * @param strategy What to set.
+     */
+    public void setCachedMessageContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyCachedMessageContext> strategy) {
+        checkSetterPreconditions();
+        cachedMessageContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+    }
+
+    /**
+     * Set the metadata cache for cached response containers.
+     * 
+     * @param cache What to set.
+     */
+    public void setResponseCache(@Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+        checkSetterPreconditions();
+        responseCache = Constraint.isNotNull(cache, "Response metadata cache cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the lifetime for the cached response record.
+     * 
+     * @param strategy What to set.
+     */
+    public void setCachedResponseLifetimeLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, Duration> strategy) {
+        checkSetterPreconditions();
+        cachedResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (responseCache == null) {
+            throw new ComponentInitializationException("Response metadata cache cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        cachedMessageContext = cachedMessageContextLookupStrategy.apply(profileRequestContext);
+        if (cachedMessageContext == null) {
+            log.error("{} Could not resolve cached message context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final Response cachedResponse = cachedMessageContext.getCachedResponse();
+        if (cachedResponse != null) {
+            log.debug("{} Cached response found, storing in to the outbound message context", getLogPrefix());
+            profileRequestContext.ensureOutboundMessageContext().setMessage(cachedResponse);
+            return;
+        }
+        log.debug("{} No cached response found, resolving the response JWT from the context", getLogPrefix());
+        final EntityStatementContext entityStatementContext =
+                entityStatementContextLookupStrategy.apply(profileRequestContext);
+        if (entityStatementContext == null) {
+            log.error("{} Could not resolve entity statement context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        if (entityStatementContext.getJWT() instanceof SignedJWT signedJwt) {
+            jwt = signedJwt;
+        } else {
+            log.error("{} No signed JWT found from the entity statement context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+
+        assert jwt != null;
+        final ResolveEntityResponse response = new ResolveEntityResponse(jwt);
+        final NimbusResponseCriterion responseCriterion = new NimbusResponseCriterion(response);
+        final Duration lifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+        if (lifetime == null) {
+            log.error("{} Could not resolve lifetime for the cached response record", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        final Instant expiration = Instant.now().plus(lifetime);
+        assert expiration != null;
+        final ResponseContainerExpirationCriterion expirationCriterion =
+                new ResponseContainerExpirationCriterion(expiration);
+        if (cachedMessageContext.getValidatedRequest() instanceof ResolveEntityRequest validatedRequest) {
+            final ResolveEntityRequestCriterion requestCriterion = new ResolveEntityRequestCriterion(validatedRequest);
+            final CriteriaSet criteria = new CriteriaSet(requestCriterion, responseCriterion, expirationCriterion);
+            try {
+                final List<ResolveEntityResponseContainer> result = responseCache.get(criteria);
+                if (result.size() != 1) {
+                    log.error("{} Unexpected result (size={}) when storing response record into the metadata cache",
+                            getLogPrefix(), result.size());
+                } else {
+                    log.debug("{} Response stored into the cache", getLogPrefix());
+                }
+            } catch (final MetadataCacheException e) {
+                log.error("{} Could not store the response record into tht metadata cache", getLogPrefix(), e);
+            }
+        } else {
+            log.error("{} No validated request found from the resolve entity context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+
+        profileRequestContext.ensureOutboundMessageContext().setMessage(response);
+    }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/LookupCachedResolveEntityResponse.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/LookupCachedResolveEntityResponse.java
new file mode 100644
index 0000000..f52c867
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/LookupCachedResolveEntityResponse.java
@@ -0,0 +1,157 @@
+/*
+ * 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.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+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.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.oidfed.metadata.cache.local.ResolveEntityRequestCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.ResolveEntityResponseContainer;
+import net.shibboleth.oidfed.profile.OidFederationEventIds;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Lookup if a cached response already exists for the validated resolve entity API request. If yes, the response is
+ * stored into {@link RelyingPartyCachedMessageContext} and a corresponding event ID is published.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link OidFederationEventIds#CACHED_RESPONSE_FOUND}
+ */
+public class LookupCachedResolveEntityResponse extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(LookupCachedResolveEntityResponse.class);
+
+    /** Strategy used to locate the cached message context. */
+    @Nonnull
+    private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> cachedMessageContextLookupStrategy;
+
+    /** Metadata cache for cached response containers. */
+    @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> responseCache;
+
+    /** Cached message context to operate on. */
+    @NonnullBeforeExec private RelyingPartyCachedMessageContext cachedMessageContext;
+
+    /** Request message to operate on. */
+    @NonnullBeforeExec private ResolveEntityRequest validatedRequest;
+
+    /**
+     * Constructor.
+     */
+    public LookupCachedResolveEntityResponse() {
+        final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> recls =
+                new ChildContextLookup<>(RelyingPartyCachedMessageContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert recls != null;
+        cachedMessageContextLookupStrategy = recls;
+    }
+
+    /**
+     * Set the strategy used to locate the cached message context
+     * 
+     * @param strategy What to set.
+     */
+    public void setCachedMessageContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyCachedMessageContext> strategy) {
+        checkSetterPreconditions();
+        cachedMessageContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+    }
+
+    /**
+     * Set the metadata cache for cached response containers.
+     * 
+     * @param cache What to set.
+     */
+    public void setResponseCache(@Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+        checkSetterPreconditions();
+        responseCache = Constraint.isNotNull(cache, "Response metadata cache cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (responseCache == null) {
+            throw new ComponentInitializationException("Response metadata cache cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        cachedMessageContext = cachedMessageContextLookupStrategy.apply(profileRequestContext);
+        if (cachedMessageContext == null) {
+            log.error("{} Could not resolve cached response context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        if (cachedMessageContext.getValidatedRequest() instanceof ResolveEntityRequest resolveEntityRequest) {
+            validatedRequest = resolveEntityRequest;
+        } else {
+            log.error("{} Could not resolve validated resolve entity request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        assert validatedRequest != null;
+        final ResolveEntityRequestCriterion requestCriterion = new ResolveEntityRequestCriterion(validatedRequest);
+        final CriteriaSet criteria = new CriteriaSet(requestCriterion);
+        try {
+            final List<ResolveEntityResponseContainer> result = responseCache.get(criteria);
+            if (result.size() != 1) {
+                log.debug("{} No cached response record found from the metadata cache", getLogPrefix(), result.size());
+            } else {
+                final ResolveEntityResponseContainer cachedResponse = result.get(0);
+                cachedMessageContext.setCachedResponse(cachedResponse.getResponse());
+                log.debug("{} Response found from the cache, publishing event {}", getLogPrefix(),
+                        OidFederationEventIds.CACHED_RESPONSE_FOUND);
+                ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.CACHED_RESPONSE_FOUND);
+                return;
+            }
+        } catch (final MetadataCacheException e) {
+            log.error("{} Could not fetch response record from the metadata cache", getLogPrefix(), e);
+        }
+   }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityProfileConfiguration.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityProfileConfiguration.java
new file mode 100644
index 0000000..a2cf306
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityProfileConfiguration.java
@@ -0,0 +1,231 @@
+/*
+ * 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.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.profile.config.navigate.MandatoryTrustMarksLookupFunction;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidfed.profile.context.VerifiedTrustChain;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Validates the currently selected trust chain against the current selected profile configuration.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link ValidateResolveEntityProfileConfiguration#RESELECT_TRUST_CHAIN}
+ */
+public class ValidateResolveEntityProfileConfiguration extends AbstractProfileAction {
+
+    /** ID of event returned if a flow wishes to indicate that another trust chain should be selected instead. */
+    @Nonnull @NotEmpty public static final String RESELECT_TRUST_CHAIN = "ReselectTrustChain";
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidateResolveEntityProfileConfiguration.class);
+
+    /** Strategy that will return or create a {@link RelyingPartyContext}. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyContext> relyingPartyContextCreationStrategy;
+
+    /** Strategy used to lookup the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /** Strategy used to lookup mandatory trust marks. */
+    @Nonnull private Function<ProfileRequestContext, List<String>> mandatoryTrustMarksLookupStrategy;
+
+    /** Strategy used to lookup subject entity ID. */
+    @NonnullAfterInit private Function<ProfileRequestContext, String> subjectEntityIdLookupStrategy;
+
+    /** Trust chain context to operate on. */
+    @NonnullBeforeExec private RelyingPartyTrustChainContext trustChainContext;
+
+    /** Selected trust chain to operate on. */
+    @NonnullBeforeExec private VerifiedTrustChain selectedTrustChain;
+
+    /** Entity ID whose trust chain are operated on. */
+    @NonnullBeforeExec private String entityId;
+
+    /**
+     * Constructor.
+     */
+    public ValidateResolveEntityProfileConfiguration() {
+        relyingPartyContextCreationStrategy = new ChildContextLookup<>(RelyingPartyContext.class, true);
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+        mandatoryTrustMarksLookupStrategy = new MandatoryTrustMarksLookupFunction();
+    }
+
+    /**
+     * Set the strategy used to return or create the {@link RelyingPartyContext} .
+     * 
+     * @param strategy creation strategy
+     */
+    public void setRelyingPartyContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyContext> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+
+        relyingPartyContextCreationStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext creation strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup the trust chain context.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustChainContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+        checkSetterPreconditions();
+        trustChainContextLookupStrategy =
+                Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup mandatory trust marks.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setMandatoryTrustMarksLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+        checkSetterPreconditions();
+        mandatoryTrustMarksLookupStrategy = Constraint.isNotNull(strategy,
+                "Mandatory trust marks lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup subject entity ID.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSubjectEntityIdLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        subjectEntityIdLookupStrategy = Constraint.isNotNull(strategy,
+                "Subject entity ID lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        trustChainContext = trustChainContextLookupStrategy.apply(profileRequestContext);
+        if (trustChainContext == null || trustChainContext.getSelectedTrustChain() == null) {
+            log.error("{} Unable to locate selected trust chain", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+
+        selectedTrustChain = trustChainContext.getSelectedTrustChain();
+        if (selectedTrustChain == null) {
+            log.error("{} Selected trust chain contents is not populated", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+
+        entityId = StringSupport.trimOrNull(subjectEntityIdLookupStrategy.apply(profileRequestContext));
+        if (entityId == null) {
+            log.error("{} Subject entity ID could not be located", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final Map<String,List<String>> verifiedTrustMarks = trustChainContext.getVerifiedTrustMarkIds();
+        final List<String> mandatoryTrustMarks = mandatoryTrustMarksLookupStrategy.apply(profileRequestContext);
+        if (mandatoryTrustMarks != null && !mandatoryTrustMarks.isEmpty()) {
+            log.debug("{} Verifying the mandatory trust marks {}", getLogPrefix(), mandatoryTrustMarks);
+            if (verifiedTrustMarks == null || verifiedTrustMarks.get(entityId) == null 
+                    || !verifiedTrustMarks.get(entityId).containsAll(mandatoryTrustMarks)) {
+                log.info("{} Rejecting registration as some of the following mandatory trust marks are missing: {}",
+                        getLogPrefix(), mandatoryTrustMarks);
+                final List<List<EntityStatement<?>>> rejectedTrustChains = trustChainContext.getRejectedTrustChains();
+                if (rejectedTrustChains == null) {
+                    trustChainContext.setRejectedTrustChains(List.of(selectedTrustChain.getTrustChain()));
+                } else {
+                    final List<List<EntityStatement<?>>> rejectedChains = new ArrayList<>(rejectedTrustChains);
+                    rejectedChains.add(selectedTrustChain.getTrustChain());
+                    trustChainContext.setRejectedTrustChains(CollectionSupport.copyToList(rejectedChains));
+                }
+                ActionSupport.buildEvent(profileRequestContext, RESELECT_TRUST_CHAIN);
+                return;
+            }
+        }
+
+        final List<EntityStatement<?>> trustChain = selectedTrustChain.getTrustChain();
+        assert trustChain != null;
+        trustChainContext.setSelectedMetadataExpiration(resolveTrustChainExpiration(trustChain));
+
+        final RelyingPartyContext rpContext = relyingPartyContextCreationStrategy.apply(profileRequestContext);
+        if (rpContext == null) {
+            log.error("{} Unable to locate or create RelyingPartyContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            return;
+        }
+        log.debug("Attaching RelyingPartyContext for {} and set it verified", entityId);
+        rpContext.setRelyingPartyId(entityId);
+        rpContext.setVerified(true);
+    }
+
+    /**
+     * Resolve expiration time for the given trust chain.
+     * 
+     * @param trustChain trust chain
+     * @return expiration time
+     */
+    @Nullable private Instant resolveTrustChainExpiration(@Nonnull final List<EntityStatement<?>> trustChain) {
+        Instant metadataExpiration = null;
+        for (final EntityStatement<?> statement : trustChain) {
+            final Instant statementExpiration = statement.getParsedPayload().getExpiration();
+            metadataExpiration = metadataExpiration == null ? statementExpiration : 
+                statementExpiration.isBefore(metadataExpiration) ? statementExpiration : metadataExpiration;
+        }
+        return metadataExpiration;
+    }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityRequest.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityRequest.java
new file mode 100644
index 0000000..bd3114d
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityRequest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+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.nimbusds.oauth2.sdk.Request;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.LocalKeyContainer;
+import net.shibboleth.oidfed.profile.OidFederationEventIds;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Validates the resolve entity request against the profile configuration and stores the validated (possibly modified)
+ * request into {@link RelyingPartyCachedMessageContext#setValidatedRequest(Request)}.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class ValidateResolveEntityRequest extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidateResolveEntityRequest.class);
+
+    /** Strategy used to create the cached message context. */
+    @Nonnull
+    private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> cachedMessageContextCreationStrategy;
+
+    /** Cache containing local copies of trusted trust anchor keys. */
+    @NonnullAfterInit private MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
+
+    /** Request message to operate on. */
+    @NonnullBeforeExec private ResolveEntityRequest requestMessage;
+
+    /** Cached message context to operate on. */
+    @NonnullBeforeExec private RelyingPartyCachedMessageContext cachedMessageContext;
+
+    /**
+     * Constructor.
+     */
+    public ValidateResolveEntityRequest() {
+        final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> reccs =
+                new ChildContextLookup<>(RelyingPartyCachedMessageContext.class, true).compose(
+                        new InboundMessageContextLookup());
+        assert reccs != null;
+        cachedMessageContextCreationStrategy = reccs;
+    }
+
+    /**
+     * Set the strategy used to return or create the resolve entity context.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setResolveEntityContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> strategy) {
+        checkSetterPreconditions();
+        cachedMessageContextCreationStrategy = Constraint.isNotNull(strategy,
+                "RelyingPartyResolveEntityContext creation strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup the trust chain context.
+     * 
+     * @param cache lookup strategy
+     */
+    public void setLocalTrustAnchorsCache(
+            @Nonnull final MetadataCache<Map<String, LocalKeyContainer>> cache) {
+        checkSetterPreconditions();
+        localTrustAnchorsCache =
+                Constraint.isNotNull(cache, "LocalTrustAnchorsCache cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (localTrustAnchorsCache == null) {
+            throw new ComponentInitializationException("LocalTrustAnchorsCache cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        requestMessage = Optional.ofNullable(profileRequestContext.getInboundMessageContext())
+                .map(messageContext -> messageContext.getMessage())
+                .filter(ResolveEntityRequest.class::isInstance)
+                .map(ResolveEntityRequest.class::cast)
+                .orElse(null);
+        if (requestMessage == null) {
+            log.error("{} Unable to fetch the request message to operate on", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        cachedMessageContext = cachedMessageContextCreationStrategy.apply(profileRequestContext);
+        if (cachedMessageContext == null) {
+            log.error("{} Unable to create resolve entity context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final List<String> validatedAnchors = requestMessage.getTrustAnchors().stream()
+                .filter(anchor -> isLocallyTrusted(anchor))
+                .toList();
+        if (validatedAnchors.isEmpty()) {
+            log.info("{} No locally trusted anchors left after filtering", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_TRUST_ANCHOR);
+            return;
+        }
+        log.debug("{} The following trust anchors were validated: {}", getLogPrefix(), validatedAnchors);
+        cachedMessageContext.setValidatedRequest(
+                new ResolveEntityRequest(requestMessage.getEndpointURI(), requestMessage.getSubject(),
+                        validatedAnchors, requestMessage.getEntityTypes(), requestMessage.getClientAuthentication()));
+    }
+
+    /**
+     * Verifies whether the given trust anchor candidate is locally trusted.
+     * 
+     * @param candidate the trust anchor candidate
+     * @return true if locally trusted, false otherwise
+     */
+    protected boolean isLocallyTrusted(@Nullable final String candidate) {
+        if (StringSupport.trimOrNull(candidate) == null) {
+            return false;
+        }
+        assert candidate != null;
+        final SubjectEntityIDCriterion criterion = new SubjectEntityIDCriterion(candidate);
+        try {
+            final List<Map<String,LocalKeyContainer>> result = localTrustAnchorsCache.get(new CriteriaSet(criterion));
+            if (result.isEmpty() || result.get(0).isEmpty()) {
+                log.debug("{} No locally trusted keys found for {}", getLogPrefix(), candidate);
+                return false;
+            }
+            return result.get(0).containsKey(candidate);
+        } catch (final MetadataCacheException e) {
+            log.error("{} Could not fetch value for {} from the metadata cache", getLogPrefix(), candidate, e);
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateSelectedTrustChain.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateSelectedTrustChain.java
new file mode 100644
index 0000000..f9aa9a7
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateSelectedTrustChain.java
@@ -0,0 +1,231 @@
+/*
+ * 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.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+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.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer;
+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.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.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Validates that the currenty selected trust chain meets the trust anchor requirements in the resolve entity request.
+ * If not and if other candidates remains, {@link OidFederationEventIds#RESELECT_TRUST_CHAIN} is published. If no other
+ * candidates are available, {@link OidFederationEventIds#INVALID_TRUST_ANCHOR} or
+ * {@link OidFederationEventIds#INVALID_SUBJECT} is published.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link OidFederationEventIds#RESELECT_TRUST_CHAIN}
+ * @event {@link OidFederationEventIds#INVALID_TRUST_ANCHOR}
+ * @event {@link OidFederationEventIds#INVALID_SUBJECT}
+ */
+public class ValidateSelectedTrustChain extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidateSelectedTrustChain.class);
+
+    /** Metadata cache for entity configurations. */
+    @NonnullAfterInit private MetadataCache<EntityConfigurationContainer> entityConfigurationCache;
+
+    /** Strategy used to lookup the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /** Strategy used to locate the resolve entity context. */
+    @Nonnull
+    private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> resolveEntityContextLookupStrategy;
+
+    /** The validated request to operate on. */
+    @NonnullBeforeExec private ResolveEntityRequest validatedRequest;
+
+    /**
+     * Constructor.
+     */
+    public ValidateSelectedTrustChain() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+        final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> recls =
+                new ChildContextLookup<>(RelyingPartyCachedMessageContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert recls != null;
+        resolveEntityContextLookupStrategy = recls;
+    }
+
+    /**
+     * Set the metadata cache for entity configurations.
+     * 
+     * @param cache What to set.
+     */
+    public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityConfigurationContainer> cache) {
+        checkSetterPreconditions();
+        entityConfigurationCache = Constraint.isNotNull(cache, "Entity configuration metadata cache cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup the trust chain context.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustChainContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+        checkSetterPreconditions();
+        trustChainContextLookupStrategy =
+                Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the resolve entity context
+     * 
+     * @param strategy What to set.
+     */
+    public void setResolveEntityContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyCachedMessageContext> strategy) {
+        checkSetterPreconditions();
+        resolveEntityContextLookupStrategy = 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 metadata cache cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        final RelyingPartyCachedMessageContext cachedResponseContext =
+                resolveEntityContextLookupStrategy.apply(profileRequestContext);
+        if (cachedResponseContext == null) {
+            log.error("{} Could not resolve cached message context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        if (cachedResponseContext.getValidatedRequest() instanceof ResolveEntityRequest resolveEntityRequest) {
+            validatedRequest = resolveEntityRequest;
+        } else {
+            log.error("{} Could not resolve request message", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final RelyingPartyTrustChainContext trustChainContext =
+                trustChainContextLookupStrategy.apply(profileRequestContext);
+        final VerifiedTrustChain selectedTrustChain =
+                trustChainContext != null ? trustChainContext.getSelectedTrustChain() : null;
+        if (selectedTrustChain == null) {
+            final List<VerifiedTrustChain> allChains =
+                    trustChainContext != null ? trustChainContext.getPolicyCompliantTrustChains() : null;
+            if (allChains == null || allChains.isEmpty()) {
+                if (isSubjectValid(validatedRequest.getSubject())) {
+                    log.debug("{} No trust chains were resolved, subject is valid", getLogPrefix());
+                    ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_TRUST_ANCHOR);
+                    return;
+                } else {
+                    log.debug("{} No trust chains were resolved, subject is not valid", getLogPrefix());
+                    ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_SUBJECT);
+                    return;
+                }
+            } else {
+                log.debug("{} No trust chains left to choose from", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_TRUST_ANCHOR);
+                return;
+            }
+        }
+        
+        final List<String> trustAnchors = validatedRequest.getTrustAnchors();
+        final List<EntityStatement<?>> candidateChain = selectedTrustChain.getTrustChain();
+        assert candidateChain != null;
+        final String candidateAnchor = candidateChain.get(candidateChain.size() - 1).getSubject();
+        if (!trustAnchors.contains(candidateAnchor)) {
+            log.debug("{} Selected trust chain candidate has unrequested trust anchor {}", getLogPrefix(),
+                    candidateAnchor);
+            assert trustChainContext != null;
+            final List<List<EntityStatement<?>>> rejectedTrustChains = trustChainContext.getRejectedTrustChains();
+            if (rejectedTrustChains == null) {
+                trustChainContext.setRejectedTrustChains(List.of(selectedTrustChain.getTrustChain()));
+            } else {
+                final List<List<EntityStatement<?>>> rejectedChains = new ArrayList<>(rejectedTrustChains);
+                rejectedChains.add(selectedTrustChain.getTrustChain());
+                trustChainContext.setRejectedTrustChains(CollectionSupport.copyToList(rejectedChains));
+            }
+            ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.RESELECT_TRUST_CHAIN);
+            return;
+        }
+    }
+
+    /**
+     * Checks if an entity configuration can be resolved for the given subject and it's thus valid for federation.
+     * 
+     * @param subject the subject to be verified
+     * @return true if the given subject is valid, false otherwise.
+     */
+    protected boolean isSubjectValid(@Nonnull final String subject) {
+        final SubjectEntityIDCriterion subjectCriterion = new SubjectEntityIDCriterion(subject);
+        try {
+            final List<EntityConfigurationContainer> result =
+                    entityConfigurationCache.get(new CriteriaSet(subjectCriterion));
+            if (result.size() == 1 && result.get(0).getStatement() != null) {
+                return true;
+            }
+        } catch (final MetadataCacheException e) {
+            log.debug("{} Exception catched when resolving entty configuration", e);
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultEntityTypesLookupFunction.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultEntityTypesLookupFunction.java
new file mode 100644
index 0000000..9866b5b
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultEntityTypesLookupFunction.java
@@ -0,0 +1,46 @@
+/*
+ * 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 javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+
+/**
+ * Default function to lookup entity types to be included to the response metadata.
+ */
+public class DefaultEntityTypesLookupFunction implements Function<ProfileRequestContext, List<String>> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public List<String> apply(@Nullable final ProfileRequestContext profileRequestContext) {
+        return Optional.ofNullable(profileRequestContext)
+                .map(prc -> prc.getInboundMessageContext())
+                .map(msgCtx -> msgCtx.getMessage())
+                .filter(ResolveEntityRequest.class::isInstance)
+                .map(ResolveEntityRequest.class::cast)
+                .map(req -> req.getEntityTypes())
+                .orElseGet(NonnullSupplier.of(CollectionSupport.emptyList()));
+    }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java
deleted file mode 100644
index d205eb4..0000000
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.security.credential;
-
-import java.util.List;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.security.credential.Credential;
-import org.slf4j.Logger;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
-
-import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
-import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
-import net.shibboleth.oidc.security.jose.criterion.ClientInformationCriterion;
-import net.shibboleth.oidfed.metadata.EntityStatement;
-import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
-import net.shibboleth.oidfed.metadata.util.EntityStatementHelper;
-import net.shibboleth.oidfed.support.ClientInformationExtensionSupport;
-import net.shibboleth.shared.annotation.ParameterName;
-import net.shibboleth.shared.collection.CollectionSupport;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.shared.resolver.CriteriaSet;
-import net.shibboleth.shared.resolver.ResolverException;
-
-/**
- * A {@link JOSEObjectCredentialResolver} that resolves credentials from the entity configuration payload. The entity
- * configuration is fetched via client custom claim
- * {@link ClientInformationExtensionSupport#KEY_VALIDATED_TRUST_CHAIN}.
- */
-public class ClientInformationFederationEntityCredentialResolver extends BasicJOSEObjectCredentialResolver {
-    
-    /** Class logger. */
-    @Nonnull
-    private final Logger log = LoggerFactory.getLogger(ClientInformationFederationEntityCredentialResolver.class);
-
-    /** Resolver for fetching federation entity credentials from entity configuration. */
-    @Nonnull private final JOSEObjectCredentialResolver entityConfigurationCredentialResolver;
-
-    /** Object mapper used for deserializing jwks from the entity configuration payload. */
-    @Nonnull private final ObjectMapper objectMapper;
-
-    /**
-     * Constructor.
-     *
-     * @param resolver The resolver for fetching federation entity credentials from entity configuration.
-     * @param mapper The object mapper used for deserializing jwks from the entity configuration payload.
-     */
-    public ClientInformationFederationEntityCredentialResolver(@Nonnull
-            @ParameterName(name="entityConfigurationCredentialResolver") final JOSEObjectCredentialResolver resolver,
-            @Nonnull @ParameterName(name="objectMapper") final ObjectMapper mapper) {
-        entityConfigurationCredentialResolver = Constraint.isNotNull(resolver,
-                "EntityConfigurationCredentialResolver cannot be null");
-        objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    @Nonnull protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet)
-            throws ResolverException {
-        
-        Constraint.isNotNull(criteriaSet, "CriteriaSet was null");
-
-        if (criteriaSet != null) {
-            final ClientInformationCriterion clientCrit = criteriaSet.get(ClientInformationCriterion.class);
-            if (clientCrit != null) {
-                return resolveFromMetadata(criteriaSet, clientCrit.getOidcClientInformation());
-            }
-        }
-            
-        log.debug("Criteria did not contain a ClientInformationCriterion could not perform resolution");
-        return CollectionSupport.emptySet();
-    }
-    
-    /**
-     * Resolve the keyset from the entity configuration payload.
-     *
-     * @param criteriaSet the criteria set
-     * @param information the RP/Client information
-     * 
-     * @return a collection of credentials from the entity configuration key set (if any).
-     * @throws ResolverException if resolution fails
-     */
-    @Nonnull protected Iterable<Credential> resolveFromMetadata(@Nonnull final CriteriaSet criteriaSet, 
-            @Nonnull final OIDCClientInformation information) throws ResolverException {
-
-        final OIDCClientMetadata metadata = information.getOIDCMetadata();
-
-        if (metadata.getCustomField(ClientInformationExtensionSupport.KEY_VALIDATED_TRUST_CHAIN)
-                instanceof List<?> list) {
-            final List<String> serialized =
-                    list.stream().filter(String.class::isInstance).map(String.class::cast).toList();
-            assert serialized != null;
-            final List<EntityStatement<?>> trustChain =
-                    EntityStatementHelper.deserializeTrustChain(serialized, objectMapper);
-            if (trustChain != null) {
-                final EntityStatement<?> configuration = trustChain.get(0);
-                assert configuration != null;
-                final SubjectEntityStatementCriterion configurationCriterion =
-                        new SubjectEntityStatementCriterion(configuration);
-                log.debug("Returning credentials resolved via entity configuration credential resolver");
-                return entityConfigurationCredentialResolver.resolve(new CriteriaSet(configurationCriterion));
-            }
-        } else {
-            log.debug("Could not find the validated trust chain from the client metadata");
-        }
-        log.trace("Returning empty set of credentials");
-        return CollectionSupport.emptySet();
-    }
-    
-}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/SelectedTrustChainFederationEntityCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/SelectedTrustChainFederationEntityCredentialResolver.java
new file mode 100644
index 0000000..c91fbde
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/SelectedTrustChainFederationEntityCredentialResolver.java
@@ -0,0 +1,124 @@
+/*
+ * 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.security.credential;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.profile.criterion.ProfileRequestContextCriterion;
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * A {@link JOSEObjectCredentialResolver} that resolves credentials from the entity configuration payload. The entity
+ * configuration is fetched from the selected trust chain stored in {@link RelyingPartyTrustChainContext}.
+ */
+public class SelectedTrustChainFederationEntityCredentialResolver extends BasicJOSEObjectCredentialResolver {
+    
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(SelectedTrustChainFederationEntityCredentialResolver.class);
+
+    /** Resolver for fetching federation entity credentials from entity configuration. */
+    @Nonnull private final JOSEObjectCredentialResolver entityConfigurationCredentialResolver;
+
+    /** Strategy used to lookup the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /**
+     * Constructor.
+     *
+     * @param resolver The resolver for fetching federation entity credentials from entity configuration.
+     */
+    public SelectedTrustChainFederationEntityCredentialResolver(@Nonnull
+            @ParameterName(name="entityConfigurationCredentialResolver") final JOSEObjectCredentialResolver resolver) {
+        entityConfigurationCredentialResolver = Constraint.isNotNull(resolver,
+                "EntityConfigurationCredentialResolver cannot be null");
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet)
+            throws ResolverException {
+        
+        Constraint.isNotNull(criteriaSet, "CriteriaSet was null");
+
+        if (criteriaSet != null) {
+            final ProfileRequestContextCriterion criterion = criteriaSet.get(ProfileRequestContextCriterion.class);
+            if (criterion != null) {
+                return resolveFromProfileRequestContext(criteriaSet, criterion.getProfileRequestContext());
+            }
+        }
+            
+        log.debug("Criteria did not contain a ProfileRequestContextCriterion could not perform resolution");
+        return CollectionSupport.emptySet();
+    }
+    
+    /**
+     * Resolve the keyset from the entity configuration payload.
+     *
+     * @param criteriaSet the criteria set
+     * @param profileRequestContext the PRC containing selected trust chain
+     * 
+     * @return a collection of credentials from the entity configuration key set (if any).
+     * @throws ResolverException if resolution fails
+     */
+    @Nonnull protected Iterable<Credential> resolveFromProfileRequestContext(@Nonnull final CriteriaSet criteriaSet, 
+            @Nonnull final ProfileRequestContext profileRequestContext) throws ResolverException {
+
+
+        final var trustChainContext = trustChainContextLookupStrategy.apply(profileRequestContext);
+        if (trustChainContext == null || trustChainContext.getSelectedTrustChain() == null) {
+            log.error("Unable to locate selected trust chain");
+            return CollectionSupport.emptySet();
+        }
+
+        final var selectedTrustChain = trustChainContext.getSelectedTrustChain();
+        if (selectedTrustChain == null) {
+            log.error("Selected trust chain contents is not populated");
+            return CollectionSupport.emptySet();
+        }
+
+        final EntityStatement<?> configuration = selectedTrustChain.getTrustChain().get(0);
+        assert configuration != null;
+        final SubjectEntityStatementCriterion configurationCriterion =
+                new SubjectEntityStatementCriterion(configuration);
+        log.debug("Returning credentials resolved via entity configuration credential resolver");
+        return entityConfigurationCredentialResolver.resolve(new CriteriaSet(configurationCriterion));
+    }
+    
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/resources/net/shibboleth/oidfed/conf/oidfed/oidfed.properties b/oidfed-common-impl/src/main/resources/net/shibboleth/oidfed/conf/oidfed/oidfed.properties
index bf994d4..c2e1c91 100644
--- a/oidfed-common-impl/src/main/resources/net/shibboleth/oidfed/conf/oidfed/oidfed.properties
+++ b/oidfed-common-impl/src/main/resources/net/shibboleth/oidfed/conf/oidfed/oidfed.properties
@@ -131,3 +131,20 @@ idp.oidfed.entityConfiguration.authorityHints = https://your.authority.example.o
 #idp.oidfed.admin.resolvertest.TrustChainMetadataCache = shibboleth.oidfed.TrustChainMetadataCache
 #idp.oidfed.admin.resolvertest.TrustMarkStatusMetadataCache = shibboleth.oidfed.TrustMarkStatusMetadataCache
 
+# resolve-entity-beans
+#idp.oidfed.resolveEntity.authn.flows = OAuth2Client
+#idp.service.logging.oidfedresolve = OIDFED.ResolveEntity
+#idp.oidfed.cache.resolveEntity.minRefreshDelay = PT1S
+#idp.oidfed.cache.resolveEntity.maxRefreshDelay = PT30S
+#idp.oidfed.resolveEntity.LocalTrustAnchorsMetadataCache = shibboleth.oidfed.LocalTrustAnchorsMetadataCache
+#idp.oidfed.resolveEntity.MetadataPolicyOperators = shibboleth.oidfed.StandardMetadataPolicyOperators
+#idp.oidfed.resolveEntity.MetadataPolicyMergingStrategy = MetadataPolicyMergingStrategy
+#idp.oidfed.resolveEntity.LocalMetadataPolicyStrategy = DefaultLocalMetadataPolicyStrategy
+#idp.oidfed.resolveEntity.TrustChainMetadataCache = shibboleth.oidfed.TrustChainMetadataCache
+#idp.oidfed.resolveEntity.TrustMarkStatusMetadataCache = shibboleth.oidfed.TrustMarkStatusMetadataCache
+#idp.oidfed.resolveEntity.sigalg = RS256
+#idp.oidfed.resolveEntity.endpointAuthMethods = private_key_jwt
+#idp.oidfed.resolveEntity.targetedEndpointAsJWTAudience = true
+#idp.oidfed.resolveEntity.requireSingleJWTAudience = true
+#idp.oidfed.resolveEntity.trustchain.resolver.useResolverApiCondition = shibboleth.Conditions.FALSE
+#idp.oidfed.resolveEntity.trustchain.resolver.fallbackToLocalCondition = shibboleth.Conditions.TRUE

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


More information about the commits mailing list