[java-idp-oidc] 30/31: JOIDC-222 - Support for OpenID Federation

Henri Mikkonen henri.mikkonen at iki.fi
Tue Jun 24 08:53:06 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=58274973172dcf2d327c04047d9b8ad4ec8f1513

commit 58274973172dcf2d327c04047d9b8ad4ec8f1513
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri May 16 11:38:18 2025 +0300

    JOIDC-222 - Support for OpenID Federation
    
    https://shibboleth.atlassian.net/browse/JOIDC-222
    
    - Added special handling for claims that need transformation from space-separated string into a list
      - Property 'idp.oidfed.policy.arraysAsSpaceSeparatedList' can be used for setting the names, defaults to 'scope'
      - Property is wired for deserializer and trust chain resolver SWF actions (ResolveTrustChains and ValidateProvidedTrustChain)
    - Improved explicit registration
      - Entity configuration is taken from the request message, not from the RP's entity configuration endpoint
    - Improved testing
---
 ...ClientMetadataFromTrustChainLookupStrategy.java |   5 +
 .../DefaultTrustChainFetchingStrategy.java         |  40 ++-
 .../FederationMetadataPolicyDeserializer.java      |  61 ++++-
 .../policy/FederationMetadataPolicyHelper.java     |  80 ++++++
 .../impl/AbstractTrustChainResolutionAction.java   | 297 +++++++++++++++++++++
 .../op/oidfed/profile/impl/ResolveTrustChains.java | 160 ++++-------
 .../profile/impl/ValidateProvidedTrustChain.java   | 120 +--------
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   3 +-
 .../oidc/metadata-lookup/metadata-lookup-beans.xml |  11 +-
 .../idp/flows/oidfed/register/register-beans.xml   |  55 +++-
 .../oidfed/resolve-entity/resolve-entity-beans.xml |   3 +-
 .../flow/oidfed/AbstractFederationFlowTest.java    |  26 ++
 .../AuthorizeFlowAutomaticRegistrationTest.java    |  23 ++
 ...shedAuthorizeFlowAutomaticRegistrationTest.java |  14 +
 .../profile/flow/oidfed/RegistrationFlowTest.java  |  92 ++++++-
 15 files changed, 726 insertions(+), 264 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultClientMetadataFromTrustChainLookupStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultClientMetadataFromTrustChainLookupStrategy.java
index 55c4cef6..f9893472 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultClientMetadataFromTrustChainLookupStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultClientMetadataFromTrustChainLookupStrategy.java
@@ -50,16 +50,21 @@ public class DefaultClientMetadataFromTrustChainLookupStrategy
         final OIDCClientMetadata configurationMetadata = chain.get(0).getClaimsSet().getRPMetadata();
         final OIDCClientMetadata subordinateMetadata = chain.get(1).getClaimsSet().getRPMetadata();
         if (subordinateMetadata == null || subordinateMetadata.toJSONObject().isEmpty()) {
+            log.trace("Subordinate statement metadata is empty, using entity configuration");
             return configurationMetadata;
         }
         if (configurationMetadata == null || configurationMetadata.toJSONObject().isEmpty()) {
+            log.trace("Entity configuration metadata is empty, using subordinate statement");
             return subordinateMetadata;
         }
         final Map<String, Object> configurationClaims = configurationMetadata.toJSONObject();
         final Map<String, Object> subordinateClaims = subordinateMetadata.toJSONObject();
         for (final String configurationClaim : configurationClaims.keySet()) {
             if (!subordinateClaims.containsKey(configurationClaim)) {
+                log.trace("Including metadata claim {} from the entity configuration", configurationClaim);
                 subordinateClaims.put(configurationClaim, configurationClaims.get(configurationClaim));
+            } else {
+                log.trace("Keeping metadata claim {} from the subordinate configuration", configurationClaim);
             }
         }
         try {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
index 4bfc9cb9..4649dbbf 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
@@ -43,7 +43,8 @@ import net.shibboleth.shared.resolver.CriteriaSet;
 
 /**
  * Default strategy for fetching trust chains for an entity specified in the criteria set. Caches for entity
- * configurations are subordinate statements are exploited for actual fetching of the entity statements.
+ * configurations and subordinate statements are exploited for actual fetching of the entity statements. The
+ * entity configuration may also be delivered via {@link SubjectEntityStatementCriterion} in the criteria set.
  */
 @ThreadSafeAfterInit
 public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableInitializableComponent
@@ -133,22 +134,33 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
         final PreSelectedTrustChainCriterion preSelectedCriterion = criteria.get(PreSelectedTrustChainCriterion.class);
         final List<String> preSelectedChain =
                 preSelectedCriterion == null ? CollectionSupport.emptyList() : preSelectedCriterion.getValue();
-        try {
-            final List<EntityStatement> entityConfigurations = entityConfigurationCache.get(criteria);
-            if (entityConfigurations.isEmpty()) {
+        final EntityStatement entityConfiguration;
+        final SubjectEntityStatementCriterion subjectStatementCriterion =
+                criteria.get(SubjectEntityStatementCriterion.class);
+        if (subjectStatementCriterion == null) {
+            try {
+                final List<EntityStatement> entityConfigurations = entityConfigurationCache.get(criteria);
+                if (entityConfigurations.isEmpty()) {
+                    return null;
+                }
+                entityConfiguration = entityConfigurations.get(0);
+            } catch (final MetadataCacheException e) {
+                log.error("Could not fetch entity configuration for the trust chain", e);
                 return null;
             }
-            final EntityStatement entityConfiguration = entityConfigurationCache.get(criteria).get(0);
-            assert entityConfiguration != null;
-            final List<List<EntityStatement>> rawChains = populateChain(
-                    CollectionSupport.listOf(CollectionSupport.listOf(entityConfiguration)), preSelectedChain);
-            final List<List<EntityStatement>> trustChains =
-                    stripIntermediateConfigurations(entityConfiguration, rawChains);
-            return Stream.concat(trustChains.stream(), resolveLocallyTrustedTrustChains(trustChains).stream()).toList();
-        } catch (final MetadataCacheException e) {
-            log.error("Could not fetch entity configuration for the trust chain", e);
+        } else {
+            entityConfiguration = subjectStatementCriterion.getValue();
         }
-        return null;
+
+        if (entityConfiguration == null) {
+            return null;
+        }
+
+        final List<List<EntityStatement>> rawChains = populateChain(
+                CollectionSupport.listOf(CollectionSupport.listOf(entityConfiguration)), preSelectedChain);
+        final List<List<EntityStatement>> trustChains =
+                stripIntermediateConfigurations(entityConfiguration, rawChains);
+        return Stream.concat(trustChains.stream(), resolveLocallyTrustedTrustChains(trustChains).stream()).toList();
     }
 
     /**
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/policy/FederationMetadataPolicyDeserializer.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/policy/FederationMetadataPolicyDeserializer.java
index c557b385..4db5a656 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/policy/FederationMetadataPolicyDeserializer.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/policy/FederationMetadataPolicyDeserializer.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy;
 
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -34,6 +35,8 @@ import com.fasterxml.jackson.databind.type.MapType;
 import com.fasterxml.jackson.databind.type.TypeFactory;
 
 import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
@@ -46,6 +49,28 @@ public class FederationMetadataPolicyDeserializer extends JsonDeserializer<Metad
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(FederationMetadataPolicyDeserializer.class);
 
+    /** List of claim names who are transformed from a space-separated String into a List. */
+    @Nonnull private final List<String> arraysAsSpaceSeparatedList;
+
+    /**
+     * Constructor.
+     */
+    public FederationMetadataPolicyDeserializer() {
+        this("scope");
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param string comma-separated list of claim names who are transformed from a space-separated String into a List.
+     */
+    public FederationMetadataPolicyDeserializer(
+            @Nonnull @ParameterName(name = "arraysAsSpaceSeparatedList") final String string) {
+        final List<String> list = Arrays.asList(Constraint.isNotEmpty(string, "The string cannot be empty").split(","));
+        assert list != null;
+        arraysAsSpaceSeparatedList = list;
+    }
+
     /** {@inheritDoc} */
     @Override @Nonnull
     public MetadataPolicy deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext)
@@ -58,32 +83,47 @@ public class FederationMetadataPolicyDeserializer extends JsonDeserializer<Metad
                 TypeFactory.defaultInstance().constructMapType(Map.class, stringType, objectType);
 
         final Map<String,Object> map = deserializationContext.readValue(jsonParser, objectMapType);
+        final String claim = jsonParser.getParsingContext().getCurrentName();
+        if (claim == null) {
+            throw new IOException("Could not find the parent claim name for the metadata policy");
+        }
+        log.debug("Processing claim {}, value {}", claim, map);
 
         if (map != null) {
             log.debug("Processing map object {}", map);
             for (final String key : map.keySet().stream().filter(Objects::nonNull).toList()) {
                 switch (key) {
                     case "value":
-                        policy.setValue(map.get("value") != null ? map.get("value") : Optional.empty());
+                        final Object value = FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
+                                arraysAsSpaceSeparatedList, claim, map.get("value"));
+                        policy.setValue(value != null ? value : Optional.empty());
                         break;
                     case "add":
-                        policy.setAdd(map.get("add"));
+                        policy.setAdd(FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
+                                arraysAsSpaceSeparatedList, claim, map.get("add")));
                         break;
                     case "default":
-                        policy.setDefaultValue(map.get("default"));
+                        policy.setDefaultValue(FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
+                                arraysAsSpaceSeparatedList, claim, map.get("default")));
                         break;
                     case "essential":
                         policy.setEssential(map.get("essential") != null ?
                                 Boolean.valueOf(String.valueOf(map.get("essential"))).booleanValue() : false);
                         break;
                     case "one_of":
-                        policy.setOneOfValues(transformObjectIntoList("one_of", map));
+                        policy.setOneOfValues(transformObjectIntoList("one_of",
+                                FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
+                                        arraysAsSpaceSeparatedList, claim, map.get("one_of"))));
                         break;
                     case "subset_of":
-                        policy.setSubsetOfValues(transformObjectIntoList("subset_of", map));
+                        policy.setSubsetOfValues(transformObjectIntoList("subset_of",
+                                FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
+                                        arraysAsSpaceSeparatedList, claim, map.get("subset_of"))));
                         break;
                     case "superset_of":
