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

Henri Mikkonen henri.mikkonen at iki.fi
Fri May 9 13:10:20 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=a035117c0077e08064858acdde9be4d8bc490587

commit a035117c0077e08064858acdde9be4d8bc490587
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri May 9 16:09:49 2025 +0300

    JOIDC-222 - Support for OpenID Federation
    
    https://shibboleth.atlassian.net/browse/JOIDC-222
    
    - Wired the updated metadata policy logic into the automatic and explicit registration, and resolve-entity
    - Wired Connect2Id's metadata policy test vectors to the PAR flow tests
---
 .../oidfed/profile/impl/OidFederationEventIds.java |  10 ++
 .../op/oidfed/profile/impl/ResolveTrustChains.java |  28 ++++--
 ...ultTrustChainMetadataPolicyMergingStrategy.java |  74 +++++++++-----
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  47 +++++++++
 .../oidc/metadata-lookup/metadata-lookup-beans.xml |  11 ++-
 .../idp/flows/oidfed/register/register-beans.xml   |  21 ++--
 .../oidfed/resolve-entity/resolve-entity-beans.xml |  23 +++--
 .../oidc/op/profile/flow/AbstractOidcFlowTest.java |  26 ++++-
 .../flow/oidfed/AbstractFederationFlowTest.java    | 107 +++++++++++++++++++--
 ...shedAuthorizeFlowAutomaticRegistrationTest.java |  37 ++++++-
 10 files changed, 324 insertions(+), 60 deletions(-)

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 a23864ce..89551686 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
@@ -43,4 +43,14 @@ public class OidFederationEventIds {
      * ID of event returned if the given subject is invalid.
      */
     @Nonnull @NotEmpty public static final String INVALID_SUBJECT = "InvalidSubject";
+
+    /**
+     * ID of event returned if the given trust anchor is invalid.
+     */
+    @Nonnull @NotEmpty public static final String INVALID_METADATA_POLICY = "InvalidMetadataPolicy";
+
+    /**
+     * ID of event returned if the given trust anchor is invalid.
+     */
+    @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/ResolveTrustChains.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java
index a9eacb53..ae3bd565 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
@@ -53,6 +53,7 @@ import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.collection.Pair;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.resolver.CriteriaSet;
 
@@ -258,20 +259,27 @@ public class ResolveTrustChains extends AbstractProfileAction {
         trustChainContext.setResolvedTrustChains(cacheResult.get(0));
         final List<Pair<List<EntityStatement>, OIDCClientInformation>> policyCompliantChains = new ArrayList<>();
 
+        String errorEventId = null;
         for (final List<EntityStatement> chain : cacheResult.get(0)) {
             if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
                 log.debug("{} Ignored resolved trust chain that doesn't match with preselected chain", getLogPrefix());
                 continue;
             }
-            final Map<String, MetadataPolicy> mergedPolicies =
-                    metadataPolicyMergingStrategy.apply(chain, EntityType.OPENID_RELYING_PARTY.getValue());
-            log.debug("{} Merged policy for chain {}", getLogPrefix(), mergedPolicies);
+            final Map<String, MetadataPolicy> mergedPolicies;
+            try {
+                mergedPolicies = metadataPolicyMergingStrategy.apply(chain,
+                        EntityType.OPENID_RELYING_PARTY.getValue());
+                log.debug("{} Merged policy for chain {}", getLogPrefix(), mergedPolicies);
+            } catch (final ConstraintViolationException e) {
+                log.warn("{} Could not merge metadata policies", getLogPrefix(), e);
+                errorEventId = OidFederationEventIds.INVALID_METADATA_POLICY;
+                continue;
+            }
             assert chain != null;
             final OIDCClientMetadata metadata = metadataLookupStrategy.apply(chain);
             if (metadata != null) {
                 final OIDCClientInformation clientInformation = new OIDCClientInformation(
                         new ClientID(chain.get(0).getEntityID().getValue()), metadata);
-                boolean compliant = true;
                 final JSONObject requestMetadata = clientInformation.toJSONObject();
                 for (final String claim : mergedPolicies.keySet()) {
                     final MetadataPolicy policy = mergedPolicies.get(claim);
@@ -282,7 +290,7 @@ public class ResolveTrustChains extends AbstractProfileAction {
                     final Boolean enforcerResult = mergeResult != null ? mergeResult.getSecond() : null;
                     if (enforcerResult == null || !enforcerResult.booleanValue()) {
                         log.warn("{} Metadata claim {} is not compliant with the policy", getLogPrefix(), claim);
-                        compliant = false;
+                        errorEventId = OidFederationEventIds.INVALID_METADATA_AGAINST_POLICY;
                     } else {
                         log.trace("{} Validation result is OK for claim {}", getLogPrefix(), claim);
                         final Object enforcedValue = mergeResult != null ? mergeResult.getFirst() : null;
@@ -290,7 +298,7 @@ public class ResolveTrustChains extends AbstractProfileAction {
                     }
                 }
 
-                if (!compliant) {
+                if (errorEventId != null) {
                     log.warn("{} The requested metadata is not compliant with the policy", getLogPrefix());
                 } else {
                     log.debug("{} The requested metadata is compliant with the policy", getLogPrefix());
@@ -306,6 +314,14 @@ public class ResolveTrustChains extends AbstractProfileAction {
             }
         }
 
+        if (policyCompliantChains.isEmpty()) {
+            if (errorEventId == null) {
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            } else {
+                ActionSupport.buildEvent(profileRequestContext, errorEventId);
+            }
+            return;
+        }
         log.debug("{} Setting the policy compliant trust chains into the context: {}", getLogPrefix(),
                 policyCompliantChains);
         trustChainContext.setPolicyCompliantTrustChains(policyCompliantChains);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java
index 513e307a..3477420d 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java
@@ -26,18 +26,17 @@ import javax.annotation.Nullable;
 import org.slf4j.Logger;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.MapType;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityType;
-import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyViolationException;
 
-import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultMetadataPolicyMergingStrategy;
 import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.collection.Pair;
 import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.ConstraintViolationException;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -54,19 +53,15 @@ public class DefaultTrustChainMetadataPolicyMergingStrategy extends AbstractIden
     @Nonnull private Logger log = LoggerFactory.getLogger(DefaultTrustChainMetadataPolicyMergingStrategy.class);
 
     /** The strategy used for merging two metadata policies. */
-    @Nonnull private BiFunction<Map<String, MetadataPolicy>, Map<String, MetadataPolicy>,
+    @NonnullAfterInit private BiFunction<Map<String, MetadataPolicy>, Map<String, MetadataPolicy>,
         Pair<Map<String, MetadataPolicy>, Boolean>> metadataPolicyMergingStrategy;
 
     /** The strategy used for local (additional) metadata policy. */
     @NonnullAfterInit private Function<List<EntityStatement>, Map<String, MetadataPolicy>>
         localMetadataPolicyStrategy;
 
-    /**
-     * Constructor.
-     */
-    public DefaultTrustChainMetadataPolicyMergingStrategy() {
-        metadataPolicyMergingStrategy = new DefaultMetadataPolicyMergingStrategy();
-    }
+    /** Object mapper used for deserializing metadata policies. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
 
     /**
      * Set the strategy used for merging two metadata policies.
@@ -92,6 +87,31 @@ public class DefaultTrustChainMetadataPolicyMergingStrategy extends AbstractIden
                 "Local metadata policy strategy cannot be null");
     }
 
+    /**
+     * Set the object mapper used for deserializing metadata policies
+     * 
+     * @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 (metadataPolicyMergingStrategy == null) {
+            throw new ComponentInitializationException("Metadata policy merging strategy cannot be null");
+        }
+        if (localMetadataPolicyStrategy == null) {
+            throw new ComponentInitializationException("Local metadata policy strategy cannot be null");
+        }
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+    }
+
     /** {@inheritDoc} */
     @Override @Nonnull
     public Map<String, MetadataPolicy> apply(@Nullable final List<EntityStatement> trustChain,
@@ -153,21 +173,27 @@ public class DefaultTrustChainMetadataPolicyMergingStrategy extends AbstractIden
      * @throws ConstraintViolationException if the map of policies could not be parsed from the entity statement
      */
     @Nullable
-    protected Map<String, MetadataPolicy> parseMetadataPolicy(@Nonnull final EntityStatement entityStatement,
+    protected Map<String, MetadataPolicy> parseMetadataPolicy(
+            @Nonnull final EntityStatement entityStatement,
             @Nonnull final String entityType) throws ConstraintViolationException {
-        final ObjectMapper objectMapper = new ObjectMapper();
-        final TypeReference<HashMap<String,MetadataPolicy>> typeRef =
-                new TypeReference<HashMap<String,MetadataPolicy>>() {};
-        final EntityType type = new EntityType(entityType);
-        try {
-            if (entityStatement.getClaimsSet().getMetadataPolicy(type) != null) {
-                return objectMapper.readValue(
-                        entityStatement.getClaimsSet().getMetadataPolicy(type).toJSONString(), typeRef);
+        final Object metadataPolicyClaim =
+                entityStatement.getClaimsSet().getClaim("metadata_policy");
+        if (metadataPolicyClaim != null) {
+            final JavaType stringType = objectMapper.constructType(String.class);
+            final JavaType metadataPolicyType = objectMapper.constructType(MetadataPolicy.class);
+            final MapType metadataPolicyMapType =
+                    objectMapper.getTypeFactory().constructMapType(Map.class, stringType, metadataPolicyType);
+            final MapType metadataPolicyByEntityTypeMapType =
+                    objectMapper.getTypeFactory().constructMapType(Map.class, stringType, metadataPolicyMapType);
+            try {
+                final Map<String, Map<String, MetadataPolicy>> result =
+                        objectMapper.readValue(metadataPolicyClaim.toString(), metadataPolicyByEntityTypeMapType);
+                if (result != null) {
+                    return result.get(entityType);
+                }
+            } catch (final JsonProcessingException e) {
+                log.warn("Could not parse trust mark issuers from the trust chain", e);
             }
-        } catch (final JsonProcessingException | PolicyViolationException e) {
-            log.debug("Could not parse metadata policy of type {} from the claims set", entityType, e);
-            throw new ConstraintViolationException("Could not parse metadata policy of type " + entityType +
-                    " from the claims set");
         }
         return null;
     }
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 add39a80..49a4c1ce 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
@@ -286,6 +286,41 @@
         </property>
     </bean>
 
+    <bean id="shibboleth.oidfed.policy.JSONObjectMapper" parent="shibboleth.oidc.JSONObjectMapper" />
+
+    <bean id="shibboleth.oidfed.policy.JSONSimpleModule" class="com.fasterxml.jackson.databind.module.SimpleModule"/>
+
+    <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
+        <property name="targetObject" ref="shibboleth.oidfed.policy.JSONSimpleModule" />
+        <property name="targetMethod" value="addDeserializer" />
+        <property name="arguments">
+            <list>
+                <value>#{ T(net.shibboleth.oidc.metadata.policy.MetadataPolicy)}</value>
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.FederationMetadataPolicyDeserializer"/>
+            </list>
+        </property>
+    </bean>
+
+    <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
+        <property name="targetObject" ref="shibboleth.oidfed.policy.JSONObjectMapper" />
+        <property name="targetMethod" value="registerModule" />
+        <property name="arguments">
+            <list>
+                <ref bean="shibboleth.oidfed.policy.JSONSimpleModule" />
+            </list>
+        </property>
+    </bean>
+
+    <util:list id="shibboleth.oidfed.StandardMetadataPolicyOperators">
+        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyValueOperator"/>
+        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyAddOperator"/>
+        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyDefaultOperator"/>
+        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyOneOfOperator"/>
+        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicySubsetOfOperator"/>
+        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicySupersetOfOperator"/>
+        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEssentialOperator"/>
+    </util:list>
+
     <!-- Used in various places as a default for issuer. -->
     <bean id="shibboleth.oidc.issuer" class="java.lang.String" c:_0="%{idp.oidc.issuer:%{idp.entityID}}" />
 
@@ -703,6 +738,18 @@
                 <!-- Missing from Nimbus. -->
                 <entry key="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_TARGET}"
                     value="#{T(net.shibboleth.oidc.profile.core.OidcError).INVALID_TARGET}" />
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.OidFederationEventIds.INVALID_METADATA_POLICY"/>
+                    </key>
+                    <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_metadata" c:_1="Merged metadata policy is invalid" c:_2="400" />
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.OidFederationEventIds.INVALID_METADATA_AGAINST_POLICY"/>
+                    </key>
+                    <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_metadata" c:_1="Requested metadata is not compliant with the merged policy" 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 29e7166a..b1755a2f 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
@@ -91,11 +91,18 @@
         p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"/>
 
     <bean id="DefaultMetadataPolicyEnforcer"
-        class="net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyEnforcer" />
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEnforcer"
+        p:metadataPolicyOperators-ref="#{'%{idp.oidfed.authorize.MetadataPolicyOperatorsy:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
 
     <bean id="DefaultTrustChainMetadataPolicyMergingStrategy" 
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
-        p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.authorize.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"/>
+        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.MetadataPolicMergingyStrategy:MetadataPolicMergingyStrategy}'.trim()}"
+        p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.authorize.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"
+        p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+
+    <bean id="MetadataPolicMergingyStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyMergingStrategy"
+        p:metadataPolicyOperators-ref="#{'%{idp.oidfed.authorize.MetadataPolicyOperatorsy:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
 
     <bean id="DefaultLocalMetadataPolicyStrategy"
         parent="shibboleth.Functions.Constant">
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 6ba028da..a977b87e 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
@@ -74,18 +74,25 @@
  
     <bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
         scope="prototype"
-        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:trustChainCache-ref="#{'%{idp.oidfed.register.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
         p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
-        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
-        p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
+        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.register.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
+        p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.register.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
         p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"/>
 
     <bean id="DefaultMetadataPolicyEnforcer"
-        class="net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyEnforcer" />
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEnforcer"
+        p:metadataPolicyOperators-ref="#{'%{idp.oidfed.register.MetadataPolicyOperatorsy:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
 
     <bean id="DefaultTrustChainMetadataPolicyMergingStrategy" 
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
-        p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.authorize.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"/>
+        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.register.MetadataPolicMergingyStrategy:MetadataPolicMergingyStrategy}'.trim()}"
+        p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.register.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"
+        p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+
+    <bean id="MetadataPolicMergingyStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyMergingStrategy"
+        p:metadataPolicyOperators-ref="#{'%{idp.oidfed.register.MetadataPolicyOperatorsy:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
 
     <bean id="DefaultLocalMetadataPolicyStrategy"
         parent="shibboleth.Functions.Constant">
@@ -112,7 +119,7 @@
 
     <bean id="ResolveTrustMarks" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustMarks"
         scope="prototype"
-        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:trustChainCache-ref="#{'%{idp.oidfed.register.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
         p:trustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.TrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}"
         p:delegatedTrustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.DelegatedTrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}">
         <property name="trustEngine">
@@ -162,7 +169,7 @@
         scope="prototype">
         <property name="localMetadataPolicyMergingStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultLocalMetadataPolicyMergingStrategy"
-                p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"/>
+                p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.register.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"/>
         </property>
         <property name="mandatoryTrustMarksLookupStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.MandatoryTrustMarksLookupFunction"
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 ae78b670..74acd075 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
@@ -45,8 +45,8 @@
 
     <bean id="shibboleth.oidfed.ResolveEntityResponseMetadataCacheBuilderSpec"
         class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
-        p:minCacheDuration="%{idp.oidfed.resolveEntity.maxRefreshDelay:PT1S}"
-        p:maxCacheDuration="%{idp.oidfed.resolveEntity.maxRefreshDelay:PT30S}">
+        p:minCacheDuration="%{idp.oidfed.resolve-entity.maxRefreshDelay:PT1S}"
+        p:maxCacheDuration="%{idp.oidfed.resolve-entity.maxRefreshDelay:PT30S}">
         <property name="criteriaToIdentifierStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityRequestCriteriaToIdentifierStrategy" />
         </property>
@@ -75,18 +75,25 @@
 
     <bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
         scope="prototype"
-        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:trustChainCache-ref="#{'%{idp.oidfed.resolve-entity.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
         p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
-        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
-        p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
+        p: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')}"/>
 
     <bean id="DefaultMetadataPolicyEnforcer"
-        class="net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyEnforcer" />
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEnforcer"
+        p:metadataPolicyOperators-ref="#{'%{idp.oidfed.resolve-entity.MetadataPolicyOperatorsy:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
 
     <bean id="DefaultTrustChainMetadataPolicyMergingStrategy" 
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
-        p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.resolve.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"/>
+        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.resolve-entity.MetadataPolicMergingyStrategy:MetadataPolicMergingyStrategy}'.trim()}"
+        p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.resolve-entity.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"
+        p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+
+    <bean id="MetadataPolicMergingyStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyMergingStrategy"
+        p:metadataPolicyOperators-ref="#{'%{idp.oidfed.resolve-entity.MetadataPolicyOperatorsy:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
 
     <bean id="DefaultLocalMetadataPolicyStrategy"
         parent="shibboleth.Functions.Constant">
@@ -109,7 +116,7 @@
 
     <bean id="ResolveTrustMarks" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustMarks"
         scope="prototype"
-        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:trustChainCache-ref="#{'%{idp.oidfed.resolve-entity.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
         p:trustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.TrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}"
         p:delegatedTrustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.DelegatedTrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}">
         <property name="trustEngine">
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
index 6d35d051..af15caf3 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
@@ -213,7 +213,14 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
         Assert.assertTrue(response instanceof ErrorResponse);
         return (ErrorResponse) response;
     }
-    
+
+    protected ErrorResponse parseErrorResponse(final FlowExecutionResult result, final String message) {
+        final Response response = parseResponse(result);
+        Assert.assertFalse(response.indicatesSuccess(), message);
+        Assert.assertTrue(response instanceof ErrorResponse, message);
+        return (ErrorResponse) response;
+    }
+
     protected <AResponseType extends Response> AResponseType parseSuccessResponse(final FlowExecutionResult result,
             final Class<AResponseType> clazz) {
         final Response response = parseResponse(result);
@@ -229,10 +236,21 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
         Assert.assertEquals(errorResponse.getErrorObject().getCode(), errorCode);
     }
 
+    protected void assertErrorCode(final FlowExecutionResult result, final String errorCode,
+            final String message) {
+        final ErrorResponse errorResponse = parseErrorResponse(result, message);
+        Assert.assertEquals(errorResponse.getErrorObject().getCode(), errorCode, message);
+    }
+
     protected void assertErrorDescriptionContains(final FlowExecutionResult result, final String errorDescription) {
-        final ErrorResponse errorResponse = parseErrorResponse(result);
-        Assert.assertNotNull(errorResponse.getErrorObject().getDescription());
-        Assert.assertTrue(errorResponse.getErrorObject().getDescription().contains(errorDescription));
+        assertErrorDescriptionContains(result, errorDescription, null);
+    }
+
+    protected void assertErrorDescriptionContains(final FlowExecutionResult result, final String errorDescription,
+            final String message) {
+        final ErrorResponse errorResponse = parseErrorResponse(result, message);
+        Assert.assertNotNull(errorResponse.getErrorObject().getDescription(), message);
+        Assert.assertTrue(errorResponse.getErrorObject().getDescription().contains(errorDescription), message);
     }
     
     protected void setJsonRequest(final String method, final String body) {
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 bba99f8d..35d197f5 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
@@ -23,11 +23,14 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.charset.Charset;
 import java.security.KeyPair;
 import java.security.NoSuchAlgorithmException;
 import java.security.interfaces.RSAPublicKey;
 import java.time.Instant;
 import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -40,9 +43,14 @@ import org.apache.hc.core5.http.HttpEntity;
 import org.mockito.ArgumentMatcher;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.JWSHeader;
@@ -54,9 +62,11 @@ import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.PlainJWT;
 import com.nimbusds.jwt.SignedJWT;
+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.idp.plugin.oidc.op.oidfed.TrustChainTestUtil;
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.AbstractOidcFlowTest;
 import net.shibboleth.oidc.security.credential.BasicJWKCredential;
@@ -68,8 +78,10 @@ import net.shibboleth.shared.logic.Constraint;
 public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
 
     final static AtomicInteger clientIndex = new AtomicInteger();
+    final static AtomicInteger intermediateIndex = new AtomicInteger();
     final String redirectUri = "https://rp.federation.local/cb";
     final String clientIdPattern = "https://testrp%s.federation.local";
+    final String intermediateIdPattern = "https://intermediate-authority%s.federation.local";
     final String anchorId = "https://trust-anchor.federation.local";
     final String anchorFetchEndpoint = anchorId + "/fetch";
     String issuer = "https://op.example.org";
@@ -78,6 +90,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     JWK leafKey;
     JWK anchorKey;
     JWK trustedAnchorKey;
+    JWK intermediateKey;
 
     @Autowired
     @Qualifier("shibboleth.oidfed.HttpClient")
@@ -97,6 +110,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 .privateKey(localAnchor.getPrivateKey())
                 .keyID("locallyTrustedAnchorKey")
                 .build();
+        intermediateKey = initializeNewJwk("RSA", 2048, "mockIntermediateKey");
     }
 
     protected JWK initializeNewJwk(final String algorithm, final int size, final String kid)
@@ -170,21 +184,23 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         return classicResponse;
     }
 
-    protected String rpEntityConfiguration(final String clientId) throws URISyntaxException {
+    protected String rpEntityConfiguration(final String clientId, final String... authorityHints)
+            throws URISyntaxException {
         final OIDCClientMetadata metadata = new OIDCClientMetadata();
         metadata.setRedirectionURI(new URI(redirectUri));
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
-        return rpEntityConfiguration(clientId, metadata);
+        return rpEntityConfiguration(clientId, metadata, authorityHints);
     }
 
-    protected String rpEntityConfiguration(final String clientId, final OIDCClientMetadata metadata)
-            throws URISyntaxException {
+    protected String rpEntityConfiguration(final String clientId, final OIDCClientMetadata metadata,
+            final String... authorityHints) throws URISyntaxException {
         final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(clientId).subject(clientId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("openid_relying_party", metadata.toJSONObject()))
-                .claim("authority_hints", new String[] { anchorId })
+                .claim("authority_hints", authorityHints == null || authorityHints.length == 0 ?
+                        new String[] { anchorId } : authorityHints)
                 .build();
         final EntityStatement rpConfiguration =
                 TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, leafKey, claimsSet);
@@ -205,6 +221,20 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         return anchorConfiguration.getSignedStatement().serialize();
     }
 
+    protected String intermediateConfiguration(final String intermediateId) {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(intermediateId).subject(intermediateId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(intermediateKey).toJSONObject(true))
+                .claim("metadata", Map.of("federation_entity", Map.of("federation_fetch_endpoint",
+                        intermediateId + "/fetch")))
+                .claim("authority_hints", new String[] { anchorId })
+                .build();
+        final EntityStatement anchorConfiguration =
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, intermediateKey, claimsSet);
+        return anchorConfiguration.getSignedStatement().serialize();
+    }
+
     protected String subordinateStatement(final String clientId) {
         final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(clientId)
                 .issueTime(Date.from(Instant.now()))
@@ -217,11 +247,29 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet);
         return rpConfiguration.getSignedStatement().serialize();
     }
