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

Henri Mikkonen henri.mikkonen at iki.fi
Wed Aug 6 06:57:57 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=9e894a359c61b0b911485d1c2072d888c0d80f38

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

commit 9e894a359c61b0b911485d1c2072d888c0d80f38
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed Aug 6 09:57:29 2025 +0300

    JOIDC-222 - Support for OpenID Federation
    
    https://shibboleth.atlassian.net/browse/JOIDC-222
    
    Generalized trust chain resolution from pure RP resolution
    - Metadata claim is parsed into Map<String,Map<String,Object>> via ObjectMapper
    - Metadata syntax validation is done via parsing to corresponding Nimbus object
      - Can be customized via idp.oidfed.MetadataValidationCondition (BiPredicate<String,Map<String,Object>>)
    
    Improved Resolve Entity API's support for the entity_type -parameter
    - Not hardcoded to openid_relying_party anymore
    - The use of the parameter is optional
---
 .../decoding/impl/ResolveEntityRequestDecoder.java |  12 +-
 .../messaging/impl/ResolveEntityRequest.java       |  22 +--
 ...ClientMetadataFromTrustChainLookupStrategy.java |  78 -----------
 ...mbinedMetadataFromTrustChainLookupStrategy.java | 109 ++++++++++++++
 .../op/oidfed/metadata/EntityStatementHelper.java  |  41 +++++-
 .../impl/DefaultAllowedEntityTypesConstraint.java  |   2 +-
 .../impl/AbstractTrustChainResolutionAction.java   | 105 ++++++++------
 .../profile/impl/BuildResolveEntityResponse.java   |  54 +++++--
 .../oidfed/profile/impl/OidFederationEventIds.java |   9 +-
 .../impl/RelyingPartyTrustChainContext.java        |  17 ++-
 .../op/oidfed/profile/impl/ResolveTrustChains.java |   9 +-
 .../op/oidfed/profile/impl/ResolveTrustMarks.java  |   3 +-
 .../op/oidfed/profile/impl/SelectTrustChain.java   |   9 +-
 .../profile/impl/StoreAutomaticRegistration.java   |  48 ++++++-
 ...eAutomaticRegistrationProfileConfiguration.java |  45 ++++--
 .../profile/impl/ValidateProvidedTrustChain.java   |   5 +-
 .../profile/impl/ValidateResolveEntityRequest.java |   4 +-
 .../profile/impl/ValidateSelectedTrustChain.java   |   6 +-
 .../navigate/DefaultEntityTypesLookupFunction.java |  46 ++++++
 .../DefaultMetadataValidationCondition.java        |  81 +++++++++++
 ...ltSelectedTrustChainMetadataLookupStrategy.java |  15 +-
 .../DefaultTrustChainSelectionStrategy.java        |   8 +-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  12 ++
 .../oidc/metadata-lookup/metadata-lookup-beans.xml |  11 +-
 .../idp/flows/oidfed/register/register-beans.xml   |  15 +-
 .../oidfed/resolve-entity/resolve-entity-beans.xml |  11 +-
 .../flow/oidfed/AbstractFederationFlowTest.java    | 156 ++++++++++++++++++---
 .../AuthorizeFlowAutomaticRegistrationTest.java    |  38 ++---
 ...shedAuthorizeFlowAutomaticRegistrationTest.java |  28 ++--
 .../profile/flow/oidfed/RegistrationFlowTest.java  |  24 ++--
 .../profile/flow/oidfed/ResolveEntityFlowTest.java |  46 +++++-
 31 files changed, 789 insertions(+), 280 deletions(-)

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
index c83878e9..655bccc1 100644
--- 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
@@ -78,15 +78,7 @@ public class ResolveEntityRequestDecoder extends BaseOAuth2RequestDecoder<Resolv
             if (trustAnchors == null) {
                 throw new MessageDecodingException("No trust_anchor 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);
+            return new ResolveEntityRequest(uri, subject, trustAnchors, parameters.get("entity_type"));
         } catch (final IOException e) {
             log.error("Could not create HTTP request from the request", e);
             throw new MessageDecodingException(e);
@@ -99,7 +91,7 @@ public class ResolveEntityRequestDecoder extends BaseOAuth2RequestDecoder<Resolv
         return message == null ? null : MoreObjects.toStringHelper(this).omitNullValues()
                 .add("subject", message.getSubject())
                 .add("trustAnchors", message.getTrustAnchors())
-                .add("entityType", message.getEntityType())
+                .add("entityTypes", message.getEntityTypes())
                 .add("endpointURI", getEndpointURI(message))
                 .toString();
     }
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
index 79d59f05..2c00b0f9 100644
--- 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
@@ -18,12 +18,14 @@ import java.net.URI;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 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.collection.CollectionSupport;
 import net.shibboleth.shared.logic.Constraint;
 
 /**
@@ -40,8 +42,8 @@ public class ResolveEntityRequest implements Request {
     /** The requested trust anchors. */
     @Nonnull @NotEmpty private final List<String> trustAnchors;
 
-    /** The requested entity type to resolve. */
-    @Nonnull @NotEmpty private final String entityType;
+    /** The requested entity types to resolve. */
+    @Nonnull private final List<String> entityTypes;
 
     /**
      * 
@@ -50,17 +52,17 @@ public class ResolveEntityRequest implements Request {
      * @param uri endpoint URI
      * @param sub subject
      * @param anchors trust anchors
-     * @param type entity type
+     * @param types optional entity types
      */
     public ResolveEntityRequest(@Nonnull final URI uri,
                   @Nonnull @NotEmpty final String sub,
                   @Nonnull @NotEmpty final List<String> anchors,
-                  @Nonnull @NotEmpty final String type) {
+                  @Nullable final List<String>  types) {
         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");
+        entityTypes = types == null ? CollectionSupport.emptyList() : CollectionSupport.copyToList(types);
     }
 
     /**
@@ -86,8 +88,8 @@ public class ResolveEntityRequest implements Request {
      *
      * @return The entity type to resolve.
      */
-    @Nonnull @NotEmpty public String getEntityType() {
-        return entityType;
+    @Nonnull public List<String> getEntityTypes() {
+        return entityTypes;
     }
 
     /** {@inheritDoc} */
@@ -108,7 +110,7 @@ public class ResolveEntityRequest implements Request {
         return MoreObjects.toStringHelper(this)
                 .add("subject", getSubject())
                 .add("trustAnchors", getTrustAnchors())
-                .add("entityType", getEntityType())
+                .add("entityTypes", getEntityTypes())
                 .add("endpointURI", getEndpointURI())
                 .toString();
     }
@@ -127,7 +129,7 @@ public class ResolveEntityRequest implements Request {
         }
         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);
+                entityTypes.containsAll(other.entityTypes) && other.entityTypes.containsAll(entityTypes) &&
+                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/metadata/DefaultClientMetadataFromTrustChainLookupStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultClientMetadataFromTrustChainLookupStrategy.java
deleted file mode 100644
index f9893472..00000000
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultClientMetadataFromTrustChainLookupStrategy.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
-
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.slf4j.Logger;
-
-import com.nimbusds.oauth2.sdk.ParseException;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
-
-import net.minidev.json.JSONObject;
-import net.shibboleth.shared.primitive.LoggerFactory;
-
-/**
- * Default strategy to combine {@link OIDCClientMetadata} from the trust chain by exploiting both entity configuration
- * and subordinate statement issued by the immediate superior.
- */
-public class DefaultClientMetadataFromTrustChainLookupStrategy
-    implements Function<List<EntityStatement>,OIDCClientMetadata> {
-
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultClientMetadataFromTrustChainLookupStrategy.class);
-
-    /** {@inheritDoc} */
-    @Override @Nullable
-    public OIDCClientMetadata apply(@Nullable final List<EntityStatement> chain) {
-        if (chain == null || chain.size() < 3) {
-            log.warn("Unexpected trust chain input: {}", chain == null ? null : "size = " + chain.size());
-            return null;
-        }
-        final 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 {
-            return OIDCClientMetadata.parse(new JSONObject(subordinateClaims));
-        } catch (final ParseException e) {
-            log.error("Could not parse from combined metadata map into a metadata object", e);
-        }
-        return null;
-    }
-
-}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultCombinedMetadataFromTrustChainLookupStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultCombinedMetadataFromTrustChainLookupStrategy.java
new file mode 100644
index 00000000..ec2eda58
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultCombinedMetadataFromTrustChainLookupStrategy.java
@@ -0,0 +1,109 @@
+/*
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default strategy to combine metadata claim contents from the trust chain by exploiting both entity configuration
+ * and subordinate statement issued by the immediate superior.
+ */
+public class DefaultCombinedMetadataFromTrustChainLookupStrategy extends AbstractIdentifiableInitializableComponent
+    implements Function<List<EntityStatement>,Map<String,Map<String,Object>>> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultCombinedMetadataFromTrustChainLookupStrategy.class);
+
+    /** Object mapper used for deserializing metadata. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /**
+     * Set the object mapper used for deserializing metadata
+     * 
+     * @param mapper What to set.
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public Map<String,Map<String,Object>> apply(@Nullable final List<EntityStatement> chain) {
+        if (chain == null || chain.size() < 3) {
+            log.warn("Unexpected trust chain input: {}", chain == null ? null : "size = " + chain.size());
+            return null;
+        }
+        final Map<String,Map<String,Object>> configurationMetadata =
+                EntityStatementHelper.parseMetadata(objectMapper, chain.get(0));
+        final Map<String,Map<String,Object>> subordinateMetadata =
+                EntityStatementHelper.parseMetadata(objectMapper, chain.get(1));
+
+        if (configurationMetadata == null || configurationMetadata.isEmpty()) {
+            log.error("Entity configuration for {} doesn't contain metadata", chain.get(0).getEntityID().getValue());
+            return null;
+        }
+
+        final Map<String,Map<String,Object>> result = new HashMap<>();
+        for (final String entityType : configurationMetadata.keySet()) {
+            final Map<String, Object> configurationClaims = configurationMetadata.get(entityType);
+            final Map<String, Object> subordinateClaims = new HashMap<>(Optional.ofNullable(subordinateMetadata)
+                    .map(metadata -> metadata.get(entityType))
+                    .orElse(CollectionSupport.emptyMap()));
+
+            for (final String configurationClaim : configurationClaims.keySet()) {
+                if (!subordinateClaims.containsKey(configurationClaim)) {
+                    log.trace("Including metadata claim {} from the entity configuration", configurationClaim);
+                    subordinateClaims.put(configurationClaim, configurationClaims.get(configurationClaim));
+                } else {
+                    log.trace("Keeping metadata claim {} from the subordinate configuration", configurationClaim);
+                }
+            }
+
+            result.put(entityType, subordinateClaims);
+        }
+
+        return result;
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementHelper.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementHelper.java
index de05c5f6..fc2491cf 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementHelper.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementHelper.java
@@ -62,7 +62,42 @@ public class EntityStatementHelper {
                     return result;
                 }
             } catch (final JsonProcessingException e) {
-                log.warn("Could not parse " + claim + " from the subordinate statement", e);
+                log.warn("Could not parse " + claim + " from entity statement", e);
+            }
+        }
+        log.trace("Returning empty map");
+        return CollectionSupport.emptyMap();
+    }
+
+    /**
+     * Parses the given claim as Map of Map of Objects from the given entity statement.
+     * 
+     * @param objectMapper object mapper used for parsing
+     * @param entityStatement entity statement from which to parse the claim from
+     * @param claim claim name to be parsed
+     * @return claim value as map or empty map if it didn't exist or could be parsed
+     */
+    @Nonnull public static Map<String, Map<String, Object>> parseClaimAsMapOfMaps(
+            @Nonnull final ObjectMapper objectMapper, @Nonnull final EntityStatement entityStatement,
+            @Nonnull final String claim ) {
+        final Object rawClaim = entityStatement.getClaimsSet().getClaim(claim);
+        log.trace("Raw {} claim value: {}", claim, rawClaim);
+        if (rawClaim != null) {
+            final JavaType objectType = objectMapper.constructType(Object.class);
+            final JavaType stringType = objectMapper.constructType(String.class);
+            final MapType objectMapType =
+                    objectMapper.getTypeFactory().constructMapType(Map.class, stringType, objectType);
+            final MapType mapOfObjectMapType =
+                    objectMapper.getTypeFactory().constructMapType(Map.class, stringType, objectMapType);
+            try {
+                final Map<String, Map<String, Object>> result =
+                        objectMapper.readValue(rawClaim.toString(), mapOfObjectMapType);
+                if (result != null) {
+                    log.trace("Parsed {} map: {}", claim, result);
+                    return result;
+                }
+            } catch (final JsonProcessingException e) {
+                log.warn("Could not parse " + claim + " from entity statement", e);
             }
         }
         log.trace("Returning empty map");
@@ -76,8 +111,8 @@ public class EntityStatementHelper {
      * @param entityStatement entity statement from which to parse the claim from
      * @return the map of metadata or empty map if they didn't exist or could be parsed
      */
-    @Nonnull public static Map<String, Object> parseMetadata(@Nonnull final ObjectMapper objectMapper,
+    @Nonnull public static Map<String, Map<String, Object>> parseMetadata(@Nonnull final ObjectMapper objectMapper,
             @Nonnull final EntityStatement entityStatement) {
-        return parseClaimAsMap(objectMapper, entityStatement, "metadata");
+        return parseClaimAsMapOfMaps(objectMapper, entityStatement, "metadata");
     }
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java
index f90c125b..f18fe3e7 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java
@@ -90,7 +90,7 @@ public class DefaultAllowedEntityTypesConstraint extends AbstractFederationPolic
         for (final EntityStatement entityStatement : trustChain) {
             assert entityStatement != null;
             assert objectMapper != null;
-            final Map<String, Object> metadata =
+            final Map<String, Map<String,Object>> metadata =
                     EntityStatementHelper.parseMetadata(objectMapper, entityStatement);
             for (final String entityType : metadata.keySet()) {
                 if (!allowedTypes.contains(entityType)) {
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
index 59e75142..12288d7b 100644
--- 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
@@ -14,10 +14,12 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -28,15 +30,9 @@ 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;
@@ -67,8 +63,8 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
     /** 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 get combined entity metadata from trust chain. */
+    @Nonnull private Function<List<EntityStatement>,Map<String,Map<String,Object>>> metadataLookupStrategy;
 
     /** Strategy used to merge metadata policies in trust chain for specific entity type. */
     @NonnullAfterInit private BiFunction<List<EntityStatement>,String,Map<String, MetadataPolicy>>
@@ -77,6 +73,9 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
     /** Enforcer function for applying metadata policy for an item. */
     @NonnullAfterInit private BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> metadataPolicyEnforcer;
 
+    /** Condition used to validate metadata for an entity type. */
+    @NonnullAfterInit private BiPredicate<String, Map<String, Object>> metadataValidationCondition;
+
     /** List of claim names who are transformed from a space-separated String into a List. */
     @Nonnull private List<String> arraysAsSpaceSeparatedList;
 
@@ -89,7 +88,6 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
                         new InboundMessageContextLookup());
         assert tccs != null;
         trustChainContextCreationStrategy = tccs;
-        metadataLookupStrategy = new DefaultClientMetadataFromTrustChainLookupStrategy();
         arraysAsSpaceSeparatedList = CollectionSupport.listOf("scope");
     }
 
@@ -117,22 +115,23 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
     }
 
     /**
-     * Set the strategy used to get combined OIDC client metadata from trust chain.
+     * Set the strategy used to get combined entity metadata from trust chain.
      * 
      * @param strategy lookup strategy
      */
-    public void setMetadataLookupStrategy(@Nonnull final Function<List<EntityStatement>,OIDCClientMetadata> strategy) {
+    public void setMetadataLookupStrategy(
+            @Nonnull final Function<List<EntityStatement>,Map<String,Map<String,Object>>> strategy) {
         checkSetterPreconditions();
         metadataLookupStrategy =
                 Constraint.isNotNull(strategy, "MetadataLookupStrategy cannot be null");
     }
 
     /**
-     * Get the strategy used to get combined OIDC client metadata from trust chain.
+     * Get the strategy used to get combined entity metadata from trust chain.
      * 
      * @return lookup strategy
      */
-    @Nonnull public Function<List<EntityStatement>,OIDCClientMetadata> getMetadataLookupStrategy() {
+    @Nonnull public Function<List<EntityStatement>,Map<String,Map<String,Object>>> getMetadataLookupStrategy() {
         checkComponentActive();
         return metadataLookupStrategy;
     }
@@ -184,7 +183,7 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
     }
 
     /**
-     * Set the  list of claim names who are transformed from a space-separated String into a List.
+     * Set the list of claim names who are transformed from a space-separated String into a List.
      * 
      * @param list list of claim names
      */
@@ -194,7 +193,7 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
     }
 
     /**
-     * Get the  list of claim names who are transformed from a space-separated String into a List.
+     * Get the list of claim names who are transformed from a space-separated String into a List.
      * 
      * @return list of claim names
      */
@@ -203,6 +202,16 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
         return arraysAsSpaceSeparatedList;
     }
 
+    /**
+     * Set the condition used to validate metadata for an entity type.
+     * 
+     * @param condition validation condition
+     */
+    public void setMetadataValidationCondition(@Nonnull final BiPredicate<String,Map<String,Object>> condition) {
+        checkSetterPreconditions();
+        metadataValidationCondition = Constraint.isNotNull(condition, "MetadataValidationCondition cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -214,6 +223,9 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
         if (metadataPolicyEnforcer == null) {
             throw new ComponentInitializationException("MetadataPolicyEnforcer cannot be null");
         }
+        if (metadataValidationCondition == null) {
+            throw new ComponentInitializationException("MetadataValidationCondition cannot be null");
+        }
     }
 
     /**
@@ -224,43 +236,46 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
      * @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);
+            @Nonnull List<Pair<List<EntityStatement>, Map<String,Map<String,Object>>>> policyCompliantChains) {
+        final Map<String,Map<String,Object>> 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);
+            final Map<String,Map<String,Object>> verifiedMetadata = new HashMap<>();
+            for (final String entityType : metadata.keySet()) {
+                final Map<String, MetadataPolicy> mergedPolicies;
                 try {
-                    final Object enforcedValue = enforceValue(claim, requestMetadata.get(claim), policy);
-                    requestMetadata.put(claim, enforcedValue);
+                    mergedPolicies = getMetadataPolicyMergingStrategy().apply(chain, entityType);
+                    log.debug("{} Merged policy for type {} for chain {}", getLogPrefix(), entityType, mergedPolicies);
                 } catch (final ConstraintViolationException e) {
-                    log.warn("{} The requested metadata is not compliant with the policy", getLogPrefix());
-                    return OidFederationEventIds.INVALID_METADATA_AGAINST_POLICY;
+                    log.warn("{} Could not merge metadata policies", getLogPrefix(), e);
+                    return OidFederationEventIds.INVALID_METADATA_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);
+                final JSONObject requestMetadata = new JSONObject(metadata.get(entityType));
+                for (final String claim : mergedPolicies.keySet()) {
+                    assert claim != null;
+                    final MetadataPolicy policy = mergedPolicies.get(claim);
+                    try {
+                        final Object enforcedValue = enforceValue(claim, requestMetadata.get(claim), policy);
+                        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());
+                if (metadataValidationCondition.test(entityType, requestMetadata)) {
+                    verifiedMetadata.put(entityType, requestMetadata);
+                    log.debug("{} Policy-enforced metadata {}", getLogPrefix(), requestMetadata.toJSONString());
+                } else {
+                    log.warn("{} Metadata validation failed for {} for entity type {}", getLogPrefix(),
+                            chain.get(0).getEntityID(), entityType);
+                }
             }
+            policyCompliantChains.add(new Pair<>(chain, verifiedMetadata));
+            log.debug("{} Policy-enforced metadata {}", getLogPrefix(), verifiedMetadata);
+            return null;
         }
         return EventIds.INVALID_MSG_CTX;
     }
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
index c5714b20..a17184aa 100644
--- 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
@@ -37,10 +37,9 @@ import org.slf4j.Logger;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 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.profile.navigate.DefaultEntityTypesLookupFunction;
 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;
@@ -57,6 +56,9 @@ public class BuildResolveEntityResponse extends AbstractBuildEntityStatementActi
     /** Strategy used to lookup the trust chain context. */
     @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
 
+    /** Strategy used to lookup the entity types included in the response metadata. */
+    @Nonnull private Function<ProfileRequestContext, List<String>> entityTypesLookupStrategy;
+
     /** Trust chain context to operate on. */
     @NonnullBeforeExec private RelyingPartyTrustChainContext trustChainContext;
 
@@ -67,6 +69,7 @@ public class BuildResolveEntityResponse extends AbstractBuildEntityStatementActi
                         new InboundMessageContextLookup());
         assert tcls != null;
         trustChainContextLookupStrategy = tcls;
+        entityTypesLookupStrategy = new DefaultEntityTypesLookupFunction();
     }
     
     /**
@@ -81,6 +84,16 @@ public class BuildResolveEntityResponse extends AbstractBuildEntityStatementActi
                 Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
     }
 
+    /**
+     * Set the strategy used to lookup the entity types included in the response metadata.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setEntityTypesLookupStrategy(@Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+        checkSetterPreconditions();
+        entityTypesLookupStrategy = Constraint.isNotNull(strategy, "EntityTypesLookupStrategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -102,27 +115,42 @@ public class BuildResolveEntityResponse extends AbstractBuildEntityStatementActi
     @Override
     protected boolean populateClaimsSetBuilder(@Nonnull final JWTClaimsSet.Builder builder,
             @Nonnull final ProfileRequestContext profileRequestContext) {
-        final Pair<List<EntityStatement>,OIDCClientInformation> selectedTrustChain =
+        final Pair<List<EntityStatement>,Map<String,Map<String,Object>>> selectedTrustChain =
                 trustChainContext.getSelectedTrustChain();
         if (selectedTrustChain == null) {
             log.debug("{} No selected trust chain found form the context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_METADATA);
             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);
+        final Map<String,Map<String,Object>> metadata = selectedTrustChain.getSecond();
+        if (metadata == null) {
+            log.debug("{} No metadata set for the selected trust chain", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_METADATA);
             return false;                        
         }
-        builder.claim("metadata",
-                CollectionSupport.singletonMap("openid_relying_party", clientInformation.toJSONObject()));
+        final List<String> entityTypes = entityTypesLookupStrategy.apply(profileRequestContext);
+        log.trace("{} The following entity types were requested: {}", getLogPrefix(), entityTypes);
+        if (entityTypes != null && !entityTypes.isEmpty()) {
+            final Map<String,Object> filteredMetadata = metadata.entrySet()
+                    .stream()
+                    .filter(entry -> entityTypes.contains(entry.getKey()))
+                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+            if  (filteredMetadata.isEmpty()) {
+                log.warn("{} No metadata for entity types {} found for the selected trust chain", getLogPrefix(),
+                        entityTypes);
+                ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_METADATA);
+                return false;
+            }
+            builder.claim("metadata", filteredMetadata);
+        } else {
+            builder.claim("metadata", metadata);
+        }
 
         final List<EntityStatement> trustChain = selectedTrustChain.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;                        
+            ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_METADATA);
+            return false;
         }
         builder.claim("trust_chain", trustChain.stream()
                 .map(statement -> statement.getSignedStatement().serialize())
@@ -136,7 +164,7 @@ public class BuildResolveEntityResponse extends AbstractBuildEntityStatementActi
         }
         builder.expirationTime(Date.from(expirationTime));
 
-        final String entityId = clientInformation.getID().getValue();
+        final String entityId = trustChain.get(0).getEntityID().getValue();
         assert entityId != null;
         final Map<String, String> trustMarks = buildTrustMarks(entityId, trustChainContext.getVerifiedTrustMarks());
         if (trustMarks != null && !trustMarks.isEmpty()) {
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
index 89551686..1f3e9f4f 100644
--- 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
@@ -45,12 +45,17 @@ public class OidFederationEventIds {
     @Nonnull @NotEmpty public static final String INVALID_SUBJECT = "InvalidSubject";
 
     /**
-     * ID of event returned if the given trust anchor is invalid.
+     * ID of event returned if the given metadata is invalid.
+     */
+    @Nonnull @NotEmpty public static final String INVALID_METADATA = "InvalidMetadata";
+
+    /**
+     * ID of event returned if the given metadata policy is invalid.
      */
     @Nonnull @NotEmpty public static final String INVALID_METADATA_POLICY = "InvalidMetadataPolicy";
 
     /**
-     * ID of event returned if the given trust anchor is invalid.
+     * ID of event returned if the given metadata is invalid against policy.
      */
     @Nonnull @NotEmpty public static final String INVALID_METADATA_AGAINST_POLICY = "InvalidMetadataAgainstPolicy";
 }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/RelyingPartyTrustChainContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/RelyingPartyTrustChainContext.java