-                        policy.setSupersetOfValues(transformObjectIntoList("superset_of", map));
+                        policy.setSupersetOfValues(transformObjectIntoList("superset_of",
+                                FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
+                                        arraysAsSpaceSeparatedList, claim, map.get("superset_of"))));
                         break;
                     case "regexp":
                         policy.setRegexp(map.get("regexp") == null ? null : "" + map.get("regexp"));
@@ -100,20 +140,19 @@ public class FederationMetadataPolicyDeserializer extends JsonDeserializer<Metad
     }
 
     /**
-     * Transforms the value from the given map into a list of objects.
+     * Transforms the given value into a list of objects.
      * 
      * @param id the key for the map of objects
-     * @param objects the map of objects
+     * @param object the object value
      * @return the value for the key as list or null
      * @throws IOException if a non-null value could not be transformed into a list
      */
     @Nullable private List<Object> transformObjectIntoList(@Nonnull final String id,
-            @Nonnull final Map<String,Object> objects) throws IOException {
-        final Object object = objects.get(id);
+            @Nullable final Object object) throws IOException {
         if (object instanceof List<?> list) {
             return list.stream().filter(Object.class::isInstance).map(Object.class::cast).toList();
         } else if (object != null) {
-            throw new IOException("Cannot transform '" + id + "' value into a list");
+            throw new IOException("The value '" + object + "' for '" + id + "' is not a list");
         }
         return null;
     }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/policy/FederationMetadataPolicyHelper.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/policy/FederationMetadataPolicyHelper.java