-    
+
+    protected String subordinateStatement(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("authority_hints", authorityHints)
+                .build();
+        final EntityStatement rpConfiguration =
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, issuerKey, claimsSet);
+        return rpConfiguration.getSignedStatement().serialize();
+    }
     protected String uniqueClientId() {
         return String.format(clientIdPattern, clientIndex.getAndIncrement());
     }
 
+    protected String uniqueIntermediateId() {
+        return String.format(intermediateIdPattern, intermediateIndex.getAndIncrement());
+    }
+
     protected void configureMockHttpClient(final String clientId) {
         try {
             mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId)));
@@ -244,6 +292,53 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         }
     }
 
+    @SuppressWarnings("unchecked")
+    protected void configureMockHttpClient(final String clientId, final Map<String, Object> testVector) {
+        final String intermediateId = uniqueIntermediateId();
+        try {
+            final Map<String, Object> vectorMetadata = (Map<String, Object>) testVector.get("metadata");
+            if (vectorMetadata.isEmpty()) {
+                mapResponse(entityConfigurationUrl(clientId), mockResponse(
+                        rpEntityConfiguration(clientId, intermediateId)));
+            } else {
+                final Map<String, Object> metadata = new HashMap<>(vectorMetadata);
+                metadata.put("redirect_uris", List.of(redirectUri));
+                metadata.put("jwks", new JWKSet(rpKey.toPublicJWK()).toJSONObject());
+                mapResponse(entityConfigurationUrl(clientId),
+                        mockResponse(rpEntityConfiguration(clientId,
+                                OIDCClientMetadata.parse(new JSONObject(metadata)), intermediateId)));
+            }
+            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)));
+            mapResponse(subordinateStatementUrl(intermediateId + "/fetch", clientId),
+                    mockResponse(subordinateStatement(intermediateId, intermediateKey, rpKey, clientId,
+                            (Map<String, Object>) testVector.get("INT"), intermediateId)));
+        } catch (UnsupportedOperationException | IOException | URISyntaxException | ParseException 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(
+                "/net/shibboleth/idp/oidc/metadata/impl/metadata-policy-test-vectors-2025-02-13.json");
+
+        final Charset utf8 = Charset.forName("UTF-8");
+        assert utf8 != null;
+        try {
+            final List<Map<String, Object>> policies =
+                    objectMapper.readValue(file.getContentAsString(utf8),
+                            new TypeReference<List<Map<String, Object>>>(){});
+            Assert.assertNotNull(policies);
+            return policies;
+        } catch (final JsonProcessingException e) {
+            throw new IOException("Could not parse JSON from the input", e);
+        }
+    }
+
     protected class RequestUriMatcher implements ArgumentMatcher<ClassicHttpRequest> {
 
         @Nonnull private final String uri;
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 6747f192..a210a44a 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
@@ -314,6 +314,33 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         assertErrorCode(result, OAuth2Error.INVALID_CLIENT_CODE);
     }
 