index 8a6ff150..c9acadcb 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/RelyingPartyTrustChainContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/RelyingPartyTrustChainContext.java
@@ -25,7 +25,6 @@ import org.opensaml.messaging.context.BaseContext;
 
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 
 import net.shibboleth.shared.collection.Pair;
 
@@ -40,10 +39,10 @@ public final class RelyingPartyTrustChainContext extends BaseContext {
     @Nullable private List<List<EntityStatement>> resolvedTrustChains;
 
     /** Policy-compliant trust chains for the relying party. */
-    @Nullable private List<Pair<List<EntityStatement>,OIDCClientInformation>> policyCompliantTrustChains;
+    @Nullable private List<Pair<List<EntityStatement>,Map<String,Map<String,Object>>>> policyCompliantTrustChains;
 
     /** Selected trust chain for the relying party. */
-    @Nullable private Pair<List<EntityStatement>,OIDCClientInformation> selectedTrustChain;
+    @Nullable private Pair<List<EntityStatement>,Map<String,Map<String,Object>>> selectedTrustChain;
 
     /** Expiration instant for the selected metadata. */
     @Nullable private Instant selectedMetadataExpiration;
@@ -84,7 +83,7 @@ public final class RelyingPartyTrustChainContext extends BaseContext {
      * 
      * @return the trust chains
      */
-    @Nullable public List<Pair<List<EntityStatement>,OIDCClientInformation>> getPolicyCompliantTrustChains() {
+    @Nullable public List<Pair<List<EntityStatement>,Map<String,Map<String,Object>>>> getPolicyCompliantTrustChains() {
         return policyCompliantTrustChains;
     }
 
@@ -96,7 +95,7 @@ public final class RelyingPartyTrustChainContext extends BaseContext {
      * @return this context
      */
     @Nonnull public RelyingPartyTrustChainContext setPolicyCompliantTrustChains(
-            @Nullable final List<Pair<List<EntityStatement>,OIDCClientInformation>> chains) {
+            @Nullable final List<Pair<List<EntityStatement>,Map<String,Map<String,Object>>>> chains) {
         policyCompliantTrustChains = chains;
         return this;
     }
@@ -106,19 +105,19 @@ public final class RelyingPartyTrustChainContext extends BaseContext {
      * 
      * @return the trust chain
      */
-    @Nullable public Pair<List<EntityStatement>,OIDCClientInformation> getSelectedTrustChain() {
+    @Nullable public Pair<List<EntityStatement>,Map<String,Map<String,Object>>> getSelectedTrustChain() {
         return selectedTrustChain;
     }
 
     /**
      * Set the selected trust chain for the relying party.
      * 
-     * @param chais the selected trust chain
+     * @param chain the selected trust chain
      * 
      * @return this context
      */
     @Nonnull public RelyingPartyTrustChainContext setSelectedTrustChains(
-            @Nullable final Pair<List<EntityStatement>,OIDCClientInformation> chain) {
+            @Nullable final Pair<List<EntityStatement>,Map<String,Map<String,Object>>> chain) {
         selectedTrustChain = chain;
         return this;
     }
@@ -178,7 +177,7 @@ public final class RelyingPartyTrustChainContext extends BaseContext {
     /**
      * Set the verified trust marks for the selected trust chain.
      * 
-     * @param trustmarks verified trust marks
+     * @param trustMarks verified trust marks
      * 
      * @return this context
      */
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 66fbb71e..2fad9aae 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,6 +19,7 @@ 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.Function;
 import java.util.function.Predicate;
@@ -31,7 +32,6 @@ import org.slf4j.Logger;
 
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
@@ -83,7 +83,7 @@ public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
     /** 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}. */
+    /** Condition to require entity configuration via {@link #entityConfigurationLookupStrategy}. */
     @Nonnull private Predicate<ProfileRequestContext> requireEntityConfigurationCondition;
 
     /** OAuth2 client id. */
@@ -156,7 +156,7 @@ public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
     }
 
     /**
-     * Set the condition to require entity configuration via {@link this#entityConfigurationLookupStrategy}.
+     * Set the condition to require entity configuration via {@link #entityConfigurationLookupStrategy}.
      * @param predicate condition
      */
     public void setRequireEntityConfigurationCondition(@Nonnull final Predicate<ProfileRequestContext> predicate) {
@@ -234,7 +234,8 @@ public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
         final RelyingPartyTrustChainContext trustChainContext =
                 getTrustChainContextCreationStrategy().apply(profileRequestContext);
         trustChainContext.setResolvedTrustChains(cacheResult.get(0));
-        final List<Pair<List<EntityStatement>, OIDCClientInformation>> policyCompliantChains = new ArrayList<>();
+        final List<Pair<List<EntityStatement>, Map<String,Map<String,Object>>>> policyCompliantChains =
+                new ArrayList<>();
 
         String errorEventId = null;
         for (final List<EntityStatement> chain : cacheResult.get(0)) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
index 0b194d61..9757b225 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
@@ -39,7 +39,6 @@ import org.slf4j.Logger;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 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.metadata.DefaultTrustChainTrustMarksParsingStrategy;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
@@ -277,7 +276,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
             return false;
         }
 
-        final Pair<List<EntityStatement>, OIDCClientInformation> selectedChain =
+        final Pair<List<EntityStatement>, Map<String,Map<String,Object>>> selectedChain =
                 trustChainContext.getSelectedTrustChain();
 
         if (selectedChain == null || selectedChain.getFirst() == null) {
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 5cba50d4..085ffdbc 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
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
 
 import java.util.List;
+import java.util.Map;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -27,7 +28,6 @@ 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.profile.navigate.DefaultTrustChainSelectionStrategy;
 import net.shibboleth.idp.profile.AbstractProfileAction;
@@ -60,7 +60,8 @@ public class SelectTrustChain extends AbstractProfileAction {
     @Nonnull private Function<ProfileRequestContext, RelyingPartyContext> relyingPartyContextCreationStrategy;
 
     /** Strategy used to fetch the selected trust chain and metadata. */
-    @NonnullAfterInit private Function<ProfileRequestContext,Pair<List<EntityStatement>, OIDCClientInformation>>
+    @NonnullAfterInit
+    private Function<ProfileRequestContext,Pair<List<EntityStatement>, Map<String,Map<String,Object>>>>
         selectedTrustChainLookupStrategy;
 
     /** Trust chain context to operate on. */
@@ -113,7 +114,7 @@ public class SelectTrustChain extends AbstractProfileAction {
      * @param strategy lookup strategy
      */
     public void setSelectedTrustChainLookupStrategy(@Nonnull final
-            Function<ProfileRequestContext,Pair<List<EntityStatement>, OIDCClientInformation>> strategy) {
+            Function<ProfileRequestContext,Pair<List<EntityStatement>, Map<String,Map<String,Object>>>> strategy) {
         checkSetterPreconditions();
         selectedTrustChainLookupStrategy =
                 Constraint.isNotNull(strategy, "SelectedTrustChainLookupStrategy cannot be null");
@@ -146,7 +147,7 @@ public class SelectTrustChain extends AbstractProfileAction {
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        final Pair<List<EntityStatement>, OIDCClientInformation> selectedChain =
+        final Pair<List<EntityStatement>, Map<String,Map<String,Object>>> selectedChain =
                 selectedTrustChainLookupStrategy.apply(profileRequestContext);
 
         if (selectedChain == null || selectedChain.getFirst() == null || selectedChain.getSecond() == null) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/StoreAutomaticRegistration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/StoreAutomaticRegistration.java
index 5074365e..6be3b655 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/StoreAutomaticRegistration.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/StoreAutomaticRegistration.java
@@ -16,6 +16,7 @@ package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
 
 import java.time.Instant;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -27,9 +28,12 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
+import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainMetadataLookupStrategy;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.oidc.metadata.ClientInformationManager;
 import net.shibboleth.oidc.metadata.ClientInformationManagerException;
@@ -55,6 +59,9 @@ public class StoreAutomaticRegistration extends AbstractProfileAction {
     /** Strategy used to lookup the trust chain context. */
     @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
 
+    /** Strategy used to lookup the selected metadata. */
+    @Nonnull private Function<ProfileRequestContext, OIDCClientMetadata> selectedMetadataLookupStrategy;
+
     /** The client information to be stored. */
     @NonnullBeforeExec private OIDCClientInformation clientInformation;
 
@@ -70,6 +77,7 @@ public class StoreAutomaticRegistration extends AbstractProfileAction {
                         new InboundMessageContextLookup());
         assert tcls != null;
         trustChainContextLookupStrategy = tcls;
+        selectedMetadataLookupStrategy = new DefaultSelectedTrustChainMetadataLookupStrategy();
     }
 
     /**
@@ -90,6 +98,30 @@ public class StoreAutomaticRegistration extends AbstractProfileAction {
         clientInformationManager = Constraint.isNotNull(manager, "The client information manager cannot be null!");
     }
 
+    /**
+     * Set the strategy used to lookup the trust chain context.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustChainContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+        checkSetterPreconditions();
+        trustChainContextLookupStrategy =
+                Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup the selected metadata.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSelectedMetadataLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCClientMetadata> strategy) {
+        checkSetterPreconditions();
+        selectedMetadataLookupStrategy =
+                Constraint.isNotNull(strategy, "SelectedMetadataLookupStrategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -115,16 +147,20 @@ public class StoreAutomaticRegistration extends AbstractProfileAction {
             return false;
         }
 
-        final Pair<List<EntityStatement>,OIDCClientInformation> selectedTrustChain =
-                trustChainContext.getSelectedTrustChain();
-        assert selectedTrustChain != null;
-        clientInformation = selectedTrustChain.getSecond();
-        if (clientInformation == null) {
-            log.error("{} Unable to locate selected metadata", getLogPrefix());
+        final OIDCClientMetadata metadata = selectedMetadataLookupStrategy.apply(profileRequestContext);
+        if (metadata == null) {
+            log.error("{} Unable to parse the RP metadata for storing", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
             return false;
         }
 
+        final Pair<List<EntityStatement>,Map<String,Map<String,Object>>> selectedTrustChain =
+                trustChainContext.getSelectedTrustChain();
+        assert selectedTrustChain != null;
+
+        clientInformation = new OIDCClientInformation(
+                new ClientID(selectedTrustChain.getFirst().get(0).getEntityID().getValue()), metadata);
+
         expiration = trustChainContext.getSelectedMetadataExpiration();
         if (expiration == null) {
             log.error("{} Unable to resolve expiration time for the selected metadata", getLogPrefix());
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
index 0e4e4ad4..530c3ca3 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
@@ -29,10 +29,13 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
+import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.MandatoryTrustMarksLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainMetadataLookupStrategy;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.LocalMetadataPolicyLookupFunction;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
@@ -69,6 +72,9 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
     /** Strategy used to lookup the trust chain context. */
     @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
 
+    /** Strategy used to lookup the selected metadata. */
+    @Nonnull private Function<ProfileRequestContext, OIDCClientMetadata> selectedMetadataLookupStrategy;
+
     /** Strategy used to lookup mandatory trust marks. */
     @Nonnull private Function<ProfileRequestContext, List<String>> mandatoryTrustMarksLookupStrategy;
 
@@ -83,10 +89,7 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
     @NonnullBeforeExec private RelyingPartyTrustChainContext trustChainContext;
 
     /** Selected trust chain to operate on. */
-    @NonnullBeforeExec private Pair<List<EntityStatement>, OIDCClientInformation> selectedTrustChain;
-
-    /** Client ID of the client. */
-    @NonnullBeforeExec private String clientId;
+    @NonnullBeforeExec private Pair<List<EntityStatement>, Map<String,Map<String,Object>>> selectedTrustChain;
 
     /**
      * Constructor.
@@ -97,6 +100,7 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
                         new InboundMessageContextLookup());
         assert tcls != null;
         trustChainContextLookupStrategy = tcls;
+        selectedMetadataLookupStrategy = new DefaultSelectedTrustChainMetadataLookupStrategy();
         mandatoryTrustMarksLookupStrategy = new MandatoryTrustMarksLookupFunction();
         localMetadataPolicyLookupStrategy = new LocalMetadataPolicyLookupFunction();
     }
@@ -113,6 +117,18 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
                 Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
     }
 
+    /**
+     * Set the strategy used to lookup the selected metadata.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSelectedMetadataLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCClientMetadata> strategy) {
+        checkSetterPreconditions();
+        selectedMetadataLookupStrategy =
+                Constraint.isNotNull(strategy, "SelectedMetadataLookupStrategy cannot be null");
+    }
+
     /**
      * Set the strategy used to lookup mandatory trust marks.
      * 
@@ -171,30 +187,35 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
             return false;
         }
 
-        final OIDCClientInformation clientInformation = selectedTrustChain.getSecond();
-        assert clientInformation != null;
-        clientId = clientInformation.getID().getValue();
-
         return true;
     }
 
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final OIDCClientMetadata metadata = selectedMetadataLookupStrategy.apply(profileRequestContext);
+        if (metadata == null) {
+            log.error("{} Unable to parse the RP metadata for storing", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return;
+        }
+        final OIDCClientInformation clientInformation = new OIDCClientInformation(
+                new ClientID(selectedTrustChain.getFirst().get(0).getEntityID().getValue()), metadata);
+
         final Map<String, MetadataPolicy> localMetadataPolicy =
                 localMetadataPolicyLookupStrategy.apply(profileRequestContext);
         if (localMetadataPolicy != null && !localMetadataPolicy.isEmpty()) {
             log.debug("{} Applying local metadata policy into the client metadata", getLogPrefix());
             final OIDCClientInformation enforcedMetadata =
-                    localMetadataPolicyMergingStrategy.apply(selectedTrustChain.getSecond(), localMetadataPolicy);
+                    localMetadataPolicyMergingStrategy.apply(clientInformation, localMetadataPolicy);
             if (enforcedMetadata == null) {
                 log.error("{} Could not apply the local metadata policy into the client metadata", getLogPrefix());
                 ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
                 return;
             }
-            selectedTrustChain.setSecond(enforcedMetadata);
+            selectedTrustChain.setSecond(Map.of("openid_relying_party", clientInformation.toJSONObject()));
         }
- 
+        final String clientId = clientInformation.getID().getValue();
         final List<String> mandatoryTrustMarks = mandatoryTrustMarksLookupStrategy.apply(profileRequestContext);
         if (mandatoryTrustMarks != null && !mandatoryTrustMarks.isEmpty()) {
             log.debug("{} Verifying the mandatory trust marks {}", getLogPrefix(), mandatoryTrustMarks);
@@ -221,7 +242,7 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
         trustChainContext.setSelectedMetadataExpiration(resolveTrustChainExpiration(trustChain));
 
         final OIDCMetadataContext oidcCtx = new OIDCMetadataContext();
-        oidcCtx.setClientInformation(selectedTrustChain.getSecond());
+        oidcCtx.setClientInformation(clientInformation);
         profileRequestContext.ensureInboundMessageContext().addSubcontext(oidcCtx);
         log.debug("{} Client information attached to the OIDCMetadataContext", getLogPrefix());
     }
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 9cfd6f4b..36384eca 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,6 +16,7 @@ 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.BiPredicate;
 import java.util.function.Function;
 
@@ -26,7 +27,6 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
@@ -126,7 +126,8 @@ public class ValidateProvidedTrustChain extends AbstractTrustChainResolutionActi
                 getTrustChainContextCreationStrategy().apply(profileRequestContext);
         assert trustChain != null;
         trustChainContext.setResolvedTrustChains(CollectionSupport.listOf(trustChain));
-        final List<Pair<List<EntityStatement>, OIDCClientInformation>> policyCompliantChains = new ArrayList<>();
+        final List<Pair<List<EntityStatement>, Map<String,Map<String,Object>>>> policyCompliantChains =
+                new ArrayList<>();
 
         final String errorEventId = populatePolicyComplaintChains(trustChain, policyCompliantChains);
         if (errorEventId != null) {
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
index 02112173..846aac2a 100644
--- 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
@@ -96,7 +96,7 @@ public class ValidateResolveEntityRequest extends AbstractProfileAction {
     /**
      * Set the strategy used to lookup the trust chain context.
      * 
-     * @param strategy lookup strategy
+     * @param cache lookup strategy
      */
     public void setLocalTrustAnchorsCache(
             @Nonnull final MetadataCache<Map<String, LocalKeyContainer>> cache) {
@@ -155,7 +155,7 @@ public class ValidateResolveEntityRequest extends AbstractProfileAction {
         log.debug("{} The following trust anchors were validated: {}", getLogPrefix(), validatedAnchors);
         resolveEntityContext.setValidatedRequest(
                 new ResolveEntityRequest(requestMessage.getEndpointURI(), requestMessage.getSubject(),
-                        validatedAnchors, requestMessage.getEntityType()));
+                        validatedAnchors, requestMessage.getEntityTypes()));
     }
 
     /**
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
index 88e63c4a..082cf41c 100644
--- 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
@@ -16,6 +16,7 @@ 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.Function;
 
 import javax.annotation.Nonnull;
@@ -26,7 +27,6 @@ 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;
@@ -160,10 +160,10 @@ public class ValidateSelectedTrustChain extends AbstractProfileAction {
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         final RelyingPartyTrustChainContext trustChainContext =
                 trustChainContextLookupStrategy.apply(profileRequestContext);
-        final Pair<List<EntityStatement>, OIDCClientInformation>  selectedTrustChain =
+        final Pair<List<EntityStatement>, Map<String,Map<String,Object>>>  selectedTrustChain =
                 trustChainContext != null ? trustChainContext.getSelectedTrustChain() : null;
         if (selectedTrustChain == null || selectedTrustChain.getFirst() == null) {
-            final List<Pair<List<EntityStatement>,OIDCClientInformation>> allChains =
+            final List<Pair<List<EntityStatement>,Map<String,Map<String,Object>>>> allChains =
                     trustChainContext != null ? trustChainContext.getPolicyCompliantTrustChains() : null;
             if (allChains == null || allChains.isEmpty()) {
                 if (isSubjectValid(validatedRequest.getSubject())) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultEntityTypesLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultEntityTypesLookupFunction.java
new file mode 100644
index 00000000..7ba6c819
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultEntityTypesLookupFunction.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+
+/**
+ * Default function to lookup entity types to be included to the response metadata.
+ */
+public class DefaultEntityTypesLookupFunction implements Function<ProfileRequestContext, List<String>> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public List<String> apply(@Nullable final ProfileRequestContext profileRequestContext) {
+        return Optional.ofNullable(profileRequestContext)
+                .map(prc -> prc.getInboundMessageContext())
+                .map(msgCtx -> msgCtx.getMessage())
+                .filter(ResolveEntityRequest.class::isInstance)
+                .map(ResolveEntityRequest.class::cast)
+                .map(req -> req.getEntityTypes())
+                .orElseGet(NonnullSupplier.of(CollectionSupport.emptyList()));
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultMetadataValidationCondition.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultMetadataValidationCondition.java
new file mode 100644
index 00000000..37330379
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultMetadataValidationCondition.java
@@ -0,0 +1,81 @@
+/*
+ * 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.navigate;
+
+import java.util.Map;
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
+import com.nimbusds.oauth2.sdk.client.ClientMetadata;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Default validation condition for metadata entries by entity type. Validation is done by parsing the entity type
+ * into a corresponding Nimbus object.
+ */
+public class DefaultMetadataValidationCondition implements BiPredicate<String, Map<String,Object>> {
+
+    /** Class logger. */
+    @Nonnull private final static Logger log = LoggerFactory.getLogger(DefaultMetadataValidationCondition.class);
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(@Nullable final String key, @Nullable final Map<String, Object> metadata) {
+        if (StringSupport.trimOrNull(key) == null || metadata == null) {
+            log.error("Invalid input to the validation condition key={}, metadata={}", key, metadata);
+            return false;
+        }
+        try {
+            switch (key) {
+                case "federation_entity":
+                    log.debug("Ignoring validation of {}", key);
+                    return true;
+                case "openid_provider":
+                    OIDCProviderMetadata.parse(new JSONObject(metadata));
+                    return true;
+                case "openid_relying_party":
+                    OIDCClientMetadata.parse(new JSONObject(metadata));
+                    return true;
+                case "oauth_authorization_server":
+                    AuthorizationServerMetadata.parse(new JSONObject(metadata));
+                    return true;
+                case "oauth_client":
+                    ClientMetadata.parse(new JSONObject(metadata));
+                    return true;
+                case "oauth_resource":
+                    log.debug("Ignoring validation of {}", key);
+                    return true;
+                default:
+                    log.debug("Ignoring validation of {}", key);
+                    return true;
+            }
+        } catch (final ParseException e) {
+            log.warn("Could not parse entity_type {}", key, e);
+        }
+        return false;
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainMetadataLookupStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainMetadataLookupStrategy.java
index 205c2c6c..bd42f6ee 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainMetadataLookupStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainMetadataLookupStrategy.java
@@ -14,13 +14,16 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate;
 
+import java.util.Map;
 import java.util.Optional;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import com.nimbusds.oauth2.sdk.ParseException;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
+import net.minidev.json.JSONObject;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext;
 
 /**
@@ -35,7 +38,17 @@ public class DefaultSelectedTrustChainMetadataLookupStrategy
     public OIDCClientMetadata doApply(@Nonnull final RelyingPartyTrustChainContext trustChainContext) {
         return Optional.ofNullable(trustChainContext.getSelectedTrustChain())
                 .map(pair -> pair.getSecond())
-                .map(clientInfo -> clientInfo.getOIDCMetadata())
+                .map(map -> map.get("openid_relying_party"))
+                .filter(Map.class::isInstance)
+                .map(Map.class::cast)
+                .map(JSONObject::new)
+                .map(json-> {
+                    try {
+                        return OIDCClientMetadata.parse(json);
+                    } catch (ParseException e) {
+                        return null;
+                    }
+                })
                 .orElse(null);
     }
 }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainSelectionStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainSelectionStrategy.java
index 3bbe1cd5..a3a82549 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainSelectionStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainSelectionStrategy.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate;
 
 import java.util.List;
+import java.util.Map;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -22,7 +23,6 @@ import javax.annotation.Nullable;
 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.profile.impl.RelyingPartyTrustChainContext;
 import net.shibboleth.shared.collection.Pair;
@@ -34,16 +34,16 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * {@link RelyingPartyTrustChainContext#getRejectedTrustChains()}.
  */
 public class DefaultTrustChainSelectionStrategy
-    extends AbstractTrustChainContextLookupFunction<Pair<List<EntityStatement>, OIDCClientInformation>> {
+    extends AbstractTrustChainContextLookupFunction<Pair<List<EntityStatement>, Map<String,Map<String,Object>>>> {
 
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(DefaultTrustChainSelectionStrategy.class);
 
     /** {@inheritDoc} */
     @Override @Nullable
-    public Pair<List<EntityStatement>, OIDCClientInformation> doApply(
+    public Pair<List<EntityStatement>, Map<String,Map<String,Object>>> doApply(
             @Nonnull final RelyingPartyTrustChainContext trustChainContext) {
-        final List<Pair<List<EntityStatement>, OIDCClientInformation>> policyCompliantChains =
+        final List<Pair<List<EntityStatement>, Map<String,Map<String,Object>>>> policyCompliantChains =
                 trustChainContext.getPolicyCompliantTrustChains();
 
         if (policyCompliantChains == null || policyCompliantChains.isEmpty()) {
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 6ef357d6..dd0c2d13 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
@@ -781,6 +781,12 @@
             class="org.springframework.beans.factory.config.MapFactoryBean">
         <property name="sourceMap">
             <map merge="true" value-type="com.nimbusds.oauth2.sdk.ErrorObject">
+                <entry>
+                    <key>
+                        <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MSG_CTX"/>
+                    </key>
+                    <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="server_error" c:_1="Internal server error" c:_2="500" />
+                </entry>
                 <entry>
                     <key>
                         <util:constant static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.OidFederationEventIds.INVALID_TRUST_ANCHOR"/>
@@ -793,6 +799,12 @@
                     </key>
                     <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_subject" c:_1="Subject in the request is invalid" c:_2="404" />
                 </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.OidFederationEventIds.INVALID_METADATA"/>
+                    </key>
+                    <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_metadata" c:_1="Metadata is invalid or not found for the requested entity types" c:_2="400" />
+                </entry>
             </map>
         </property>
     </bean>
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 526384c2..c30d807f 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
@@ -89,7 +89,16 @@
         p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
         p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
         p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
-        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"/>
+        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"
+        p:metadataValidationCondition-ref="#{'%{idp.oidfed.MetadataValidationCondition:DefaultMetadataValidationCondition}'.trim()}">
+        <property name="metadataLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultCombinedMetadataFromTrustChainLookupStrategy"
+                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+        </property>
+    </bean>
+
+    <bean id="DefaultMetadataValidationCondition"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultMetadataValidationCondition" />
 
     <bean id="DefaultMetadataPolicyEnforcer"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEnforcer"
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 c5424448..082d9956 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
@@ -41,7 +41,9 @@
         scope="prototype"
         p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.register.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
         p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.register.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
-        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}">
+        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"
+        p:metadataLookupStrategy-ref="DefaultCombinedMetadataFromTrustChainLookupStrategy"
+        p:metadataValidationCondition-ref="#{'%{idp.oidfed.MetadataValidationCondition:DefaultMetadataValidationCondition}'.trim()}">
         <property name="providedTrustChainValidationStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultProvidedTrustChainValidationStrategy"
                 p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}"
@@ -74,6 +76,13 @@
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.ExplicitClientRegistrationRequestTrustChainLookupFunction" />
         </property>
     </bean>
+
+    <bean id="DefaultMetadataValidationCondition"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultMetadataValidationCondition" />
+
+    <bean id="DefaultCombinedMetadataFromTrustChainLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultCombinedMetadataFromTrustChainLookupStrategy"
+        p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
  
     <bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
         scope="prototype"
@@ -83,7 +92,9 @@
         p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.register.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
         p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
         p:requireEntityConfigurationCondition-ref="shibboleth.Conditions.TRUE"
-        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}">
+        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"
+        p:metadataLookupStrategy-ref="DefaultCombinedMetadataFromTrustChainLookupStrategy"
+        p:metadataValidationCondition-ref="#{'%{idp.oidfed.MetadataValidationCondition:DefaultMetadataValidationCondition}'.trim()}">
         <property name="entityConfigurationLookupStrategy">
             <bean parent="shibboleth.Functions.Expression"
                 c:expression="#custom.apply(#input.ensureInboundMessageContext().getMessage().getEntityConfiguration(), null)">
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 dc13f569..f9606281 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
@@ -80,7 +80,16 @@
         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:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"/>
+        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"
+        p:metadataValidationCondition-ref="#{'%{idp.oidfed.MetadataValidationCondition:DefaultMetadataValidationCondition}'.trim()}">
+        <property name="metadataLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultCombinedMetadataFromTrustChainLookupStrategy"
+                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+        </property>
+    </bean>
+
+    <bean id="DefaultMetadataValidationCondition"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultMetadataValidationCondition" />
 
     <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 e15f8217..204714e0 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
@@ -64,7 +64,10 @@ import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.PlainJWT;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.SubjectType;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
 import net.minidev.json.JSONObject;
@@ -223,6 +226,41 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         return rpConfiguration.getSignedStatement().serialize();
     }
 
+    protected String opEntityConfiguration(final String issuer, final String... authorityHints)
+            throws URISyntaxException {
+        return opEntityConfiguration(issuer, emptyOpMetadata(issuer), authorityHints);
+    }
+
+    protected String opEntityConfiguration(final String issuer, final OIDCProviderMetadata metadata,
+            final String... authorityHints) throws URISyntaxException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(issuer).subject(issuer)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+                .claim("metadata", Map.of("openid_provider", metadata.toJSONObject()))
+                .claim("authority_hints", authorityHints == null || authorityHints.length == 0 ?
+                        new String[] { anchorId } : authorityHints)
+                .build();
+        final EntityStatement rpConfiguration =
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, leafKey, claimsSet);
+        return rpConfiguration.getSignedStatement().serialize();
+    }
+
+    protected String entityConfiguration(final String entityId, final Map<String, Object> metadata,
+            final String... authorityHints) {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+                .claim("metadata", metadata)
+                .claim("authority_hints", authorityHints == null || authorityHints.length == 0 ?
+                        new String[] { anchorId } : authorityHints)
+                .build();
+        final EntityStatement configuration =
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, leafKey, claimsSet);
+        return configuration.getSignedStatement().serialize();
+    }
+
     protected String trustedAnchorConfiguration() {
         final String anchorId = "https://trust-anchor.federation.local";
         final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(anchorId)
@@ -266,12 +304,12 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         return anchorConfiguration.getSignedStatement().serialize();
     }
 
-    protected String subordinateStatement(final String clientId) {
-        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(clientId)
+    protected String subordinateStatement(final String issuer, final Map<String, Object> metadata) {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(issuer)
                 .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", new OIDCClientMetadata().toJSONObject()))
+                .claim("metadata", metadata)
                 .claim("authority_hints", new String[] { anchorId })
                 .build();
         final EntityStatement rpConfiguration =
@@ -279,14 +317,14 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         return rpConfiguration.getSignedStatement().serialize();
     }
 
-    protected String subordinateStatement(final String issuer, final JWK issuerKey, final JWK subjetKey,
+    protected String rpSubordinateStatement(final String issuer, final JWK issuerKey, final JWK subjetKey,
             final String subjectId, final Map<String, Object> rpPolicy, final String... authorityHints) {
         final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(issuer).subject(subjectId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(subjetKey).toJSONObject(true))
                 .claim("metadata", Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
-                .claim("metadata_policy", Map.of("openid_relying_party", rpPolicy))
+                .claim("metadata_policy", rpPolicy)
                 .claim("authority_hints", authorityHints)
                 .build();
         final EntityStatement rpConfiguration =
@@ -302,7 +340,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(subjetKey).toJSONObject(true))
                 .claim("metadata", Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
-                .claim("metadata_policy", Map.of("openid_relying_party", rpPolicy))
+                .claim("metadata_policy", rpPolicy)
                 .claim("authority_hints", authorityHints)
                 .claim("constraints", constraints)
                 .build();
@@ -319,54 +357,68 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         return String.format(intermediateIdPattern, intermediateIndex.getAndIncrement());
     }
 
-    protected void configureMockHttpClient(final String clientId) {
+    protected void rpConfigureMockHttpClient(final String clientId) {
         try {
             mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId)));
             mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
             mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
-                    mockResponse(subordinateStatement(clientId)));
+                    mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
+                            new OIDCClientMetadata().toJSONObject()))));
         } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
             Assert.fail("Could not initialize mock HTTP client", e);
         }
     }
 
-    protected void configureMockHttpClient(final String clientId, final OIDCClientMetadata metadata) {
+    protected void rpConfigureMockHttpClient(final String clientId, final OIDCClientMetadata metadata) {
         try {
             mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId, metadata)));
             mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
             mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
-                    mockResponse(subordinateStatement(clientId)));
+                    mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
+                            new OIDCClientMetadata().toJSONObject()))));
+        } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+    }
+
+    protected void rpConfigureMockHttpClient(final String clientId, final JSONObject metadata) {
+        try {
+            mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId)));
+            mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+                    mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party", metadata))));
         } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
             Assert.fail("Could not initialize mock HTTP client", e);
         }
     }
 