new file mode 100644
index 00000000..287e203d
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/policy/FederationMetadataPolicyHelper.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.policy;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Static utility method related to federation metadata policies.
+ */
+public class FederationMetadataPolicyHelper {
+
+    /**
+     * Transforms a list into a space-separated string. Operation is done for non-null values if the given claim is
+     * included in the given list of claims that are to be transformed.
+     * 
+     * @param arraysAsSpaceSeparatedList the list of claim names to be transformed
+     * @param claim the candidate claim
+     * @param value the candidate claim value
+     * @return the claim value transformed into a space-separated string if it met the requirements, or initial value
+     *    if not
+     * @throws ConstraintViolationException if the non-null value to be transformed was not a list
+     */
+    @Nullable
+    public static Object transformListIntoSpaceSeparatedString(@Nonnull final List<String> arraysAsSpaceSeparatedList,
+            @Nonnull final String claim, @Nullable final Object value) throws ConstraintViolationException {
+        if (arraysAsSpaceSeparatedList.contains(claim)) {
+            if (value instanceof List<?> list) {
+                return list.stream()
+                        .map(item -> String.valueOf(item))
+                        .collect(Collectors.joining(" "));
+            } else if (value != null) {
+                throw new ConstraintViolationException(
+                        "Unexpected value for claim " + claim + ": the value is not a List");
+            }
+        }
+        return value;
+    }
+
+    /**
+     * Transforms a space-separated string into a list. Operation is done for non-null values if the given claim is
+     * included in the given list of claims that are to be transformed.
+     * 
+     * @param arraysAsSpaceSeparatedList the list of claim names to be transformed
+     * @param claim the candidate claim
+     * @param value the candidate claim value
+     * @return the claim value transformed into a list if it met the requirements, or initial value
+     * @throws ConstraintViolationException if the non-null value to be transformed was not a string
+     */
+    @Nullable
+    public static Object transformSpaceSeparatedStringIntoList(@Nonnull final List<String> arraysAsSpaceSeparatedList,
+            @Nonnull final String claim, @Nullable final Object value) throws ConstraintViolationException {
+        if (arraysAsSpaceSeparatedList.contains(claim)) {
+            if (value instanceof String string) {
+                return List.of(string.split(" "));
+            } else if (value != null) {
+                throw new ConstraintViolationException(
+                        "Unexpected value for claim " + claim + ": the value is not a String");
+            }
+        }
+        return value;
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractTrustChainResolutionAction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractTrustChainResolutionAction.java
new file mode 100644
index 00000000..59e75142
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractTrustChainResolutionAction.java
@@ -0,0 +1,297 @@
+/*
+ * 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.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityType;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultClientMetadataFromTrustChainLookupStrategy;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.FederationMetadataPolicyHelper;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+
+/**
+ * Base action for actions initializing {@link RelyingPartyTrustChainContext} and performing metadata and metadata
+ * policy related operations.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * 
+ * @since 4.3.0
+ */
+public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(AbstractTrustChainResolutionAction.class);
+
+    /** Strategy used to create the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextCreationStrategy;
+
+    /** Strategy used to get combined OIDC client metadata from trust chain. */
+    @Nonnull private Function<List<EntityStatement>,OIDCClientMetadata> metadataLookupStrategy;
+
+    /** Strategy used to merge metadata policies in trust chain for specific entity type. */
+    @NonnullAfterInit private BiFunction<List<EntityStatement>,String,Map<String, MetadataPolicy>>
+        metadataPolicyMergingStrategy;
+
+    /** Enforcer function for applying metadata policy for an item. */
+    @NonnullAfterInit private BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> metadataPolicyEnforcer;
+
+    /** List of claim names who are transformed from a space-separated String into a List. */
+    @Nonnull private List<String> arraysAsSpaceSeparatedList;
+
+    /**
+     * Constructor.
+     */
+    public AbstractTrustChainResolutionAction() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tccs =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class, true).compose(
+                        new InboundMessageContextLookup());
+        assert tccs != null;
+        trustChainContextCreationStrategy = tccs;
+        metadataLookupStrategy = new DefaultClientMetadataFromTrustChainLookupStrategy();
+        arraysAsSpaceSeparatedList = CollectionSupport.listOf("scope");
+    }
+
+    /**
+     * Set the strategy used to create the trust chain context.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setTrustChainContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+        checkSetterPreconditions();
+        trustChainContextCreationStrategy =
+                Constraint.isNotNull(strategy, "TrustChainContextCreationStrategy cannot be null");
+    }
+
+    /**
+     * Get the strategy used to create the trust chain context.
+     * 
+     * @return creation strategy
+     */
+    @Nonnull
+    public Function<ProfileRequestContext, RelyingPartyTrustChainContext> getTrustChainContextCreationStrategy() {
+        checkComponentActive();
+        return trustChainContextCreationStrategy;
+    }
+
+    /**
+     * Set the strategy used to get combined OIDC client metadata from trust chain.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setMetadataLookupStrategy(@Nonnull final Function<List<EntityStatement>,OIDCClientMetadata> strategy) {
+        checkSetterPreconditions();
+        metadataLookupStrategy =
+                Constraint.isNotNull(strategy, "MetadataLookupStrategy cannot be null");
+    }
+
+    /**
+     * Get the strategy used to get combined OIDC client metadata from trust chain.
+     * 
+     * @return lookup strategy
+     */
+    @Nonnull public Function<List<EntityStatement>,OIDCClientMetadata> getMetadataLookupStrategy() {
+        checkComponentActive();
+        return metadataLookupStrategy;
+    }
+
+    /**
+     * Set the strategy used to merge metadata policies in trust chain for specific entity type.
+     * 
+     * @param strategy merging strategy
+     */
+    public void setMetadataPolicyMergingStrategy(@Nonnull final
+            BiFunction<List<EntityStatement>,String,Map<String, MetadataPolicy>> strategy) {
+        checkSetterPreconditions();
+        metadataPolicyMergingStrategy =
+                Constraint.isNotNull(strategy, "MetadataPolicyMergingStrategy cannot be null");
+    }
+
+    /**
+     * Get the strategy used to merge metadata policies in trust chain for specific entity type.
+     * 
+     * @return merging strategy
+     */
+    @Nonnull
+    public BiFunction<List<EntityStatement>,String,Map<String, MetadataPolicy>> getMetadataPolicyMergingStrategy() {
+        checkComponentActive();
+        assert metadataPolicyMergingStrategy != null;
+        return metadataPolicyMergingStrategy;
+    }
+
+    /**
+     * Set the enforcer function for applying metadata policy for an item.
+     * 
+     * @param enforcer policy enforcer
+     */
+    public void setMetadataPolicyEnforcer(
+            @Nonnull final BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> enforcer) {
+        checkSetterPreconditions();
+        metadataPolicyEnforcer = Constraint.isNotNull(enforcer, "Metadata policy enforcer cannot be null");
+    }
+
+    /**
+     * Get the enforcer function for applying metadata policy for an item.
+     * 
+     * @return policy enforcer
+     */
+    @Nonnull public BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> getMetadataPolicyEnforcer() {
+        checkComponentActive();
+        assert metadataPolicyEnforcer != null;
+        return metadataPolicyEnforcer;
+    }
+
+    /**
+     * Set the  list of claim names who are transformed from a space-separated String into a List.
+     * 
+     * @param list list of claim names
+     */
+    public void setArraysAsSpaceSeparatedList(@Nonnull final List<String> list) {
+        checkSetterPreconditions();
+        arraysAsSpaceSeparatedList = Constraint.isNotNull(list, "ArraysAsSpaceSeparatedList cannot be null");
+    }
+
+    /**
+     * Get the  list of claim names who are transformed from a space-separated String into a List.
+     * 
+     * @return list of claim names
+     */
+    @Nonnull public List<String> getArraysAsSpaceSeparatedList() {
+        checkComponentActive();
+        return arraysAsSpaceSeparatedList;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (metadataPolicyMergingStrategy == null) {
+            throw new ComponentInitializationException("MetadataPolicyMergingStrategy cannot be null");
+        }
+        if (metadataPolicyEnforcer == null) {
+            throw new ComponentInitializationException("MetadataPolicyEnforcer cannot be null");
+        }
+    }
+
+    /**
+     * Populates the given policy compliant trust chains with the given trust chain if its metadata is policy compliant.
+     * 
+     * @param chain the trust chain to be evaluated
+     * @param policyCompliantChains the list of policy-compliant trust chains to be populated
+     * @return error event ID if metadata policy merging or enforcement failed, null otherwise
+     */
+    @Nullable protected String populatePolicyComplaintChains(@Nonnull final List<EntityStatement> chain,
+            @Nonnull List<Pair<List<EntityStatement>, OIDCClientInformation>> policyCompliantChains) {
+        final Map<String, MetadataPolicy> mergedPolicies;
+        try {
+            mergedPolicies = getMetadataPolicyMergingStrategy().apply(chain,
+                    EntityType.OPENID_RELYING_PARTY.getValue());
+            log.debug("{} Merged policy for chain {}", getLogPrefix(), mergedPolicies);
+        } catch (final ConstraintViolationException e) {
+            log.warn("{} Could not merge metadata policies", getLogPrefix(), e);
+            return OidFederationEventIds.INVALID_METADATA_POLICY;
+        }
+        assert chain != null;
+        final OIDCClientMetadata metadata = getMetadataLookupStrategy().apply(chain);
+        log.trace("{} Metadata resolved via lookup strategy: {}", getLogPrefix(), metadata);
+        if (metadata != null) {
+            final OIDCClientInformation clientInformation = new OIDCClientInformation(
+                    new ClientID(chain.get(0).getEntityID().getValue()), metadata);
+            final JSONObject requestMetadata = clientInformation.toJSONObject();
+            for (final String claim : mergedPolicies.keySet()) {
+                assert claim != null;
+                final MetadataPolicy policy = mergedPolicies.get(claim);
+                try {
+                    final Object enforcedValue = enforceValue(claim, requestMetadata.get(claim), policy);
+                    requestMetadata.put(claim, enforcedValue);
+                } catch (final ConstraintViolationException e) {
+                    log.warn("{} The requested metadata is not compliant with the policy", getLogPrefix());
+                    return OidFederationEventIds.INVALID_METADATA_AGAINST_POLICY;
+                }
+            }
+
+            log.debug("{} The requested metadata is compliant with the policy", getLogPrefix());
+            try {
+                policyCompliantChains.add(new Pair<>(chain, OIDCClientInformation.parse(requestMetadata)));
+                log.debug("{} Policy-enforced metadata {}", getLogPrefix(), requestMetadata.toJSONString());
+                return null;
+            } catch (final ParseException e) {
+                log.error("{} Could not parse the metadata object", getLogPrefix(), e);
+            }
+        }
+        return EventIds.INVALID_MSG_CTX;
+    }
+
+    /**
+     * Enforces the given value with the given metadata policy.
+     * 
+     * @param claim name of the claim to be enforced
+     * @param value value of the claim
+     * @param policy the metadata policy to be used for enforcing
+     * @return the enforced value
+     * @throws ConstraintViolationException if the operation was not successful
+     */
+    @Nullable protected Object enforceValue(@Nonnull final String claim, @Nullable final Object value,
+            @Nullable final MetadataPolicy policy) throws ConstraintViolationException {
+        log.debug("{} Claim {} set in policy included in the request: {}", getLogPrefix(), claim,
+                value == null);
+        final Object enforcerInput = FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
+                arraysAsSpaceSeparatedList, claim, value);
+
+        final Pair<Object,Boolean> mergeResult = getMetadataPolicyEnforcer().apply(enforcerInput, policy);
+        final Boolean enforcerResult = mergeResult != null ? mergeResult.getSecond() : null;
+        if (enforcerResult == null || !enforcerResult.booleanValue()) {
+            throw new ConstraintViolationException("Metadata claim " + claim + " is not compliant with the policy");
+        }
+        log.trace("{} Validation result is OK for claim {}", getLogPrefix(), claim);
+        return Optional.ofNullable(mergeResult)
+                .map(pair -> pair.getFirst())
+                .map(result -> FederationMetadataPolicyHelper.transformListIntoSpaceSeparatedString(
+                        arraysAsSpaceSeparatedList,claim, result))
+                .orElse(null);
+    }
+
+}
\ 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/ResolveTrustChains.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java
index ae3bd565..66fbb71e 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java
@@ -19,46 +19,39 @@ import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
-import java.util.function.BiFunction;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
-import com.nimbusds.oauth2.sdk.ParseException;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityType;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
-import net.minidev.json.JSONObject;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultClientMetadataFromTrustChainLookupStrategy;
+import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityStatementCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultPreSelectedTrustChainIDsLookupStrategy;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainIDsLookupStrategy;
-import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
-import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.collection.Pair;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.resolver.CriteriaSet;
 
 import org.opensaml.messaging.context.MessageContext;
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 
 /**
@@ -70,7 +63,7 @@ import org.opensaml.profile.action.ActionSupport;
  * 
  * @since 4.3.0
  */
