[java-idp-oidc] branch dev/JOIDC-222 updated: JOIDC-222 - Support for OpenID Federation

Henri Mikkonen henri.mikkonen at iki.fi
Fri Apr 18 11:49:40 UTC 2025


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

hjmikkon pushed a commit to branch dev/JOIDC-222
in repository java-idp-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=12f632bef628d33a26c9140d6f27b00080273867

The following commit(s) were added to refs/heads/dev/JOIDC-222 by this push:
     new 12f632be JOIDC-222 - Support for OpenID Federation
12f632be is described below

commit 12f632bef628d33a26c9140d6f27b00080273867
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Apr 18 14:49:15 2025 +0300

    JOIDC-222 - Support for OpenID Federation
    
    https://shibboleth.atlassian.net/browse/JOIDC-222
    
    - Initial (incomplete) implementation of Resolve Entity API (oidfed/resolve-entity). WIP.
      - Currently hardcoded to serve only 'openid_relying_party' entity types
      - New profile configuration: OIDFED.ResolveEntity (DefaultOIDFederationResolveEntityProfileConfiguration)
        - Comfigurable lifetimes for cached success and error responses (default 5 minutes
      - The responses are cached by using the request message contents (parameters) as the key
        - Only locally trusted trust anchors are included in the key
---
 ...ederationResolveEntityProfileConfiguration.java | 164 +++++++++++++++
 ...ederationResolveEntityProfileConfiguration.java |  66 ++++++
 ...yCachedErrorResponseLifetimeLookupFunction.java |  45 ++++
 ...achedSuccessResponseLifetimeLookupFunction.java |  45 ++++
 .../decoding/impl/ResolveEntityRequestDecoder.java | 107 ++++++++++
 .../messaging/impl/ResolveEntityRequest.java       | 133 ++++++++++++
 .../messaging/impl/ResolveEntityResponse.java      |  99 +++++++++
 .../DefaultEntityStatementFetchingStrategy.java    |   5 +-
 ...eEntityRequestCriteriaToIdentifierStrategy.java |  42 ++++
 ...ityResponseContainerExpirationTimeStrategy.java |  59 ++++++
 ...faultResolveEntityResponseFetchingStrategy.java |  65 ++++++
 ...EntityResponseIdentifierExtractionStrategy.java |  38 ++++
 .../ResolveEntityContainerExpirationCriterion.java |  79 +++++++
 .../metadata/ResolveEntityRequestCriterion.java    |  80 +++++++
 .../metadata/ResolveEntityResponseContainer.java   |  85 ++++++++
 .../metadata/ResolveEntityResponseCriterion.java   |  80 +++++++
 .../metadata/TrustAnchorEntityIDsCriterion.java    |  80 +++++++
 .../BuildResolveEntityErrorResponseFromEvent.java  | 157 ++++++++++++++
 .../profile/impl/BuildResolveEntityResponse.java   | 153 ++++++++++++++
 .../impl/FormOutboundResolveEntityResponse.java    | 230 +++++++++++++++++++++
 .../impl/LookupCachedResolveEntityRespomse.java    | 151 ++++++++++++++
 .../oidfed/profile/impl/OidFederationEventIds.java |  46 +++++
 .../impl/RelyingPartyResolveEntityContext.java     |  79 +++++++
 .../op/oidfed/profile/impl/SelectTrustChain.java   |   3 +-
 .../profile/impl/ValidateResolveEntityRequest.java | 185 +++++++++++++++++
 .../profile/impl/ValidateSelectedTrustChain.java   | 224 ++++++++++++++++++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  21 ++
 .../oidfed/resolve-entity/resolve-entity-beans.xml | 198 ++++++++++++++++++
 .../oidfed/resolve-entity/resolve-entity-flow.xml  |  77 +++++++
 .../idp/service/relying-party/postconfig.xml       |   5 +-
 .../profile/flow/oidfed/ResolveEntityFlowTest.java |  82 ++++++++
 .../shibboleth/idp/module/conf/relying-party.xml   |   1 +
 32 files changed, 2879 insertions(+), 5 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationResolveEntityProfileConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationResolveEntityProfileConfiguration.java
new file mode 100644
index 00000000..a0dfb3d3
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationResolveEntityProfileConfiguration.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.config;
+
+import java.time.Duration;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.profile.config.AbstractConditionalProfileConfiguration;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.Positive;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+
+/**
+ * Implementation of a profile configuration for the OpenID Federation Resolve Entity.
+ */
+public class DefaultOIDFederationResolveEntityProfileConfiguration extends AbstractConditionalProfileConfiguration
+        implements OIDFederationResolveEntityProfileConfiguration {
+
+    /** OIDC provider information profile counter name. */
+    @Nonnull @NotEmpty public static final String PROFILE_COUNTER = "net.shibboleth.idp.profiles.oidfed.resolve-entity";
+
+    /** Lookup function to override issuer value. */
+    @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+
+    /** Lookup function to supply cached success response lifetime. */
+    @Nonnull private Function<ProfileRequestContext,Duration> cachedSuccessResponseLifetimeLookupStrategy;
+
+    /** Lookup function to supply cached error response lifetime. */
+    @Nonnull private Function<ProfileRequestContext,Duration> cachedErrorResponseLifetimeLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public DefaultOIDFederationResolveEntityProfileConfiguration() {
+        this(PROFILE_ID);
+    }
+    
+    /**
+     * Creates a new configuration instance.
+     *
+     * @param profileId Unique profile identifier.
+     */
+    public DefaultOIDFederationResolveEntityProfileConfiguration(@Nonnull @NotEmpty final String profileId) {
+        super(profileId);
+        issuerLookupStrategy = FunctionSupport.constant(null);
+        cachedSuccessResponseLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofMinutes(5));
+        cachedErrorResponseLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofMinutes(5));
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nullable @NotEmpty public String getIssuer(@Nullable final ProfileRequestContext profileRequestContext) {
+        return issuerLookupStrategy.apply(profileRequestContext);
+    }
+    
+    /**
+     * Set overridden issuer value.
+     * 
+     * @param issuer issuer value
+     */
+    public void setIssuer(@Nullable @NotEmpty final String issuer) {
+        issuerLookupStrategy = FunctionSupport.constant(issuer);
+    }
+    
+    /**
+     * Sets lookup strategy for overridden issuer value.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Positive @Nonnull
+    public Duration getCachedSuccessResponseLifetime(@Nullable final ProfileRequestContext profileRequestContext) {
+        final Duration lifetime = cachedSuccessResponseLifetimeLookupStrategy.apply(profileRequestContext);
+
+        Constraint.isTrue(lifetime != null && !lifetime.isZero() && !lifetime.isNegative(),
+                "Success response lifetime must be greater than 0");
+        assert lifetime != null;
+        return lifetime;
+    }
+
+    /**
+     * Set the lifetime of a cached success response.
+     * 
+     * @param lifetime lifetime of a cached success response
+     */
+    public void setCachedSuccessResponseLifetime(@Positive @Nonnull final Duration lifetime) {
+        final Duration successLifetime = Constraint.isNotNull(lifetime,
+                "Cached success response lifetime cannot be null");
+        Constraint.isTrue(!successLifetime.isZero() && !successLifetime.isNegative(),
+                "Cached success response lifetime must be greater than 0");
+
+        cachedSuccessResponseLifetimeLookupStrategy = FunctionSupport.constant(successLifetime);
+    }
+
+    /**
+     * Set a lookup strategy for the cached success response lifetime.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setCachedSuccessResponseLifetimeLookupStrategy(
+            @Nullable final Function<ProfileRequestContext,Duration> strategy) {
+        cachedSuccessResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Positive @Nonnull
+    public Duration getCachedErrorResponseLifetime(@Nullable final ProfileRequestContext profileRequestContext) {
+        final Duration lifetime = cachedErrorResponseLifetimeLookupStrategy.apply(profileRequestContext);
+
+        Constraint.isTrue(lifetime != null && !lifetime.isZero() && !lifetime.isNegative(),
+                "Error response lifetime must be greater than 0");
+        assert lifetime != null;
+        return lifetime;
+    }
+
+    /**
+     * Set the lifetime of a cached error response.
+     * 
+     * @param lifetime lifetime of a cached error response
+     */
+    public void setCachedErrorResponseLifetime(@Positive @Nonnull final Duration lifetime) {
+        final Duration errorLifetime = Constraint.isNotNull(lifetime,
+                "Cached error response lifetime cannot be null");
+        Constraint.isTrue(!errorLifetime.isZero() && !errorLifetime.isNegative(),
+                "Cached error response lifetime must be greater than 0");
+
+        cachedErrorResponseLifetimeLookupStrategy = FunctionSupport.constant(errorLifetime);
+    }
+
+    /**
+     * Set a lookup strategy for the cached error response lifetime.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setCachedErrorResponseLifetimeLookupStrategy(
+            @Nullable final Function<ProfileRequestContext,Duration> strategy) {
+        cachedErrorResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationResolveEntityProfileConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationResolveEntityProfileConfiguration.java
new file mode 100644
index 00000000..f5afddaa
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationResolveEntityProfileConfiguration.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.config;
+
+import java.time.Duration;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.profile.config.OverriddenIssuerProfileConfiguration;
+import net.shibboleth.shared.annotation.ConfigurationSetting;
+import net.shibboleth.shared.annotation.constraint.Positive;
+
+/** 
+ * Profile configuration for an OpenID Federation Resolve Entity.
+ */
+public interface OIDFederationResolveEntityProfileConfiguration extends OverriddenIssuerProfileConfiguration, 
+    OIDFederationProfileConfiguration {
+    
+    /** OIDC base protocol URI. */
+    public static final String PROTOCOL_URI = "https://openid.net/specs/openid-federation-1_0.html";
+
+    /** ID for this profile configuration. */
+    public static final String PROFILE_ID = "http://shibboleth.net/ns/profiles/oidfed/resolve-entity";
+
+    /**
+     * Get cached success response lifetime.
+     * 
+     * <p>Defaults to 5 minutes.</p>
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return cached success response lifetime
+     */
+    @ConfigurationSetting(name="cachedSuccessResponseLifetime")
+    @Positive @Nonnull
+    Duration getCachedSuccessResponseLifetime(@Nullable final ProfileRequestContext profileRequestContext);
+
+    /**
+     * Get cached error response lifetime.
+     * 
+     * <p>Defaults to 5 minutes.</p>
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return cached error response lifetime
+     */
+    @ConfigurationSetting(name="cachedErrorResponseLifetime")
+    @Positive @Nonnull
+    Duration getCachedErrorResponseLifetime(@Nullable final ProfileRequestContext profileRequestContext);
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedErrorResponseLifetimeLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedErrorResponseLifetimeLookupFunction.java
new file mode 100644
index 00000000..d22579c7
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedErrorResponseLifetimeLookupFunction.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.config;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+
+/**
+ * A function that returns
+ * {@link OIDFederationResolveEntityProfileConfiguration#getCachedErrorResponseLifetime(ProfileRequestContext)}.
+ * 
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+public class ResolveEntityCachedErrorResponseLifetimeLookupFunction
+    extends AbstractRelyingPartyLookupFunction<Duration> {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public Duration apply(@Nullable final ProfileRequestContext input) {
+        return Optional.ofNullable(getRelyingPartyContextLookupStrategy().apply(input))
+                .map(relyingPartyContext -> relyingPartyContext.getProfileConfig())
+                .filter(OIDFederationResolveEntityProfileConfiguration.class::isInstance)
+                .map(OIDFederationResolveEntityProfileConfiguration.class::cast)
+                .map(config -> config.getCachedErrorResponseLifetime(input))
+                .orElse(null);
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedSuccessResponseLifetimeLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedSuccessResponseLifetimeLookupFunction.java
new file mode 100644
index 00000000..1ffecda0
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedSuccessResponseLifetimeLookupFunction.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.config;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+
+/**
+ * A function that returns
+ * {@link OIDFederationResolveEntityProfileConfiguration#getCachedSuccessResponseLifetime(ProfileRequestContext)}.
+ * 
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+public class ResolveEntityCachedSuccessResponseLifetimeLookupFunction
+    extends AbstractRelyingPartyLookupFunction<Duration> {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public Duration apply(@Nullable final ProfileRequestContext input) {
+        return Optional.ofNullable(getRelyingPartyContextLookupStrategy().apply(input))
+                .map(relyingPartyContext -> relyingPartyContext.getProfileConfig())
+                .filter(OIDFederationResolveEntityProfileConfiguration.class::isInstance)
+                .map(OIDFederationResolveEntityProfileConfiguration.class::cast)
+                .map(config -> config.getCachedSuccessResponseLifetime(input))
+                .orElse(null);
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ResolveEntityRequestDecoder.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ResolveEntityRequestDecoder.java
new file mode 100644
index 00000000..11668fac
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ResolveEntityRequestDecoder.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.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 javax.annotation.Nullable;
+
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.slf4j.Logger;
+
+import com.google.common.base.MoreObjects;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.http.JakartaServletUtils;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.plugin.oidc.op.decoding.impl.RequestUtil;
+import net.shibboleth.idp.plugin.oidc.op.oauth2.decoding.impl.BaseOAuth2RequestDecoder;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Message decoder decoding OpenID Federation Resolve Entity request {@link ResolveEntityRequest}.
+ * 
+ * @since 4.3.0
+ */
+public class ResolveEntityRequestDecoder extends BaseOAuth2RequestDecoder<ResolveEntityRequest> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ResolveEntityRequestDecoder.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected ResolveEntityRequest parseMessage() throws MessageDecodingException {
+        final HttpServletRequest request = getHttpServletRequest();
+        assert request != null;
+        if (!"GET".equalsIgnoreCase(request.getMethod())) {
+            throw new MessageDecodingException("This message decoder only supports the HTTP GET method");
+        }
+        try {
+            final HTTPRequest httpRequest = JakartaServletUtils.createHTTPRequest(request);
+            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 Map<String, List<String>> parameters = httpRequest.getQueryParameters();
+            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_anchors"))
+                    .filter(Objects::nonNull)
+                    .filter(list -> list.size() > 0)
+                    .orElse(null);
+            if (trustAnchors == null) {
+                throw new MessageDecodingException("No trust_anchors included in the request");
+            }
+            final String entityType = Optional.ofNullable(parameters.get("entity_type"))
+                    .filter(Objects::nonNull)
+                    .filter(list -> list.size() == 1)
+                    .map(list -> list.get(0))
+                    .orElse(null);
+            if (entityType == null) {
+                throw new MessageDecodingException("No single entity_type value in the request");
+            }
+            return new ResolveEntityRequest(uri, subject, trustAnchors, entityType);
+        } catch (final IOException e) {
+            log.error("Could not create HTTP request from the request", e);
+            throw new MessageDecodingException(e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected String getMessageToLog(@Nullable final ResolveEntityRequest message) {
+        return message == null ? null : MoreObjects.toStringHelper(this).omitNullValues()
+                .add("subject", message.getSubject())
+                .add("trustAnchors", message.getTrustAnchors())
+                .add("entityType", message.getEntityType())
+                .add("endpointURI", getEndpointURI(message))
+                .toString();
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityRequest.java
new file mode 100644
index 00000000..79d59f05
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityRequest.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl;
+
+import java.net.URI;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.base.MoreObjects;
+import com.nimbusds.oauth2.sdk.Request;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Request message to the OpenID federation resolve entity API.
+ */
+public class ResolveEntityRequest implements Request {
+
+    /** The endpoint URI of the request. */
+    @Nonnull private final URI endpointUri;
+
+    /** The requested subject. */
+    @Nonnull @NotEmpty private final String subject;
+
+    /** The requested trust anchors. */
+    @Nonnull @NotEmpty private final List<String> trustAnchors;
+
+    /** The requested entity type to resolve. */
+    @Nonnull @NotEmpty private final String entityType;
+
+    /**
+     * 
+     * Constructor.
+     *
+     * @param uri endpoint URI
+     * @param sub subject
+     * @param anchors trust anchors
+     * @param type entity type
+     */
+    public ResolveEntityRequest(@Nonnull final URI uri,
+                  @Nonnull @NotEmpty final String sub,
+                  @Nonnull @NotEmpty final List<String> anchors,
+                  @Nonnull @NotEmpty final String type) {
+        endpointUri = Constraint.isNotNull(uri, "Endpoint URI cannot be null");
+        subject = Constraint.isNotNull(sub, "Subject cannot be empty");
+        Constraint.isNotEmpty(anchors, "Trust anchors cannot be empty");
+        trustAnchors = anchors;
+        entityType = Constraint.isNotNull(type, "Entity type cannot be empty");
+    }
+
+    /**
+     * Returns the requested subject.
+     *
+     * @return The subject.
+     */
+    @Nonnull @NotEmpty public String getSubject() {
+        return subject;
+    }
+
+    /**
+     * Returns the requested trust anchor.
+     *
+     * @return The trust anchor.
+     */
+    @Nonnull @NotEmpty public List<String> getTrustAnchors() {
+        return trustAnchors;
+    }
+
+    /**
+     * Returns the requested entity type to resolve.
+     *
+     * @return The entity type to resolve.
+     */
+    @Nonnull @NotEmpty public String getEntityType() {
+        return entityType;
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nonnull public URI getEndpointURI() {
+        return endpointUri;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public HTTPRequest toHTTPRequest() {
+        //TODO
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("subject", getSubject())
+                .add("trustAnchors", getTrustAnchors())
+                .add("entityType", getEntityType())
+                .add("endpointURI", getEndpointURI())
+                .toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ResolveEntityRequest other = (ResolveEntityRequest) obj;
+        return endpointUri.equals(other.endpointUri) && subject.equals(other.subject) &&
+                entityType.equals(other.entityType) && trustAnchors.containsAll(other.trustAnchors) &&
+                other.trustAnchors.containsAll(trustAnchors);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityResponse.java
new file mode 100644
index 00000000..73728591
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityResponse.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Response;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Response message to the OpenID federation resolve entity endpoint.
+ */
+public class ResolveEntityResponse implements Response {
+
+    /** The content type. */
+    @Nonnull public static final ContentType CONTENT_TYPE = new ContentType("application", "resolve-response+jwt");
+
+    /** The JWT included in the response. */
+    @Nonnull private final SignedJWT jwt;
+
+    /**
+     * Constructor.
+     *
+     * @param statement JWT
+     */
+    public ResolveEntityResponse(@Nonnull final SignedJWT statement) {
+        jwt = Constraint.isNotNull(statement, "Entity statement cannot be null");
+    }
+
+    /**
+     * Get the JWT included in the response.
+     *
+     * @return JWT
+     */
+    @Nonnull public SignedJWT getJWT() {
+        return jwt;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean indicatesSuccess() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public HTTPResponse toHTTPResponse() {
+        final HTTPResponse httpResponse = new HTTPResponse(HTTPResponse.SC_OK);
+        httpResponse.setEntityContentType(CONTENT_TYPE);
+        httpResponse.setContent(jwt.serialize());
+        return httpResponse;
+    }
+
+    /**
+     * Parses a federation resolve entity success response from the given HTTP response.
+     *
+     * @param httpResponse the HTTP response
+     * @return resolve entity success response
+     * @throws ParseException if HTTP response could not be parsed into resolve entity response
+     */
+    @Nonnull
+    public static ResolveEntityResponse parse(@Nonnull final HTTPResponse httpResponse)
+        throws ParseException {
+        
+        httpResponse.ensureStatusCode(HTTPResponse.SC_OK);
+        httpResponse.ensureEntityContentType(CONTENT_TYPE);
+        final String content = httpResponse.getContent();
+        
+        if (StringSupport.trimOrNull(content) == null) {
+            throw new ParseException("Message body is empty");
+        }
+
+        try {
+            final SignedJWT jwt = SignedJWT.parse(httpResponse.getContent());
+            assert jwt != null;
+            return new ResolveEntityResponse(jwt);
+        } catch (final java.text.ParseException e) {
+            throw new ParseException(e.getMessage(), e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
index f6ec9424..0493ca47 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
@@ -42,6 +42,7 @@ import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponen
 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;
 
 /**
@@ -126,8 +127,8 @@ public class DefaultEntityStatementFetchingStrategy extends AbstractIdentifiable
     public EntityStatement apply(@Nullable final CriteriaSet criteria) {
         checkComponentActive();
         final URI uri = criteriaToEndpointStrategy.apply(criteria);
-        if (uri == null) {
-            log.error("No URI could be resolved for fetching entity statement");
+        if (uri == null || StringSupport.trimOrNull(uri.getScheme()) == null) {
+            log.error("No valid URI could be resolved for fetching entity statement: {}", uri);
             return null;
         }
         log.debug("Using URI {} for fetching entity statement", uri);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityRequestCriteriaToIdentifierStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityRequestCriteriaToIdentifierStrategy.java
new file mode 100644
index 00000000..d2e77ae3
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityRequestCriteriaToIdentifierStrategy.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Strategy for extracting a resolve entity request as String via {@link ResolveEntityRequestCriterion} from
+ * {@link CriteriaSet}.
+ */
+ at ThreadSafe
+public class DefaultResolveEntityRequestCriteriaToIdentifierStrategy
+    implements Function<CriteriaSet, String> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public String apply(@Nullable final CriteriaSet criteria) {
+        return Optional.ofNullable(criteria)
+                .map(set -> set.get(ResolveEntityRequestCriterion.class))
+                .map(criterion -> criterion.getRequest())
+                .map(request -> request.toString())
+                .orElse(null);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseContainerExpirationTimeStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseContainerExpirationTimeStrategy.java
new file mode 100644
index 00000000..4730da4d
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseContainerExpirationTimeStrategy.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.oidc.metadata.cache.ExpirationTimeContext;
+
+/**
+ * Default strategy for fetching expiration time for the resolve entity response container. The expiration instant is
+ * fetched from which is before: the success response message's JWT expiration time or the instant returned by
+ * {@link ResolveEntityResponseContainer#getExpirationInstant()}.
+ */
+ at ThreadSafe
+public class DefaultResolveEntityResponseContainerExpirationTimeStrategy
+        implements Function<ExpirationTimeContext<ResolveEntityResponseContainer>, Instant> {
+
+    /** {@inheritDoc} */
+    @Nullable public Instant apply(@Nullable final ExpirationTimeContext<ResolveEntityResponseContainer> context) {
+        if (context == null) {
+            return null;
+        }
+        final Instant contextExpiration = context.getNow().plus(context.getMaxCacheDuration());
+        final ResolveEntityResponseContainer container = context.getMetadata();
+        if (container == null || container.getExpirationInstant() == null) {
+            return contextExpiration;
+        }
+        final Instant containerExpiration = container.getExpirationInstant();
+        if (container.getResponse() instanceof ResolveEntityResponse successResponse) {
+            try {
+                final Instant jwtExpiration =
+                        successResponse.getJWT().getJWTClaimsSet().getExpirationTime().toInstant();
+                return jwtExpiration.isBefore(containerExpiration) ? jwtExpiration : containerExpiration;
+            } catch (final ParseException e) {
+                // ignore, use container expiration
+            }
+        }
+        return containerExpiration;
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseFetchingStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseFetchingStrategy.java
new file mode 100644
index 00000000..13270d4f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseFetchingStrategy.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default strategy for fetching resolve entity response container via criteria.
+ */
+ at ThreadSafeAfterInit
+public class DefaultResolveEntityResponseFetchingStrategy extends AbstractIdentifiableInitializableComponent
+        implements Function<CriteriaSet, ResolveEntityResponseContainer> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultResolveEntityResponseFetchingStrategy.class);
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public ResolveEntityResponseContainer apply(@Nullable final CriteriaSet criteria) {
+        checkComponentActive();
+        if (criteria == null) {
+            return null;
+        }
+        final ResolveEntityRequestCriterion requestCriterion = criteria.get(ResolveEntityRequestCriterion.class);
+        if (requestCriterion == null) {
+            log.debug("No request criterion given, returning null");
+            return null;
+        }
+        final ResolveEntityResponseCriterion responseCriterion = criteria.get(ResolveEntityResponseCriterion.class);
+        if (responseCriterion == null) {
+            log.debug("No response criterion given, returning null");
+            return null;
+        }
+        final ResolveEntityContainerExpirationCriterion expirationCriterion =
+                criteria.get(ResolveEntityContainerExpirationCriterion.class);
+        if (expirationCriterion == null) {
+            log.debug("No expiration criterion given, returning null");
+            return null;
+        }
+        return new ResolveEntityResponseContainer(responseCriterion.getResponse(), requestCriterion.getRequest(),
+                expirationCriterion.getExpirationInstant());
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseIdentifierExtractionStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseIdentifierExtractionStrategy.java
new file mode 100644
index 00000000..a5009e49
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseIdentifierExtractionStrategy.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Default strategy for extracting request identifier out of {@link ResolveEntityResponseContainer}.
+ */
+ at ThreadSafe
+public class DefaultResolveEntityResponseIdentifierExtractionStrategy
+        implements Function<ResolveEntityResponseContainer, String> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public String apply(@Nullable final ResolveEntityResponseContainer responseContainer) {
+        return Optional.ofNullable(responseContainer)
+                .map(container -> container.getRequest())
+                .map(request -> request.toString())
+                .orElse(null);
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityContainerExpirationCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityContainerExpirationCriterion.java
new file mode 100644
index 00000000..83888cab
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityContainerExpirationCriterion.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.time.Instant;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing expiration instant for a resolve entity container.
+ */
+public class ResolveEntityContainerExpirationCriterion implements Criterion {
+
+    /** The expiration instant. */
+    @Nonnull private final Instant instant;
+
+    /**
+     * Constructor.
+     *
+     * @param expirationInstant expiration instant, must not be null
+     */
+    public ResolveEntityContainerExpirationCriterion(@Nonnull final Instant expirationInstant) {
+        instant = Constraint.isNotNull(expirationInstant, "Expiration instant cannot be null");
+    }
+
+    /**
+     * Get the expiration instant.
+     * 
+     * @return the expiration instant
+     */
+    @Nonnull public Instant getExpirationInstant() {
+        return instant;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "ResolveEntityContainerExpirationCriterion [instant=" + instant.toEpochMilli() + "]";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(instant);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ResolveEntityContainerExpirationCriterion other = (ResolveEntityContainerExpirationCriterion) obj;
+        return instant.equals(other.instant);
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityRequestCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityRequestCriterion.java
new file mode 100644
index 00000000..d59a72f4
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityRequestCriterion.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing request message to a resolve entity API.
+ */
+public class ResolveEntityRequestCriterion implements Criterion {
+
+    /** The request message. */
+    @Nonnull private final ResolveEntityRequest request;
+
+    /**
+     * Constructor.
+     *
+     * @param requestMessage request message, must not be null
+     */
+    public ResolveEntityRequestCriterion(@Nonnull final ResolveEntityRequest requestMessage) {
+        request = Constraint.isNotNull(requestMessage, "Request cannot be null");
+    }
+
+    /**
+     * Get the request message.
+     * 
+     * @return the request message
+     */
+    @Nonnull public ResolveEntityRequest getRequest() {
+        return request;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "ResolveEntityRequestCriterion [request=" + request + "]";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(request);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ResolveEntityRequestCriterion other = (ResolveEntityRequestCriterion) obj;
+        //TODO: proper equals-check
+        return request.equals(other.request);
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseContainer.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseContainer.java
new file mode 100644
index 00000000..1ae9856f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseContainer.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A container class for metadata caches carrying request and response message details related to Resolve Entity API.
+ */
+public class ResolveEntityResponseContainer implements Serializable {
+
+    /** Serial version UID. */
+    private static final long serialVersionUID = 756269369356865370L;
+
+    /** Response message. */
+    @Nonnull private final Response response;
+
+    /** Request message. */
+    @Nonnull private final ResolveEntityRequest request;
+
+    /** Expiration instant for this container. */
+    @Nonnull private final Instant expiration;
+
+    /**
+     * Constructor.
+     *
+     * @param responseMessage response message
+     * @param requestMessage request message
+     * @param expirationInstant expiration instant
+     */
+    public ResolveEntityResponseContainer(@Nonnull final Response responseMessage,
+            @Nonnull final ResolveEntityRequest requestMessage, @Nonnull final Instant expirationInstant) {
+        response = Constraint.isNotNull(responseMessage, "Response message cannot be null");
+        request = Constraint.isNotNull(requestMessage, "Request message cannot be null");
+        expiration = Constraint.isNotNull(expirationInstant, "Expiration instant cannot be null");
+    }
+
+    /**
+     * Get response message.
+     * 
+     * @return response message
+     */
+    public Response getResponse() {
+        return response;
+    }
+
+    /**
+     * Get request message.
+     * 
+     * @return request message
+     */
+    public ResolveEntityRequest getRequest() {
+        return request;
+    }
+
+    /**
+     * Get expiration instant.
+     * 
+     * @return expiration instant
+     */
+    public Instant getExpirationInstant() {
+        return expiration;
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseCriterion.java
new file mode 100644
index 00000000..43691690
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseCriterion.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing response message to a resolve entity request.
+ */
+public class ResolveEntityResponseCriterion implements Criterion {
+
+    /** The response message. */
+    @Nonnull private final Response response;
+
+    /**
+     * Constructor.
+     *
+     * @param responseMessage response message, must not be null
+     */
+    public ResolveEntityResponseCriterion(@Nonnull final Response responseMessage) {
+        response = Constraint.isNotNull(responseMessage, "Response cannot be null");
+    }
+
+    /**
+     * Get the response message.
+     * 
+     * @return the response message
+     */
+    @Nonnull public Response getResponse() {
+        return response;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "ResolveEntityResponseCriterion [response=" + response + "]";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(response);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ResolveEntityResponseCriterion other = (ResolveEntityResponseCriterion) obj;
+        return response.equals(other.response);
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TrustAnchorEntityIDsCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TrustAnchorEntityIDsCriterion.java
new file mode 100644
index 00000000..97934e88
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TrustAnchorEntityIDsCriterion.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing trust anchor entity IDs in resolve entity request.
+ */
+public class TrustAnchorEntityIDsCriterion implements Criterion {
+
+    /** The entity ID values. */
+    @Nonnull final List<String> values;
+
+    /**
+     * Constructor.
+     *
+     * @param entityIds the entity ID values, must not be null nor empty
+     */
+    public TrustAnchorEntityIDsCriterion(@Nonnull @NotEmpty final List<String> entityIds) {
+        Constraint.isNotEmpty(entityIds, "Entity IDs cannot be null nor empty");
+        values = entityIds;
+    }
+
+    /**
+     * Get the entity ID values.
+     * 
+     * @return the entity ID values
+     */
+    @Nonnull @NotEmpty public List<String> getValues() {
+        return values;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "TrustAnchorEntityIDsCriterion [values=" + values + "]";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(values);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final TrustAnchorEntityIDsCriterion other = (TrustAnchorEntityIDsCriterion) obj;
+        return values.containsAll(other.values) && other.values.containsAll(values);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java
new file mode 100644
index 00000000..53006b02
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.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.idp.plugin.oidc.op.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.context.EventContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ErrorObject;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.config.ResolveEntityCachedErrorResponseLifetimeLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityContainerExpirationCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityRequestCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityResponseContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityResponseCriterion;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractBuildErrorResponseFromEvent;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.profile.messaging.JSONErrorResponse;
+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 RelyingPartyResolveEntityContext} is found, it's exploited
+ * for storing the response message in the configured {@link this#responseCache}.
+ */
+public class BuildResolveEntityErrorResponseFromEvent extends AbstractBuildErrorResponseFromEvent<JSONErrorResponse> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(BuildResolveEntityErrorResponseFromEvent.class);
+
+    /** 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, RelyingPartyResolveEntityContext> resolveEntityContextLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public BuildResolveEntityErrorResponseFromEvent() {
+        cachedResponseLifetimeLookupStrategy = new ResolveEntityCachedErrorResponseLifetimeLookupFunction();
+        final Function<ProfileRequestContext, RelyingPartyResolveEntityContext> recls =
+                new ChildContextLookup<>(RelyingPartyResolveEntityContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert recls != null;
+        resolveEntityContextLookupStrategy = recls;
+
+    }
+
+    /**
+     * 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,RelyingPartyResolveEntityContext> 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 JSONErrorResponse buildErrorResponse(final ErrorObject error,
+            final ProfileRequestContext profileRequestContext) {
+        final JSONErrorResponse response = new JSONErrorResponse(error);
+        final RelyingPartyResolveEntityContext resolveEntityContext =
+                resolveEntityContextLookupStrategy.apply(profileRequestContext);
+
+        if (resolveEntityContext != null && resolveEntityContext.getValidatedRequest() != null) {
+            final ResolveEntityResponseCriterion responseCriterion = new ResolveEntityResponseCriterion(response);
+            final Duration cachedResponseLifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+            final Instant expiration = Instant.now().plus(cachedResponseLifetime);
+            assert expiration != null;
+            final ResolveEntityContainerExpirationCriterion expirationCriterion =
+                    new ResolveEntityContainerExpirationCriterion(expiration);
+            final ResolveEntityRequest validatedRequest = resolveEntityContext.getValidatedRequest();
+            assert validatedRequest != null;
+            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);
+            }
+        }
+
+        return response;
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityResponse.java
new file mode 100644
index 00000000..bf5a7b2b
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityResponse.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+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.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+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;
+
+    /** 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;
+    }
+    
+    /**
+     * 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");
+    }
+
+    /** {@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 Pair<List<EntityStatement>,OIDCClientInformation> selectedTrustChain =
+                trustChainContext.getSelectedTrustChain();
+        if (selectedTrustChain == null) {
+            log.debug("{} No selected trust chain found form the context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;                                    
+        }
+        final OIDCClientInformation clientInformation = selectedTrustChain.getSecond();
+        if (clientInformation == null) {
+            log.debug("{} No client information set in the trust chain context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;                        
+        }
+        builder.claim("metadata",
+                CollectionSupport.singletonMap("openid_relying_party", clientInformation.toJSONObject()));
+
+        final List<EntityStatement> trustChain = selectedTrustChain.getFirst();
+        if (trustChain == null || trustChain.isEmpty()) {
+            log.debug("{} No selected trust chain set in the trust chain context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;                        
+        }
+        builder.claim("trust_chain", trustChain.stream()
+                .map(statement -> statement.getSignedStatement().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));
+
+
+        return 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.getClaimsSet().getExpirationTime().toInstant();
+            metadataExpiration = metadataExpiration == null ? statementExpiration : 
+                statementExpiration.isBefore(metadataExpiration) ? statementExpiration : metadataExpiration;
+        }
+        return metadataExpiration;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundResolveEntityResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundResolveEntityResponse.java
new file mode 100644
index 00000000..4ceb9033
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundResolveEntityResponse.java
@@ -0,0 +1,230 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
+
+import java.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 com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.config.ResolveEntityCachedSuccessResponseLifetimeLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityContainerExpirationCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityRequestCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityResponseContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityResponseCriterion;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.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 EntityStatement}. The response is put in the {@link this#responseCache} using the lifetime resolved via
+ * {@link this#cachedResponseLifetimeLookupStrategy}.
+ * 
+ * @since 4.3.0
+ */
+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, RelyingPartyResolveEntityContext> resolveEntityContextLookupStrategy;
+
+    /** 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 RelyingPartyResolveEntityContext resolveEntityContext;
+
+    /**
+     * Constructor.
+     */
+    public FormOutboundResolveEntityResponse() {
+        final Function<ProfileRequestContext,EntityStatementContext> escls =
+                new ChildContextLookup<>(EntityStatementContext.class).compose(
+                        new OutboundMessageContextLookup());
+        assert escls != null;
+        entityStatementContextLookupStrategy = escls;
+        final Function<ProfileRequestContext, RelyingPartyResolveEntityContext> recls =
+                new ChildContextLookup<>(RelyingPartyResolveEntityContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert recls != null;
+        resolveEntityContextLookupStrategy = recls;
+        cachedResponseLifetimeLookupStrategy = new ResolveEntityCachedSuccessResponseLifetimeLookupFunction();
+    }
+
+    /**
+     * 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 resolve entity context
+     * 
+     * @param strategy What to set.
+     */
+    public void setResolveEntityContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyResolveEntityContext> strategy) {
+        checkSetterPreconditions();
+        resolveEntityContextLookupStrategy = 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;
+        }
+        resolveEntityContext = resolveEntityContextLookupStrategy.apply(profileRequestContext);
+        if (resolveEntityContext == null) {
+            log.error("{} Could not resolve 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 Response cachedResponse = resolveEntityContext.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 ResolveEntityResponseCriterion responseCriterion = new ResolveEntityResponseCriterion(response);
+        final ResolveEntityRequest validatedRequest = resolveEntityContext.getValidatedRequest();
+        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 ResolveEntityContainerExpirationCriterion expirationCriterion =
+                new ResolveEntityContainerExpirationCriterion(expiration);
+        if (validatedRequest == null) {
+            log.error("{} No validated request found from the resolve entity context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        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);
+        }
+        profileRequestContext.ensureOutboundMessageContext().setMessage(response);
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedResolveEntityRespomse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedResolveEntityRespomse.java
new file mode 100644
index 00000000..de2ebfbc
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedResolveEntityRespomse.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
+
+import java.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.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityRequestCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityResponseContainer;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.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 RelyingPartyResolveEntityContext} and a corresponding event ID is published.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link OidFederationEventIds#CACHED_RESOLVE_ENTITY_RESPONSE}
+ * 
+ * @since 4.3.0
+ */
+public class LookupCachedResolveEntityRespomse extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(LookupCachedResolveEntityRespomse.class);
+
+    /** Strategy used to locate the resolve entity context. */
+    @Nonnull
+    private Function<ProfileRequestContext, RelyingPartyResolveEntityContext> resolveEntityContextLookupStrategy;
+
+    /** Metadata cache for cached response containers. */
+    @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> responseCache;
+
+    /** Trust chain context to operate on. */
+    @NonnullBeforeExec private RelyingPartyResolveEntityContext resolveEntityContext;
+
+    /** Request message to operate on. */
+    @NonnullBeforeExec private ResolveEntityRequest validatedRequest;
+
+    /**
+     * Constructor.
+     */
+    public LookupCachedResolveEntityRespomse() {
+        final Function<ProfileRequestContext, RelyingPartyResolveEntityContext> recls =
+                new ChildContextLookup<>(RelyingPartyResolveEntityContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert recls != null;
+        resolveEntityContextLookupStrategy = recls;
+    }
+
+    /**
+     * Set the strategy used to locate the resolve entity context
+     * 
+     * @param strategy What to set.
+     */
+    public void setResolveEntityContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyResolveEntityContext> strategy) {
+        checkSetterPreconditions();
+        resolveEntityContextLookupStrategy = 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;
+        }
+
+        resolveEntityContext = resolveEntityContextLookupStrategy.apply(profileRequestContext);
+        if (resolveEntityContext == null || resolveEntityContext.getValidatedRequest() == null) {
+            log.error("{} Could not resolve validated resolve entity request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        validatedRequest = resolveEntityContext.getValidatedRequest();
+
+        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);
+                resolveEntityContext.setCachedResponse(cachedResponse.getResponse());
+                log.debug("{} Response found from the cache, publishing event {}", getLogPrefix(),
+                        OidFederationEventIds.CACHED_RESOLVE_ENTITY_RESPONSE);
+                ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.CACHED_RESOLVE_ENTITY_RESPONSE);
+                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/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java
new file mode 100644
index 00000000..a23864ce
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.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.idp.plugin.oidc.op.oidfed.profile.impl;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/**
+ * OpenID Federation -specific constants to use for {@link org.opensaml.profile.action.ProfileAction}
+ * {@link org.opensaml.profile.context.EventContext}s.
+ */
+public class OidFederationEventIds {
+
+    /**
+     * 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";
+
+    /**
+     * ID of event returned if cached resolve entity response was found and set to the context.
+     */
+    @Nonnull @NotEmpty public static final String CACHED_RESOLVE_ENTITY_RESPONSE = "CachedResolveEntityResponseFound";
+
+    /**
+     * ID of event returned if the given trust anchor is invalid.
+     */
+    @Nonnull @NotEmpty public static final String INVALID_TRUST_ANCHOR = "InvalidTrustAnchor";
+
+    /**
+     * ID of event returned if the given subject is invalid.
+     */
+    @Nonnull @NotEmpty public static final String INVALID_SUBJECT = "InvalidSubject";
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/RelyingPartyResolveEntityContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/RelyingPartyResolveEntityContext.java
new file mode 100644
index 00000000..0a86ab99
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/RelyingPartyResolveEntityContext.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+
+/**
+ * Subcontext carrying information for resolve entity related to a relying party.
+ * 
+ * @since 4.3.0
+ */
+public final class RelyingPartyResolveEntityContext extends BaseContext {
+
+    /** Validated (possibly modified) resolve entity request. */
+    @Nullable private ResolveEntityRequest validatedRequest;
+
+    /** Cached response message. */
+    @Nullable private Response cachedResponse;
+
+    /**
+     * Get the validated (possibly modified) resolve entity request.
+     * 
+     * @return the validated request
+     */
+    @Nullable public ResolveEntityRequest getValidatedRequest() {
+        return validatedRequest;
+    }
+
+    /**
+     * Set the the validated (possibly modified) resolve entity request.
+     * 
+     * @param request the validated request
+     * @return this context
+     */
+    @Nonnull public RelyingPartyResolveEntityContext setValidatedRequest(
+            @Nullable final ResolveEntityRequest request) {
+        validatedRequest = request;
+        return this;
+    }
+
+    /**
+     * Get the cached response message.
+     * 
+     * @return the cached response
+     */
+    @Nullable public Response getCachedResponse() {
+        return cachedResponse;
+    }
+
+    /**
+     * Set the cached response message.
+     * 
+     * @param response cached response
+     * @return this context
+     */
+    @Nonnull public RelyingPartyResolveEntityContext setCachedResponse(@Nullable final Response response) {
+        cachedResponse = response;
+        return this;
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java
index cec81961..5cba50d4 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java
@@ -137,8 +137,7 @@ public class SelectTrustChain extends AbstractProfileAction {
 
         trustChainContext = trustChainContextLookupStrategy.apply(profileRequestContext);
         if (trustChainContext == null || trustChainContext.getPolicyCompliantTrustChains() == null) {
-            log.error("{} Unable to locate policy-compliant trust chains", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            log.debug("{} Unable to locate policy-compliant trust chains, nothing to do", getLogPrefix());
             return false;
         }
         return true;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateResolveEntityRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateResolveEntityRequest.java
new file mode 100644
index 00000000..02112173
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateResolveEntityRequest.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
+
+import java.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 net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.LocalKeyContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.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 RelyingPartyResolveEntityContext#setValidatedRequest(ResolveEntityRequest)}.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * 
+ * @since 4.3.0
+ */
+public class ValidateResolveEntityRequest extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidateResolveEntityRequest.class);
+
+    /** Strategy used to create the resolve entity context. */
+    @Nonnull
+    private Function<ProfileRequestContext, RelyingPartyResolveEntityContext> resolveEntityContextCreationStrategy;
+
+    /** 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;
+
+    /** Trust chain context to operate on. */
+    @NonnullBeforeExec private RelyingPartyResolveEntityContext resolveEntityContext;
+
+    /**
+     * Constructor.
+     */
+    public ValidateResolveEntityRequest() {
+        final Function<ProfileRequestContext, RelyingPartyResolveEntityContext> reccs =
+                new ChildContextLookup<>(RelyingPartyResolveEntityContext.class, true).compose(
+                        new InboundMessageContextLookup());
+        assert reccs != null;
+        resolveEntityContextCreationStrategy = reccs;
+    }
+
+    /**
+     * Set the strategy used to return or create the resolve entity context.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setResolveEntityContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyResolveEntityContext> strategy) {
+        checkSetterPreconditions();
+        resolveEntityContextCreationStrategy = Constraint.isNotNull(strategy,
+                "RelyingPartyResolveEntityContext creation strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup the trust chain context.
+     * 
+     * @param strategy 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;
+        }
+        
+        resolveEntityContext = resolveEntityContextCreationStrategy.apply(profileRequestContext);
+        if (resolveEntityContext == 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);
+        resolveEntityContext.setValidatedRequest(
+                new ResolveEntityRequest(requestMessage.getEndpointURI(), requestMessage.getSubject(),
+                        validatedAnchors, requestMessage.getEntityType()));
+    }
+
+    /**
+     * 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 == null || 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/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateSelectedTrustChain.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateSelectedTrustChain.java
new file mode 100644
index 00000000..88e63c4a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateSelectedTrustChain.java
@@ -0,0 +1,224 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
+
+import java.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 com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.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}
+ * 
+ * @since 4.3.0
+ */
+public class ValidateSelectedTrustChain extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidateSelectedTrustChain.class);
+
+    /** Metadata cache for entity configurations. */
+    @NonnullAfterInit private MetadataCache<EntityStatement> 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, RelyingPartyResolveEntityContext> 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, RelyingPartyResolveEntityContext> recls =
+                new ChildContextLookup<>(RelyingPartyResolveEntityContext.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<EntityStatement> 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,RelyingPartyResolveEntityContext> 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 RelyingPartyResolveEntityContext resolveEntityContext =
+                resolveEntityContextLookupStrategy.apply(profileRequestContext);
+        if (resolveEntityContext == null || resolveEntityContext.getValidatedRequest() == null) {
+            log.error("{} Could not resolve validated request messaGE", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        validatedRequest = resolveEntityContext.getValidatedRequest();
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final RelyingPartyTrustChainContext trustChainContext =
+                trustChainContextLookupStrategy.apply(profileRequestContext);
+        final Pair<List<EntityStatement>, OIDCClientInformation>  selectedTrustChain =
+                trustChainContext != null ? trustChainContext.getSelectedTrustChain() : null;
+        if (selectedTrustChain == null || selectedTrustChain.getFirst() == null) {
+            final List<Pair<List<EntityStatement>,OIDCClientInformation>> 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.getFirst();
+        assert candidateChain != null;
+        final String candidateAnchor = candidateChain.get(candidateChain.size() - 1).getEntityID().getValue();
+        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.getFirst()));
+            } else {
+                final List<List<EntityStatement>> rejectedChains = new ArrayList<>(rejectedTrustChains);
+                rejectedChains.add(selectedTrustChain.getFirst());
+                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<EntityStatement> result = entityConfigurationCache.get(new CriteriaSet(subjectCriterion));
+            if (result.size() == 1) {
+                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/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 5bac9ce4..05fc2260 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -722,6 +722,27 @@
         </property>
     </bean>
 
+    <bean id="shibboleth.oidc.DefaultResolveEntityApiMappedErrors"
+            parent="shibboleth.oidc.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="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.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.idp.plugin.oidc.op.oidfed.profile.impl.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>
+            </map>
+        </property>
+    </bean>
+
     <bean id="shibboleth.oidc.DefaultUnregisteredClientPolicyFilename" class="java.lang.String" factory-method="valueOf">
         <constructor-arg value="%{idp.oidc.DefaultUnregisteredClientPolicyFile:%{idp.home}/conf/oidc-unregistered-client-policy.json}" />
     </bean>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
new file mode 100644
index 00000000..57377fad
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
@@ -0,0 +1,198 @@
+<?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.oidc.profileId" class="java.lang.String"
+        c:_0="#{T(net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationResolveEntityProfileConfiguration).PROFILE_ID}" />
+
+    <bean id="shibboleth.oidc.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.oidfedresolve:OIDFED.ResolveEntity}" />
+
+    <util:constant id="shibboleth.metrics.ProfileCounter"
+        static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationResolveEntityProfileConfiguration.PROFILE_COUNTER" />
+
+    <bean id="DecodeMessage" class="org.opensaml.profile.action.impl.DecodeMessage" scope="prototype">
+        <constructor-arg>
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.decoding.impl.ResolveEntityRequestDecoder" scope="prototype"
+                p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+                p:removeIpAddressFromEndpointUri="%{idp.oidc.logging.removeIpAddressFromProtocolMessage:false}"
+                p:customRequestParser="#{getObject('%{idp.oidc.requestParser.ResolveEntityRequest:}'.trim())}"/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeOutboundResponseMessageContext"
+        scope="prototype" />
+
+    <bean id="shibboleth.ClientIDLookupStrategy" parent="shibboleth.Functions.Expression"
+        c:expression="new com.nimbusds.oauth2.sdk.id.ClientID(#input.getMessage().getSubject())" />
+
+    <bean id="InitializeRelyingPartyContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeRelyingPartyContext" scope="prototype"
+        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+        p:inbound="true" />
+
+    <bean id="shibboleth.oidfed.ResolveEntityResponseMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+        <constructor-arg>
+            <bean p:cacheId="DefaultResolveEntityResponseMetadataCache" parent="shibboleth.oidfed.ResolveEntityResponseMetadataCacheBuilderSpec"
+                p:cleanupTaskInterval="PT30S"/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="shibboleth.oidfed.ResolveEntityResponseMetadataCacheBuilderSpec"
+        class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
+        p:minCacheDuration="%{idp.oidfed.resolveEntity.maxRefreshDelay:PT1S}"
+        p:maxCacheDuration="%{idp.oidfed.resolveEntity.maxRefreshDelay:PT30S}">
+        <property name="criteriaToIdentifierStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityRequestCriteriaToIdentifierStrategy" />
+        </property>
+        <property name="identifierExtractionStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityResponseIdentifierExtractionStrategy" />
+        </property>
+        <property name="metadataExpirationTimeStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityResponseContainerExpirationTimeStrategy"/>
+        </property>
+        <property name="metadataFilterStrategy">
+            <bean parent="shibboleth.BiFunctions.Expression" c:expression="#input1"/>
+        </property>
+        <property name="fetchStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityResponseFetchingStrategy" />
+        </property>
+    </bean>
+
+    <bean id="ValidateRequest" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateResolveEntityRequest"
+        scope="prototype"
+        p:localTrustAnchorsCache-ref="#{'%{idp.oidfed.resove-entity.LocalTrustAnchorsMetadataCache:shibboleth.oidfed.LocalTrustAnchorsMetadataCache}'.trim()}" />
+
+    <bean id="LookupCachedResolveEntityRespomse"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.LookupCachedResolveEntityRespomse"
+        scope="prototype"
+        p:responseCache-ref="shibboleth.oidfed.ResolveEntityResponseMetadataCache" />
+
+    <bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
+        scope="prototype"
+        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
+        p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
+        p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"/>
+
+    <bean id="DefaultMetadataPolicyEnforcer"
+        class="net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyEnforcer" />
+
+    <bean id="DefaultTrustChainMetadataPolicyMergingStrategy" 
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
+        p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.resolve.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"/>
+
+    <bean id="DefaultLocalMetadataPolicyStrategy"
+        parent="shibboleth.Functions.Constant">
+        <constructor-arg name="target">
+            <util:map/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="SelectTrustChain" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.SelectTrustChain"
+        scope="prototype">
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.Expression"
+                c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext))" />
+        </property>
+    </bean>
+
+    <bean id="ValidateSelectedTrustChain" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateSelectedTrustChain"
+        scope="prototype"
+        p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"/>
+
+    <bean id="ResolveTrustMarks" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustMarks"
+        scope="prototype"
+        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}">
+        <property name="trustEngine">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
+                <constructor-arg>
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementCredentialResolver" />
+                 </constructor-arg>
+            </bean>
+        </property>
+        <property name="trustedTrustMarkIssuersLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"
+                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+        </property>
+    </bean>
+
+    <bean id="PopulateEntityStatementSignatureSigningParameters"
+        class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters" scope="prototype"
+        c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
+        p:securityParametersContextLookupStrategy-ref="EntityStatementSecurityParametersContextLookupStrategy">
+        <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.entity.sigalg:RS256}" />
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="EntityStatementSecurityParametersContextLookupStrategy" parent="shibboleth.Functions.Compose"
+            c:g-ref="shibboleth.oidc.ChildLookupOrCreate.JWTSecurityParameters"
+            c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+
+    <bean id="EntityStatementSecurityParametersCreationViaMessageContextStrategy" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g" ref="EntityStatementSecurityParametersContextLookupStrategy" />
+        <constructor-arg name="f">
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input.getParent()" />
+        </constructor-arg>
+    </bean>
+
+    <bean id="BuildEntityStatement"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.BuildResolveEntityResponse" scope="prototype"
+        p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy"
+        p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}">
+        <property name="subjectLookupStrategy">
+            <bean parent="shibboleth.Functions.Expression"
+                c:expression="#input.ensureInboundMessageContext().getMessage().getSubject()" />
+        </property>    
+    </bean>
+
+    <bean id="SignEntityStatement" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+            scope="prototype" c:executionDirection="OUTBOUND ">
+        <constructor-arg name="messageHandler">
+            <bean id="SignEntityStatementHandler"
+                class="net.shibboleth.oidc.security.impl.SignJWTHandler" scope="prototype" p:logName="Entity Statement"
+                p:securityParametersLookupStrategy-ref="EntityStatementSecurityParametersCreationViaMessageContextStrategy"
+                p:typeHeader="entity-statement+jwt">
+                <property name="claimsToSignLookupStrategy">
+                     <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.JWTClaimsSetFromEntityStatementLookupFunction" />
+                </property>
+                <property name="jwtUpdateConsumer">
+                    <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.EntityStatementUpdateStrategy" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+
+    <bean id="FormOutboundMessage"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.FormOutboundResolveEntityResponse" scope="prototype"
+        p:responseCache-ref="shibboleth.oidfed.ResolveEntityResponseMetadataCache" />
+
+    <bean id="BuildErrorResponseFromEvent"
+        class="net.shibboleth.idp.plugin.oidc.op.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>
+
+</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
new file mode 100644
index 00000000..903e3fcc
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
@@ -0,0 +1,77 @@
+<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="oidc/abstract-api">
+
+    <action-state id="InitializeMandatoryContexts">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="PopulateMetricContext" />
+        <evaluate expression="FlowStartPopulateAuditContext" />
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="DecodeMessage">
+            <set name="flowScope.transitionAfterDecode" value="'SelectConfiguration'" />
+         </transition>
+    </action-state>
+
+    <action-state id="SelectConfiguration">
+        <evaluate expression="InitializeRelyingPartyContext" />
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+        <evaluate expression="SelectProfileConfiguration" />
+        <evaluate expression="PostLookupPopulateAuditContext" />
+        <evaluate expression="PopulateInboundInterceptContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckInboundInterceptContext" />
+    </action-state>
+
+    <decision-state id="CheckInboundInterceptContext">
+        <on-entry>
+            <set name="flowScope.skipOAuth2ClientAuth" value="true" />
+        </on-entry>
+        <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+            then="LookupCachedResponse" else="DoInboundInterceptSubflow" />
+    </decision-state>
+
+    <subflow-state id="DoInboundInterceptSubflow" subflow="intercept">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="LookupCachedResponse" />
+    </subflow-state>
+
+    <action-state id="LookupCachedResponse">
+        <evaluate expression="ValidateRequest" />
+        <evaluate expression="LookupCachedResolveEntityRespomse" />
+        <evaluate expression="'proceed'" />
+        <transition on="CachedResolveEntityResponseFound" to="BuildResponseMessage" />
+        <transition on="proceed" to="ResolveTrustChains" />
+    </action-state>
+
+    <action-state id="ResolveTrustChains">
+        <evaluate expression="ResolveTrustChains" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="SelectTrustChain" />
+    </action-state>
+
+    <action-state id="SelectTrustChain">
+        <evaluate expression="SelectTrustChain" />
+        <evaluate expression="ValidateSelectedTrustChain" />
+        <evaluate expression="ResolveTrustMarks" />
+        <evaluate expression="'proceed'" />
+        <transition on="ReselectTrustChain" to="SelectTrustChain" />
+        <transition on="proceed" to="BuildResponse" />
+    </action-state>
+
+
+    <action-state id="BuildResponse">
+        <evaluate expression="PopulateEntityStatementSignatureSigningParameters" />
+        <evaluate expression="BuildEntityStatement" />
+        <evaluate expression="SignEntityStatement" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="BuildResponseMessage" />
+    </action-state>
+
+    <bean-import resource="resolve-entity-beans.xml" />
+
+</flow>
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 0eefd026..3c10ef46 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -109,7 +109,7 @@
     <bean id="AbstractOIDFederationProfile" abstract="true"
         p:securityConfiguration-ref="shibboleth.oidc.federation.DefaultSecurityConfiguration" />
 
-    <bean id="OIDFED.Configuration" parent="AbstractOIDCProfile" lazy-init="true"
+    <bean id="OIDFED.Configuration" parent="AbstractOIDFederationProfile" lazy-init="true"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationEntityConfigurationProfileConfiguration"
         p:issuer-ref="shibboleth.oidc.issuer"
         p:authorityHints="%{idp.oidfed.entity.authorityHints:https://example.org}" />
@@ -122,6 +122,9 @@
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationExplicitRegistrationProfileConfiguration"
         p:mandatoryTrustMarks="%{idp.oidfed.explicitRegistration.mandatoryTrustMarks:}" />
 
+    <bean id="OIDFED.ResolveEntity" parent="AbstractOIDFederationProfile" lazy-init="true"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationResolveEntityProfileConfiguration" />
+
     <!-- Metadata-driven variants. -->
     
     <bean id="AbstractMDDrivenOIDCProfile" parent="AbstractMDDrivenProfile" abstract="true">
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java
new file mode 100644
index 00000000..bdb6fa75
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed;
+
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.oidc.profile.messaging.JSONErrorResponse;
+
+/**
+ * Flow tests for the OpenID federation resolve entity flow.
+ */
+public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
+    
+    public static final String FLOW_ID = "oidfed/resolve-entity";
+    
+    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 testInvalidSubject() throws Exception {
+        request.setMethod("GET");
+        request.setQueryString("sub=mockClientId&trust_anchors=" + 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();
+        configureMockHttpClient(clientId);
+        request.setQueryString("sub=" + clientId + "&trust_anchors=mockAnchors&entity_type=openid_relying_party");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_trust_anchor");
+    }
+
+    @Test
+    public void testWithTrustedTrustAnchor() throws Exception {
+        request.setMethod("GET");
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId);
+        request.setQueryString("sub=" + clientId + "&trust_anchors=" + 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.getJWTClaimsSet().getSubject(), clientId);
+    }
+
+    protected JSONErrorResponse parseErrorResponse(final FlowExecutionResult result) {
+        final Response response = parseResponse(result);
+        Assert.assertTrue(response instanceof JSONErrorResponse);
+        return (JSONErrorResponse) response;
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 0d2ac430..faa7eee3 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -47,6 +47,7 @@
                 <bean parent="OAUTH2.Revocation" />
                 <bean parent="OAUTH2.PAR" />
                 <bean parent="OIDFED.Configuration" p:cachedEntityStatementLifetime="PT2S" />
+                <bean parent="OIDFED.ResolveEntity" />
             </list>
         </property>
     </bean>

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


More information about the commits mailing list