-    protected void configureMockHttpClient(final String clientId, final String rpEntityConfiguration) {
+    protected void rpConfigureMockHttpClient(final String clientId, final String rpEntityConfiguration) {
         try {
             mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration));
             mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
             mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
-                    mockResponse(subordinateStatement(clientId)));
+                    mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
+                            new OIDCClientMetadata().toJSONObject()))));
         } catch (UnsupportedOperationException | IOException e) {
             Assert.fail("Could not initialize mock HTTP client", e);
         }
     }
 
-    protected void configureMockHttpClientWithAnchorConstraints(final String clientId,
+    protected void rpConfigureMockHttpClientWithAnchorConstraints(final String clientId,
             final Map<String,Object> constraints) {
         try {
             mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId)));
             mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
             mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
                     mockResponse(subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId,
-                            Collections.emptyMap(), constraints)));
+                            Map.of("openid_relying_party", Collections.emptyMap()), constraints)));
         } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
             Assert.fail("Could not initialize mock HTTP client", e);
         }
     }
 
     @SuppressWarnings("unchecked")
-    protected void configureMockHttpClient(final String clientId, final Map<String, Object> testVector) {
+    protected void rpConfigureMockHttpClient(final String clientId, final Map<String, Object> testVector) {
         final String intermediateId = uniqueIntermediateId();
         try {
             final Map<String, Object> vectorMetadata = (Map<String, Object>) testVector.get("metadata");
@@ -384,16 +436,82 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
             mapResponse(entityConfigurationUrl(intermediateId), mockResponse(intermediateConfiguration(intermediateId)));
             mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
             mapResponse(subordinateStatementUrl(anchorFetchEndpoint, intermediateId),
-                    mockResponse(subordinateStatement(anchorId, trustedAnchorKey, intermediateKey, intermediateId,
-                            (Map<String, Object>) testVector.get("TA"), anchorId)));
+                    mockResponse(rpSubordinateStatement(anchorId, trustedAnchorKey, intermediateKey, intermediateId,
+                            Map.of("openid_relying_party", (Map<String, Object>) testVector.get("TA")), anchorId)));
             mapResponse(subordinateStatementUrl(intermediateId + "/fetch", clientId),
-                    mockResponse(subordinateStatement(intermediateId, intermediateKey, rpKey, clientId,
-                            (Map<String, Object>) testVector.get("INT"), intermediateId)));
+                    mockResponse(rpSubordinateStatement(intermediateId, intermediateKey, rpKey, clientId,
+                            Map.of("openid_relying_party", (Map<String, Object>) testVector.get("INT")),
+                            intermediateId)));
         } catch (UnsupportedOperationException | IOException | URISyntaxException | ParseException e) {
             Assert.fail("Could not initialize mock HTTP client", e);
         }
     }
 