-public class ResolveTrustChains extends AbstractProfileAction {
+public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
 
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(ResolveTrustChains.class);
@@ -81,25 +74,18 @@ public class ResolveTrustChains extends AbstractProfileAction {
     /** Strategy used to obtain the client id value for authorize/token request. */
     @NonnullAfterInit private Function<MessageContext, ClientID> clientIDLookupStrategy;
 
-    /** Strategy used to create the trust chain context. */
-    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextCreationStrategy;
-
-    /** Strategy used to get combined OIDC client metadata from trust chain. */
-    @Nonnull private Function<List<EntityStatement>,OIDCClientMetadata> metadataLookupStrategy;
-
-    /** Strategy used to merge metadata policies in trust chain for specific entity type. */
-    @NonnullAfterInit private BiFunction<List<EntityStatement>,String,Map<String, MetadataPolicy>>
-        metadataPolicyMergingStrategy;
-
-    /** Enforcer function for applying metadata policy for an item. */
-    @NonnullAfterInit private BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> metadataPolicyEnforcer;
-
     /** Strategy used to fetch the pre-selected trust chain entity IDs. */
     @Nonnull private Function<ProfileRequestContext, List<String>> preSelectedTrustChainIdsLookupStrategy;
 
     /** Strategy used to get entity IDs from a trust chain. */
     @Nonnull private Function<List<EntityStatement>, List<String>> trustChainIDsLookupStrategy;
 
+    /** Strategy used to fetch entity configuration delivered to the trust chain cache. */
+    @Nonnull private Function<ProfileRequestContext, EntityStatement> entityConfigurationLookupStrategy;
+
+    /** Condition to require entity configuration via {@link this#entityConfigurationLookupStrategy}. */
+    @Nonnull private Predicate<ProfileRequestContext> requireEntityConfigurationCondition;
+
     /** OAuth2 client id. */
     @NonnullBeforeExec private String clientId;
 
@@ -107,14 +93,11 @@ public class ResolveTrustChains extends AbstractProfileAction {
      * Constructor.
      */
     public ResolveTrustChains() {
-        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tccs =
-                new ChildContextLookup<>(RelyingPartyTrustChainContext.class, true).compose(
-                        new InboundMessageContextLookup());
-        assert tccs != null;
-        trustChainContextCreationStrategy = tccs;
-        metadataLookupStrategy = new DefaultClientMetadataFromTrustChainLookupStrategy();
+        super();
         preSelectedTrustChainIdsLookupStrategy = new DefaultPreSelectedTrustChainIDsLookupStrategy();
         trustChainIDsLookupStrategy = new DefaultTrustChainIDsLookupStrategy();
+        entityConfigurationLookupStrategy = FunctionSupport.constant(null);
+        requireEntityConfigurationCondition = PredicateSupport.alwaysFalse();
     }
 
     /**
@@ -139,59 +122,47 @@ public class ResolveTrustChains extends AbstractProfileAction {
     }
 
     /**
-     * Set the strategy used to get combined OIDC client metadata from trust chain.
+     * Set the strategy used to fetch the pre-selected trust chain entity IDs.
      * 
      * @param strategy lookup strategy
      */
-    public void setMetadataLookupStrategy(@Nonnull final Function<List<EntityStatement>,OIDCClientMetadata> strategy) {
+    public void setPreSelectedTrustChainIdsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
         checkSetterPreconditions();
-        metadataLookupStrategy =
-                Constraint.isNotNull(strategy, "MetadataLookupStrategy cannot be null");
+        preSelectedTrustChainIdsLookupStrategy = Constraint.isNotNull(strategy,
+                "PreSelectedTrustChainIdsLookupStrategy cannot be null");
     }
 
     /**
-     * Set the strategy used to merge metadata policies in trust chain for specific entity type.
+     * Set the strategy used to get entity IDs from a trust chain.
      * 
-     * @param strategy merging strategy
+     * @param strategy lookup strategy
      */
-    public void setMetadataPolicyMergingStrategy(@Nonnull final
-            BiFunction<List<EntityStatement>,String,Map<String, MetadataPolicy>> strategy) {
+    public void setTrustChainIDsLookupStrategy(@Nonnull final Function<List<EntityStatement>, List<String>> strategy) {
         checkSetterPreconditions();
-        metadataPolicyMergingStrategy =
-                Constraint.isNotNull(strategy, "MetadataPolicyMergingStrategy cannot be null");
+        trustChainIDsLookupStrategy = Constraint.isNotNull(strategy, "TrustChainIDsLookupStrategy cannot be null");
     }
 
     /**
-     * Set the enforcer function for applying metadata policy for an item.
-     * 
-     * @param enforcer policy enforcer
-     */
-    public void setMetadataPolicyEnforcer(
-            @Nonnull final BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> enforcer) {
-        checkSetterPreconditions();
-        metadataPolicyEnforcer = Constraint.isNotNull(enforcer, "Metadata policy enforcer cannot be null");
-    }
-
-    /**
-     * Set the strategy used to fetch the pre-selected trust chain entity IDs.
+     * Set the strategy used to fetch entity configuration delivered to the trust chain cache.
      * 
      * @param strategy lookup strategy
      */
-    public void setPreSelectedTrustChainIdsLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+    public void setEntityConfigurationLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, EntityStatement> strategy) {
         checkSetterPreconditions();
-        preSelectedTrustChainIdsLookupStrategy = Constraint.isNotNull(strategy,
-                "PreSelectedTrustChainIdsLookupStrategy cannot be null");
+        entityConfigurationLookupStrategy = Constraint.isNotNull(strategy,
+                "EntityConfigurationLookupStrategy cannot be null");
     }
 
     /**
-     * Set the strategy used to get entity IDs from a trust chain.
-     * 
-     * @param strategy lookup strategy
+     * Set the condition to require entity configuration via {@link this#entityConfigurationLookupStrategy}.
+     * @param predicate condition
      */
-    public void setTrustChainIDsLookupStrategy(@Nonnull final Function<List<EntityStatement>, List<String>> strategy) {
+    public void setRequireEntityConfigurationCondition(@Nonnull final Predicate<ProfileRequestContext> predicate) {
         checkSetterPreconditions();
-        trustChainIDsLookupStrategy = Constraint.isNotNull(strategy, "TrustChainIDsLookupStrategy cannot be null");
+        requireEntityConfigurationCondition =
+                Constraint.isNotNull(predicate, "RequireEntityConfigurationCondition cannot be null");
     }
 
     /** {@inheritDoc} */
@@ -205,9 +176,6 @@ public class ResolveTrustChains extends AbstractProfileAction {
         if (clientIDLookupStrategy == null) {
             throw new ComponentInitializationException("ClientIDLookupStrategy cannot be null");
         }
-        if (metadataPolicyEnforcer == null) {
-            throw new ComponentInitializationException("MetadataPolicyEnforcer cannot be null");
-        }
     }
 
     /** {@inheritDoc} */
@@ -239,6 +207,15 @@ public class ResolveTrustChains extends AbstractProfileAction {
         log.debug("{} Resolving trust chain for {}", getLogPrefix(), clientId);
         assert clientId != null;
         final CriteriaSet criteriaSet = new CriteriaSet(new SubjectEntityIDCriterion(clientId));
+        final EntityStatement entityConfiguration = entityConfigurationLookupStrategy.apply(profileRequestContext);
+        if (entityConfiguration != null) {
+            log.debug("{} Entity configuration resolved and included to the criteria set", getLogPrefix());
+            criteriaSet.add(new SubjectEntityStatementCriterion(entityConfiguration));
+        } else if (requireEntityConfigurationCondition.test(profileRequestContext)) {
+            log.error("{} Mandatory entity configuration could not be resolved", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+            return;
+        }
         final List<List<List<EntityStatement>>> cacheResult;
         try {
             cacheResult = trustChainCache.get(criteriaSet);
@@ -255,63 +232,18 @@ public class ResolveTrustChains extends AbstractProfileAction {
                 .orElse(CollectionSupport.emptyList());
 
         final RelyingPartyTrustChainContext trustChainContext =
-                trustChainContextCreationStrategy.apply(profileRequestContext);
+                getTrustChainContextCreationStrategy().apply(profileRequestContext);
         trustChainContext.setResolvedTrustChains(cacheResult.get(0));
         final List<Pair<List<EntityStatement>, OIDCClientInformation>> policyCompliantChains = new ArrayList<>();
 
         String errorEventId = null;
         for (final List<EntityStatement> chain : cacheResult.get(0)) {
+            assert chain != null;
             if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
                 log.debug("{} Ignored resolved trust chain that doesn't match with preselected chain", getLogPrefix());
                 continue;
             }
-            final Map<String, MetadataPolicy> mergedPolicies;
-            try {
-                mergedPolicies = metadataPolicyMergingStrategy.apply(chain,
-                        EntityType.OPENID_RELYING_PARTY.getValue());
-                log.debug("{} Merged policy for chain {}", getLogPrefix(), mergedPolicies);
-            } catch (final ConstraintViolationException e) {
-                log.warn("{} Could not merge metadata policies", getLogPrefix(), e);
-                errorEventId = OidFederationEventIds.INVALID_METADATA_POLICY;
-                continue;
-            }
-            assert chain != null;
-            final OIDCClientMetadata metadata = metadataLookupStrategy.apply(chain);
-            if (metadata != null) {
-                final OIDCClientInformation clientInformation = new OIDCClientInformation(
-                        new ClientID(chain.get(0).getEntityID().getValue()), metadata);
-                final JSONObject requestMetadata = clientInformation.toJSONObject();
-                for (final String claim : mergedPolicies.keySet()) {
-                    final MetadataPolicy policy = mergedPolicies.get(claim);
-                    final Object value = requestMetadata.get(claim);
-                    log.debug("{} Claim {} set in policy included in the request: {}", getLogPrefix(), claim,
-                            value == null);
-                    final Pair<Object,Boolean> mergeResult = metadataPolicyEnforcer.apply(value, policy);
-                    final Boolean enforcerResult = mergeResult != null ? mergeResult.getSecond() : null;
-                    if (enforcerResult == null || !enforcerResult.booleanValue()) {
-                        log.warn("{} Metadata claim {} is not compliant with the policy", getLogPrefix(), claim);
-                        errorEventId = OidFederationEventIds.INVALID_METADATA_AGAINST_POLICY;
-                    } else {
-                        log.trace("{} Validation result is OK for claim {}", getLogPrefix(), claim);
-                        final Object enforcedValue = mergeResult != null ? mergeResult.getFirst() : null;
-                        requestMetadata.put(claim, enforcedValue);
-                    }
-                }
-
-                if (errorEventId != null) {
-                    log.warn("{} The requested metadata is not compliant with the policy", getLogPrefix());
-                } else {
-                    log.debug("{} The requested metadata is compliant with the policy", getLogPrefix());
-                    try {
-                        policyCompliantChains.add(new Pair<>(chain, OIDCClientInformation.parse(requestMetadata)));
-                    } catch (final ParseException e) {
-                        log.error("{} Could not parse the metadata object", getLogPrefix(), e);
-                    }
-                }
-                log.debug("{} Policy-enforced metadata {}", getLogPrefix(), requestMetadata.toJSONString());
-            } else {
-                log.error("{} No client information found from the entity statement", getLogPrefix());
-            }
+            errorEventId = populatePolicyComplaintChains(chain, policyCompliantChains);
         }
 
         if (policyCompliantChains.isEmpty()) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java
index 22bb359c..9cfd6f4b 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java
@@ -16,8 +16,6 @@ package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
-import java.util.function.BiFunction;
 import java.util.function.BiPredicate;
 import java.util.function.Function;
 
@@ -25,20 +23,11 @@ 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.oauth2.sdk.ParseException;
-import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityType;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
-import net.minidev.json.JSONObject;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultClientMetadataFromTrustChainLookupStrategy;
-import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.collection.CollectionSupport;
@@ -47,7 +36,6 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 
 /**
@@ -59,7 +47,7 @@ import org.opensaml.profile.action.ActionSupport;
  * 
  * @since 4.3.0
  */
-public class ValidateProvidedTrustChain extends AbstractProfileAction {
+public class ValidateProvidedTrustChain extends AbstractTrustChainResolutionAction {
 
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(ValidateProvidedTrustChain.class);
@@ -68,48 +56,12 @@ public class ValidateProvidedTrustChain extends AbstractProfileAction {
     @NonnullAfterInit
     private BiPredicate<ProfileRequestContext, List<EntityStatement>> providedTrustChainValidationStrategy;
 
-    /** Strategy used to create the trust chain context. */
-    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextCreationStrategy;
-
     /** Strategy used to locate the provided trust chain. */
     @NonnullAfterInit private Function<ProfileRequestContext, List<EntityStatement>> providedTrustChainLookupStrategy;
 
-    /** Strategy used to get combined OIDC client metadata from trust chain. */
-    @Nonnull private Function<List<EntityStatement>,OIDCClientMetadata> metadataLookupStrategy;
-
-    /** Strategy used to merge metadata policies in trust chain for specific entity type. */
-    @NonnullAfterInit private BiFunction<List<EntityStatement>,String,Map<String, MetadataPolicy>>
-        metadataPolicyMergingStrategy;
-
-    /** Enforcer function for applying metadata policy for an item. */
-    @NonnullAfterInit private BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> metadataPolicyEnforcer;
-
     /** Trust chain to operate on. */
     @NonnullBeforeExec private List<EntityStatement> trustChain;
 
-    /**
-     * Constructor.
-     */
-    public ValidateProvidedTrustChain() {
-        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tccs =
-                new ChildContextLookup<>(RelyingPartyTrustChainContext.class, true).compose(
-                        new InboundMessageContextLookup());
-        assert tccs != null;
-        trustChainContextCreationStrategy = tccs;
-        metadataLookupStrategy = new DefaultClientMetadataFromTrustChainLookupStrategy();
-    }
-
-    /**
-     * Set the strategy used to get combined OIDC client metadata from trust chain.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setMetadataLookupStrategy(@Nonnull final Function<List<EntityStatement>,OIDCClientMetadata> strategy) {
-        checkSetterPreconditions();
-        metadataLookupStrategy =
-                Constraint.isNotNull(strategy, "MetadataLookupStrategy cannot be null");
-    }
-
     /**
      * Set the strategy used to locate the provided trust chain.
      * 
@@ -120,28 +72,6 @@ public class ValidateProvidedTrustChain extends AbstractProfileAction {
         providedTrustChainLookupStrategy =
                 Constraint.isNotNull(strategy, "ProvidedTrustChainLookupStrategy cannot be null");
     }
-    /**
-     * Set the strategy used to merge metadata policies in trust chain for specific entity type.
-     * 
-     * @param strategy merging strategy
-     */
-    public void setMetadataPolicyMergingStrategy(@Nonnull final
-            BiFunction<List<EntityStatement>,String,Map<String, MetadataPolicy>> strategy) {
-        checkSetterPreconditions();
-        metadataPolicyMergingStrategy =
-                Constraint.isNotNull(strategy, "MetadataPolicyMergingStrategy cannot be null");
-    }
-
-    /**
-     * Set the enforcer function for applying metadata policy for an item.
-     * 
-     * @param enforcer policy enforcer
-     */
-    public void setMetadataPolicyEnforcer(
-            @Nonnull final BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> enforcer) {
-        checkSetterPreconditions();
-        metadataPolicyEnforcer = Constraint.isNotNull(enforcer, "Metadata policy enforcer cannot be null");
-    }
 
     /**
      * Set the strategy used to validate provided trust chain.
@@ -166,9 +96,6 @@ public class ValidateProvidedTrustChain extends AbstractProfileAction {
         if (providedTrustChainValidationStrategy == null) {
             throw new ComponentInitializationException("ProvidedTrustChainValidationStrategy cannot be null");
         }
-        if (metadataPolicyEnforcer == null) {
-            throw new ComponentInitializationException("MetadataPolicyEnforcer cannot be null");
-        }
     }
 
     /** {@inheritDoc} */
@@ -196,52 +123,15 @@ public class ValidateProvidedTrustChain extends AbstractProfileAction {
         }
 
         final RelyingPartyTrustChainContext trustChainContext =
-                trustChainContextCreationStrategy.apply(profileRequestContext);
+                getTrustChainContextCreationStrategy().apply(profileRequestContext);
         assert trustChain != null;
         trustChainContext.setResolvedTrustChains(CollectionSupport.listOf(trustChain));
         final List<Pair<List<EntityStatement>, OIDCClientInformation>> policyCompliantChains = new ArrayList<>();
 
-        final Map<String, MetadataPolicy> mergedPolicies =
-                metadataPolicyMergingStrategy.apply(trustChain, EntityType.OPENID_RELYING_PARTY.getValue());
-        log.debug("{} Merged policy for chain {}", getLogPrefix(), mergedPolicies);
-        final OIDCClientMetadata metadata = metadataLookupStrategy.apply(trustChain);
-        if (metadata == null) {
-            log.error("{} Could not extract metadata", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
-            return;
-        }
-        final OIDCClientInformation clientInformation = new OIDCClientInformation(
-                new ClientID(trustChain.get(0).getEntityID().getValue()), metadata);
-        final JSONObject requestMetadata = clientInformation.toJSONObject();
-        boolean compliant = true;
-        for (final String claim : mergedPolicies.keySet()) {
-            final MetadataPolicy policy = mergedPolicies.get(claim);
-            final Object value = requestMetadata.get(claim);
-            log.debug("{} Claim {} set in policy included in the request: {}", getLogPrefix(), claim,
-                    value == null);
-            final Pair<Object,Boolean> mergeResult = metadataPolicyEnforcer.apply(value, policy);
-            final Boolean enforcerResult = mergeResult != null ? mergeResult.getSecond() : null;
-            if (enforcerResult == null || !enforcerResult.booleanValue()) {
-                log.warn("{} Metadata claim {} is not compliant with the policy", getLogPrefix(), claim);
-                compliant = false;
-            } else {
-                log.trace("{} Validation result is OK for claim {}", getLogPrefix(), claim);
-                final Object enforcedValue = mergeResult != null ? mergeResult.getFirst() : null;
-                requestMetadata.put(claim, enforcedValue);
-                log.debug("{} The requested metadata is compliant with the policy", getLogPrefix());
-            }
-        }
-        if (!compliant) {
+        final String errorEventId = populatePolicyComplaintChains(trustChain, policyCompliantChains);
+        if (errorEventId != null) {
             log.error("{} The request metadata is not compliant with the merged policy", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
-            return;
-        }
-
-        try {
-            policyCompliantChains.add(new Pair<>(trustChain, OIDCClientInformation.parse(requestMetadata)));
-        } catch (final ParseException e) {
-            log.error("{} Could not parse the metadata object", getLogPrefix(), e);
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            ActionSupport.buildEvent(profileRequestContext, errorEventId);
             return;
         }
 
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 05b40a34..20213c99 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
@@ -296,7 +296,8 @@
         <property name="arguments">
             <list>
                 <value>#{ T(net.shibboleth.oidc.metadata.policy.MetadataPolicy)}</value>
-                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.FederationMetadataPolicyDeserializer"/>
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.FederationMetadataPolicyDeserializer"
+                    c:_0="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"/>
             </list>
         </property>
     </bean>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
index b1755a2f..526384c2 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
@@ -88,7 +88,8 @@
         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')}"/>
+        p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
+        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"/>
 
     <bean id="DefaultMetadataPolicyEnforcer"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEnforcer"
@@ -109,7 +110,13 @@
         <constructor-arg name="target">
             <util:map>
                 <entry key="scope">
-                    <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="openid" />
+                    <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy">
+                        <property name="defaultValue">
+                            <util:list value-type="java.lang.String">
+                                <value>openid</value>
+                            </util:list>
+                        </property>
+                    </bean>
                 </entry>
                 <entry key="token_endpoint_auth_method">
                     <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="private_key_jwt" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
index a977b87e..3b34316d 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
@@ -40,7 +40,8 @@
     <bean id="ValidateProvidedTrustChain" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateProvidedTrustChain"
         scope="prototype"
         p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.register.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
-        p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.register.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}">
+        p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.register.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
+        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}">
         <property name="providedTrustChainValidationStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultProvidedTrustChainValidationStrategy">
                 <property name="trustEngine">
@@ -74,11 +75,51 @@
  
     <bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
         scope="prototype"
-        p:trustChainCache-ref="#{'%{idp.oidfed.register.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:trustChainCache-ref="#{'%{idp.oidfed.register.TrustChainMetadataCache:FetchThroughTrustChainMetadataCache}'.trim()}"
         p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
         p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.register.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
         p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.register.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
-        p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"/>
+        p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
+        p:requireEntityConfigurationCondition-ref="shibboleth.Conditions.TRUE"
+        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}">
+        <property name="entityConfigurationLookupStrategy">
+            <bean parent="shibboleth.Functions.Expression"
+                c:expression="#custom.apply(#input.ensureInboundMessageContext().getMessage().getEntityConfiguration(), null)">
+                <property name="customObject">
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementSignatureValidationFilterStrategy">
+                        <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>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="FetchThroughTrustChainMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+        <constructor-arg>
+            <bean p:cacheId="FetchThroughTrustChainMetadataCache" parent="FetchThroughTrustChainMetadataCacheBuilderSpec"/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="FetchThroughTrustChainMetadataCacheBuilderSpec"
+        class="net.shibboleth.oidc.metadata.cache.impl.FetchThroughMetadataCacheBuilderSpec"
+        p:criteriaToIdentifierStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy">
+        <property name="identifierExtractionStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainSubjectIdentifierExtractionStrategy" />
+        </property>
+        <property name="fetchStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainFetchingStrategy"
+                p:criteriaToSubjectEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
+                p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
+                p:subordinateStatementCache-ref="shibboleth.oidfed.SubordinateEntityStatementMetadataCache"
+                p:localTrustAnchorsCache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" />
+        </property>
+    </bean>
 
     <bean id="DefaultMetadataPolicyEnforcer"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEnforcer"
@@ -99,7 +140,13 @@
         <constructor-arg name="target">
             <util:map>
                 <entry key="scope">
-                    <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="openid" />
+                    <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy">
+                        <property name="defaultValue">
+                            <util:list value-type="java.lang.String">
+                                <value>openid</value>
+                            </util:list>
+                        </property>
+                    </bean>
                 </entry>
                 <entry key="token_endpoint_auth_method">
                     <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="private_key_jwt" />
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
index 74acd075..dc13f569 100644
--- 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
@@ -79,7 +79,8 @@
         p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
         p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.resolve-entity.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
         p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.resolve-entity.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
-        p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"/>
+        p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
+        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"/>
 
     <bean id="DefaultMetadataPolicyEnforcer"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEnforcer"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
index 35d197f5..f17165e7 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
@@ -207,6 +207,21 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         return rpConfiguration.getSignedStatement().serialize();
     }
 
+    protected String rpEntityConfigurationUnmatchingKey(final String clientId,
+            final OIDCClientMetadata metadata, final String... authorityHints) throws URISyntaxException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(clientId).subject(clientId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+                .claim("metadata", Map.of("openid_relying_party", metadata.toJSONObject()))
+                .claim("authority_hints", authorityHints == null || authorityHints.length == 0 ?
+                        new String[] { anchorId } : authorityHints)
+                .build();
+        final EntityStatement rpConfiguration =
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, anchorKey, claimsSet);
+        return rpConfiguration.getSignedStatement().serialize();
+    }
+
     protected String trustedAnchorConfiguration() {
         final String anchorId = "https://trust-anchor.federation.local";
         final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(anchorId)
@@ -292,6 +307,17 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         }
     }
 
+    protected void configureMockHttpClient(final String clientId, final String rpEntityConfiguration) {
+        try {
+            mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration));
+            mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+                    mockResponse(subordinateStatement(clientId)));
+        } catch (UnsupportedOperationException | IOException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+    }
+
     @SuppressWarnings("unchecked")
     protected void configureMockHttpClient(final String clientId, final Map<String, Object> testVector) {
         final String intermediateId = uniqueIntermediateId();
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
index 8bb080d9..1bd9782b 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
@@ -33,9 +33,11 @@ import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
 import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.AuthorizeFlowTest;
 import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultPushedAuthorizationRequestUriSerializationFunction;
@@ -101,6 +103,27 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
     }
 
+    @Test
+    public void testWithInvalidTrustChain_signedRequestObject_unmatchingRpEntityConfigurationSignature()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        configureMockHttpClient(clientId, rpEntityConfigurationUnmatchingKey(clientId, metadata));
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+    }
+
     @Test
     public void testWithValidTrustChain_signedRequestObject_nonMatchingClientId()
             throws IOException, UnsupportedOperationException, URISyntaxException {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
index a210a44a..e5a15c96 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
@@ -69,6 +69,20 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString());
     }
 
+    @Test
+    public void testUnmatchingRpConfigurationSignature() throws Exception {
+        final String clientId = uniqueClientId();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        configureMockHttpClient(clientId, rpEntityConfigurationUnmatchingKey(clientId, metadata));
+        final SignedJWT jwt = createPrivateKeyJWT(validClaimsSet(clientId, issuer),
+                rpKey.toRSAKey().toRSAPrivateKey(), JWSAlgorithm.RS512);
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT, rpKey.toRSAKey().toPublicKey());
+        assertErrorCode(result, OAuth2Error.INVALID_CLIENT_CODE);
+    }
+    
     @Test
     public void testWithPublicClientWithoutRequestObject() throws Exception {
         final OIDCClientMetadata metadata = new OIDCClientMetadata();
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
index c532a0a7..a11f16a9 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
@@ -14,7 +14,13 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
 import java.io.IOException;
+import java.net.URI;
 import java.util.List;
 
 import org.opensaml.storage.StorageRecord;
@@ -25,7 +31,9 @@ import org.springframework.webflow.executor.FlowExecutionResult;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatementClaimsSet;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
@@ -65,6 +73,21 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         assertErrorCode(result, "invalid_client_metadata");
     }
 
+    @Test
+    public void testInvalidEntityConfiguration_wrongSignerKey() throws Exception {
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId);
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        setRequest("POST", rpEntityConfigurationUnmatchingKey(clientId, metadata),
+                "application/entity-statement+jwt");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        assertErrorCode(result, "invalid_request");
+    }
+
     @Test
     public void testValidEntityConfiguration_invalidType() throws Exception {
         final String clientId = uniqueClientId();
@@ -80,9 +103,29 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         configureMockHttpClient(clientId);
         setRequest("POST", rpEntityConfiguration(clientId), "application/entity-statement+jwt");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
         assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
     }
 
+    @Test
+    public void testValidEntityConfiguration_customScope() throws Exception {
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId);
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        final Scope scope = Scope.parse("openid profile email custom");
+        metadata.setScope(scope);
+        setRequest("POST", rpEntityConfiguration(clientId, metadata), "application/entity-statement+jwt");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        final OIDCClientMetadata providedMetadata = assertResponseStatement(
+                parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
+        Assert.assertEquals(providedMetadata.getScope(), scope);
+    }
+
     @Test
     public void testValidEntityConfiguration_repeat() throws Exception {
         final String clientId = uniqueClientId();
@@ -90,6 +133,8 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
             configureMockHttpClient(clientId);
             setRequest("POST", rpEntityConfiguration(clientId), "application/entity-statement+jwt");
             final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+            verify(federationHttpClient, times(0)).executeOpen(any(),
+                    argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
             assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
         }
     }
@@ -101,9 +146,52 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
                 subordinateStatement(clientId) + "\", \"" + trustedAnchorConfiguration() + "\"]";
         setRequest("POST", trustChain, "application/trust-chain+json");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(super.subordinateStatementUrl(anchorFetchEndpoint, clientId))), any());
         assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
     }
 