+    @Test
+    public void testWithTestVectors() throws Exception {
+        final List<Map<String, Object>> vectors = loadPolicyTestVectors();
+        for (final Map<String, Object> vector : vectors) {
+            initializeMocks();
+            initializeThreadLocals();
+            final String clientId = uniqueClientId();
+            configureMockHttpClient(clientId, vector);
+            final SignedJWT jwt = createPrivateKeyJWT(validClaimsSet(clientId, issuer),
+                    rpKey.toRSAKey().toRSAPrivateKey(), JWSAlgorithm.RS256);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.PRIVATE_KEY_JWT, rpKey.toRSAKey().toPublicKey());
+            final String ERROR_MESSAGE = "Unexpected result with test vector " + vector.get("n");
+            if (vector.get("error") instanceof String error) {
+                assertErrorCode(result, "invalid_metadata", ERROR_MESSAGE);
+                if ("invalid_policy".equals(error)) {
+                    assertErrorDescriptionContains(result, "Merged metadata policy is invalid", ERROR_MESSAGE);
+                } else {
+                    assertErrorDescriptionContains(result, "Requested metadata is not compliant with the merged policy",
+                            ERROR_MESSAGE);
+                }
+            } else {
+                assertSuccessResponse(result, clientId, ERROR_MESSAGE);
+            }
+        }
+    }
+
     protected void verifyAuthorizeEndpoint(final String clientId, final String requestUri) {
         verifyAuthorizeEndpoint(clientId, requestUri, null);
     }
@@ -408,11 +435,15 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
     }
 
     protected void assertSuccessResponse(final FlowExecutionResult result, final String id) {
+        assertSuccessResponse(result, id, null);
+    }
+
+    protected void assertSuccessResponse(final FlowExecutionResult result, final String id, final String message) {
         final PushedAuthorizationSuccessResponse resp =
                 parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
-        Assert.assertNotNull(resp);
-        Assert.assertNotNull(resp.getRequestURI());
-        Assert.assertNotNull(resp.getLifetime());
+        Assert.assertNotNull(resp, message);
+        Assert.assertNotNull(resp.getRequestURI(), message);
+        Assert.assertNotNull(resp.getLifetime(), message);
     }
 
 }

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


More information about the commits mailing list