+    protected OIDCProviderMetadata emptyOpMetadata(final String issuer) {
+        return new OIDCProviderMetadata(new Issuer(issuer), List.of(SubjectType.PUBLIC),
+                URI.create("https://mock.example.org/jwks"));
+    }
+
+    protected void opConfigureMockHttpClient(final String issuer) {
+        try {
+            mapResponse(entityConfigurationUrl(issuer), mockResponse(opEntityConfiguration(issuer)));
+            mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, issuer),
+                    mockResponse(subordinateStatement(issuer,  Map.of("openid_provider",
+                            emptyOpMetadata(issuer).toJSONObject()))));
+        } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+    }
+
+    protected void opConfigureMockHttpClient(final String issuer, final OIDCProviderMetadata metadata) {
+        try {
+            mapResponse(entityConfigurationUrl(issuer), mockResponse(opEntityConfiguration(issuer, metadata)));
+            mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, issuer),
+                    mockResponse(subordinateStatement(issuer,  Map.of("openid_provider",
+                            metadata.toJSONObject()))));
+        } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+    }
+
+    protected void opConfigureMockHttpClient(final String issuer, final JSONObject metadata) {
+        try {
+            mapResponse(entityConfigurationUrl(issuer), mockResponse(opEntityConfiguration(issuer)));
+            mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, issuer),
+                    mockResponse(subordinateStatement(issuer,  Map.of("openid_provider", metadata))));
+        } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+    }
+
+    protected void opConfigureMockHttpClient(final String issuer, final String opEntityConfiguration) {
+        try {
+            mapResponse(entityConfigurationUrl(issuer), mockResponse(opEntityConfiguration));
+            mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, issuer),
+                    mockResponse(subordinateStatement(issuer,  Map.of("openid_provider",
+                            emptyOpMetadata(issuer).toJSONObject()))));
+        } catch (UnsupportedOperationException | IOException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+    }
+
+    protected void opConfigureMockHttpClientWithAnchorConstraints(final String issuer,
+            final Map<String,Object> constraints) {
+        try {
+            mapResponse(entityConfigurationUrl(issuer), mockResponse(opEntityConfiguration(issuer)));
+            mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, issuer),
+                    mockResponse(subordinateStatement(anchorId, trustedAnchorKey, leafKey, issuer,
+                            Map.of("openid_provider", Collections.emptyMap()), constraints)));
+        } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+    }
+
     protected List<Map<String, Object>> loadPolicyTestVectors() throws IOException {
         final ObjectMapper objectMapper = new ObjectMapper();
         final Resource file = new ClassPathResource(
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 dfba9965..1d627309 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
@@ -58,7 +58,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_noRequestObject()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result = launchAuthenticationRequest(clientId, "openid profile");
         Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
     }
@@ -67,7 +67,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_plainRequestObject()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", plainRequestObject(Map.of(
                         "iss", clientId,
@@ -84,7 +84,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_signedRequestObject()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
@@ -111,7 +111,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         final OIDCClientMetadata metadata = new OIDCClientMetadata();
         metadata.setRedirectionURI(new URI(redirectUri));
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
-        configureMockHttpClient(clientId, rpEntityConfigurationUnmatchingKey(clientId, metadata));
+        rpConfigureMockHttpClient(clientId, rpEntityConfigurationUnmatchingKey(clientId, metadata));
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
@@ -129,7 +129,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_signedRequestObject_nonMatchingClientId()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId + "2", "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
@@ -147,7 +147,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_signedRequestObject_missingJti()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
@@ -164,7 +164,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_signedRequestObject_missingExp()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
@@ -181,7 +181,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_signedRequestObject_invalidIss()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId + "2",
@@ -199,7 +199,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_signedRequestObject_missingIss()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "client_id", clientId,
@@ -216,7 +216,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_signedRequestObject_invalidClientId()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
@@ -234,7 +234,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_signedRequestObject_missingClientId()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
@@ -251,7 +251,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_signedRequestObject_containsForbiddenSub()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
@@ -270,7 +270,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_signedRequestObject_containsAdditionalAudience()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
@@ -288,7 +288,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_leafKeySignedRequestObject()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
@@ -303,7 +303,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithPar_unresolvableTrustChain()
             throws Exception {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final URI uri = createParGeneratedRequestUri(Map.of(
                 "client_id", clientId,
                 "response_type", "code",
@@ -317,7 +317,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithInvalidTrustChain_entityTypeConstraint_signedRequestObject()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClientWithAnchorConstraints(clientId, Map.of("allowed_entity_types", Collections.emptyList()));
+        rpConfigureMockHttpClientWithAnchorConstraints(clientId, Map.of("allowed_entity_types", Collections.emptyList()));
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
@@ -335,7 +335,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithInvalidTrustChain_namingConstraint_signedRequestObject()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClientWithAnchorConstraints(clientId, Map.of("naming_constraints",
+        rpConfigureMockHttpClientWithAnchorConstraints(clientId, Map.of("naming_constraints",
                 Map.of("permitted", List.of(".wrongfederation.local"))));
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
@@ -354,7 +354,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithValidTrustChain_withConstraints_signedRequestObject()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClientWithAnchorConstraints(clientId,
+        rpConfigureMockHttpClientWithAnchorConstraints(clientId,
                 Map.of("naming_constraints", Map.of("permitted", List.of(".federation.local")),
                         "allowed_entity_types", List.of("openid_relying_party")));
         final FlowExecutionResult result =
@@ -380,7 +380,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
     public void testWithPar_matchingAutoRegisteredTrustChain()
             throws IOException, UnsupportedOperationException, URISyntaxException {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final FlowExecutionResult result =
                 launchAuthenticationRequest(clientId, "openid profile", createParGeneratedRequestUri(Map.of(
                         "client_id", clientId,
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 e5a15c96..68a00117 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
@@ -58,7 +58,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
     @Test
     public void testSuccess() throws Exception {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final SignedJWT jwt = createPrivateKeyJWT(validClaimsSet(clientId, issuer),
                 rpKey.toRSAKey().toRSAPrivateKey(), JWSAlgorithm.RS512);
         final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
@@ -75,7 +75,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         final OIDCClientMetadata metadata = new OIDCClientMetadata();
         metadata.setRedirectionURI(new URI(redirectUri));
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
-        configureMockHttpClient(clientId, rpEntityConfigurationUnmatchingKey(clientId, metadata));
+        rpConfigureMockHttpClient(clientId, rpEntityConfigurationUnmatchingKey(clientId, metadata));
         final SignedJWT jwt = createPrivateKeyJWT(validClaimsSet(clientId, issuer),
                 rpKey.toRSAKey().toRSAPrivateKey(), JWSAlgorithm.RS512);
         final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
@@ -91,7 +91,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId, metadata);
+        rpConfigureMockHttpClient(clientId, metadata);
         setHttpFormRequest("POST", createRequestParameters(clientId));
         final FlowExecutionResult result =
                 flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
@@ -106,7 +106,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId, metadata);
+        rpConfigureMockHttpClient(clientId, metadata);
         setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
                 "client_id", clientId,
                 "iss", clientId,
@@ -132,7 +132,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId, metadata);
+        rpConfigureMockHttpClient(clientId, metadata);
         setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
                 "client_id", clientId,
                 "iss", clientId,
@@ -154,7 +154,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId, metadata);
+        rpConfigureMockHttpClient(clientId, metadata);
         setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
                 "client_id", clientId,
                 "aud", issuer,
@@ -176,7 +176,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId, metadata);
+        rpConfigureMockHttpClient(clientId, metadata);
         setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
                 "client_id", clientId,
                 "iss", clientId + "2",
@@ -199,7 +199,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId, metadata);
+        rpConfigureMockHttpClient(clientId, metadata);
         setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
                 "iss", clientId,
                 "aud", issuer,
@@ -221,7 +221,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId, metadata);
+        rpConfigureMockHttpClient(clientId, metadata);
         setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
                 "client_id", clientId + "2",
                 "iss", clientId,
@@ -244,7 +244,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId, metadata);
+        rpConfigureMockHttpClient(clientId, metadata);
         setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
                 "client_id", clientId,
                 "iss", clientId,
@@ -266,7 +266,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId, metadata);
+        rpConfigureMockHttpClient(clientId, metadata);
         setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
                 "client_id", clientId,
                 "sub", "mockValue",
@@ -290,7 +290,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId, metadata);
+        rpConfigureMockHttpClient(clientId, metadata);
         setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
                 "client_id", clientId,
                 "iss", clientId,
@@ -313,7 +313,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId, metadata);
+        rpConfigureMockHttpClient(clientId, metadata);
         setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", plainRequestObject(Map.of(
                 "client_id", clientId,
                 "iss", clientId,
@@ -335,7 +335,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
             initializeMocks();
             initializeThreadLocals();
             final String clientId = uniqueClientId();
-            configureMockHttpClient(clientId, vector);
+            rpConfigureMockHttpClient(clientId, vector);
             final SignedJWT jwt = createPrivateKeyJWT(validClaimsSet(clientId, issuer),
                     rpKey.toRSAKey().toRSAPrivateKey(), JWSAlgorithm.RS256);
             final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
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 e0717249..0e5e013c 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
@@ -78,7 +78,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testInvalidEntityConfiguration_wrongSignerKey() throws Exception {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final OIDCClientMetadata metadata = new OIDCClientMetadata();
         metadata.setRedirectionURI(new URI(redirectUri));
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
@@ -93,7 +93,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testValidEntityConfiguration_invalidType() throws Exception {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         setRequest("POST", rpEntityConfiguration(clientId), "application/json");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         assertErrorCode(result, "invalid_client_metadata");
@@ -102,7 +102,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testValidEntityConfiguration() throws Exception {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         setRequest("POST", rpEntityConfiguration(clientId), "application/entity-statement+jwt");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         verify(federationHttpClient, times(0)).executeOpen(any(),
@@ -113,7 +113,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testValidEntityConfiguration_customScope() throws Exception {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         final OIDCClientMetadata metadata = new OIDCClientMetadata();
         metadata.setRedirectionURI(new URI(redirectUri));
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
@@ -132,7 +132,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     public void testValidEntityConfiguration_repeat() throws Exception {
         final String clientId = uniqueClientId();
         for (int i = 0; i < 2; i++) {
-            configureMockHttpClient(clientId);
+            rpConfigureMockHttpClient(clientId);
             setRequest("POST", rpEntityConfiguration(clientId), "application/entity-statement+jwt");
             final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
             verify(federationHttpClient, times(0)).executeOpen(any(),
@@ -145,7 +145,8 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     public void testValidTrustChain() throws Exception {
         final String clientId = uniqueClientId();
         final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
-                subordinateStatement(clientId) + "\", \"" + trustedAnchorConfiguration() + "\"]";
+                subordinateStatement(clientId, Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
+                + "\", \"" + trustedAnchorConfiguration() + "\"]";
         setRequest("POST", trustChain, "application/trust-chain+json");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         verify(federationHttpClient, times(0)).executeOpen(any(),
@@ -255,12 +256,13 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testInvalidTrustChain_wrongRpEntityConfigurationSignerKey() throws Exception {
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(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() + "\"]";
+                subordinateStatement(clientId, Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
+                + "\", \"" + trustedAnchorConfiguration() + "\"]";
         setRequest("POST", trustChain, "application/trust-chain+json");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         verify(federationHttpClient, times(0)).executeOpen(any(),
@@ -279,7 +281,8 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         final Scope scope = Scope.parse("openid profile email custom");
         metadata.setScope(scope);
         final String trustChain = "[\"" + rpEntityConfiguration(clientId, metadata) + "\", \"" +
-                subordinateStatement(clientId) + "\", \"" + trustedAnchorConfiguration() + "\"]";
+                subordinateStatement(clientId, Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
+                + "\", \"" + trustedAnchorConfiguration() + "\"]";
         setRequest("POST", trustChain, "application/trust-chain+json");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         final OIDCClientMetadata providedMetadata = assertResponseStatement(
@@ -295,7 +298,8 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     public void testValidTrustChain_repeat() throws Exception {
         final String clientId = uniqueClientId();
         final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
-                subordinateStatement(clientId) + "\", \"" + trustedAnchorConfiguration() + "\"]";
+                subordinateStatement(clientId, Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
+                + "\", \"" + trustedAnchorConfiguration() + "\"]";
         for (int i = 0; i < 2; i++) {
             setRequest("POST", trustChain, "application/trust-chain+json");
             final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
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
index 2cb4669d..93f2cb0d 100644
--- 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
@@ -14,6 +14,9 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed;
 
+import java.util.List;
+import java.util.Map;
+
 import org.springframework.webflow.executor.FlowExecutionResult;
 import org.testng.Assert;
 import org.testng.annotations.Test;
@@ -24,6 +27,8 @@ import com.nimbusds.oauth2.sdk.Response;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
 import net.shibboleth.oidc.profile.messaging.JSONErrorResponse;
 
+import net.minidev.json.JSONObject;
+
 /**
  * Flow tests for the OpenID federation resolve entity flow.
  */
@@ -54,23 +59,58 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
     public void testUntrustedAnchor() throws Exception {
         request.setMethod("GET");
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         request.setQueryString("sub=" + clientId + "&trust_anchor=mockAnchors&entity_type=openid_relying_party");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         assertErrorCode(result, "invalid_trust_anchor");
     }
 
     @Test
-    public void testWithTrustedTrustAnchor() throws Exception {
+    public void testRPWithTrustedTrustAnchor() throws Exception {
         request.setMethod("GET");
         final String clientId = uniqueClientId();
-        configureMockHttpClient(clientId);
+        rpConfigureMockHttpClient(clientId);
         request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         final ResolveEntityResponse parsedResponse =
                 parseSuccessResponse(result, ResolveEntityResponse.class);
         final SignedJWT response = parsedResponse.getJWT();
         Assert.assertEquals(response.getJWTClaimsSet().getSubject(), clientId);
+        Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchorInvalidMetadata() throws Exception {
+        request.setMethod("GET");
+        final String clientId = uniqueClientId();
+        rpConfigureMockHttpClient(clientId, new JSONObject(Map.of("response_types", "invalid")));
+        request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_metadata");
+    }
+
+    @Test
+    public void testOPWithTrustedTrustAnchor() throws Exception {
+        request.setMethod("GET");
+        final String entityId = uniqueClientId();
+        opConfigureMockHttpClient(entityId);
+        request.setQueryString("sub=" + entityId + "&trust_anchor=" + anchorId + "&entity_type=openid_provider");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final ResolveEntityResponse parsedResponse =
+                parseSuccessResponse(result, ResolveEntityResponse.class);
+        final SignedJWT response = parsedResponse.getJWT();
+        Assert.assertEquals(response.getJWTClaimsSet().getSubject(), entityId);
+        Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+    }
+
+    @Test
+    public void testOPWithTrustedTrustAnchorInvalidMetadata() throws Exception {
+        request.setMethod("GET");
+        final String entityId = uniqueClientId();
+        opConfigureMockHttpClient(entityId, new JSONObject(Map.of("issuer", List.of("unexpected", "values"))));
+        request.setQueryString("sub=" + entityId + "&trust_anchor=" + anchorId + "&entity_type=openid_provider");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_metadata");
     }
 
     protected JSONErrorResponse parseErrorResponse(final FlowExecutionResult result) {

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


More information about the commits mailing list