+    @Test
+    public void testInvalidTrustChain_wrongRpEntityConfigurationSignerKey() throws Exception {
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId);
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        final String trustChain = "[\"" + rpEntityConfigurationUnmatchingKey(clientId, metadata) + "\", \"" +
+                subordinateStatement(clientId) + "\", \"" + trustedAnchorConfiguration() + "\"]";
+        setRequest("POST", trustChain, "application/trust-chain+json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(super.subordinateStatementUrl(anchorFetchEndpoint, clientId))), any());
+        assertErrorCode(result, "invalid_request");
+    }
+
+    @Test
+    public void testValidTrustChain_customScope() throws Exception {
+        final String clientId = uniqueClientId();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        final Scope scope = Scope.parse("openid profile email custom");
+        metadata.setScope(scope);
+        final String trustChain = "[\"" + rpEntityConfiguration(clientId, metadata) + "\", \"" +
+                subordinateStatement(clientId) + "\", \"" + trustedAnchorConfiguration() + "\"]";
+        setRequest("POST", trustChain, "application/trust-chain+json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final OIDCClientMetadata providedMetadata = assertResponseStatement(
+                parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(super.subordinateStatementUrl(anchorFetchEndpoint, clientId))), any());
+        Assert.assertEquals(providedMetadata.getScope(), scope);
+    }
+
     @Test
     public void testValidTrustChain_repeat() throws Exception {
         final String clientId = uniqueClientId();
@@ -116,7 +204,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         }
     }
 
-    protected void assertResponseStatement(final ExplicitClientRegistrationResponse response,
+    protected OIDCClientMetadata assertResponseStatement(final ExplicitClientRegistrationResponse response,
             final String expectedClientId) throws IOException, ParseException, net.minidev.json.parser.ParseException {
         final EntityStatement entityStatement = response.getEntityStatement();
         final EntityStatementClaimsSet statementClaims = entityStatement.getClaimsSet();
@@ -145,7 +233,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         Assert.assertEquals(storedInfo.getOIDCMetadata().getRedirectionURIStrings(),
                 metadata.getRedirectionURIStrings());
         Assert.assertTrue(metadata.getRedirectionURIStrings().contains(redirectUri));
-        
+        return metadata;
     }
     
 }

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


More information about the commits mailing list