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

Henri Mikkonen henri.mikkonen at iki.fi
Thu May 22 12:34:44 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=ca56d2cab069f58c1a56a78ea8bdbabbd951bb1f

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

commit ca56d2cab069f58c1a56a78ea8bdbabbd951bb1f
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu May 22 15:33:35 2025 +0300

    JOIDC-222 - Support for OpenID Federation
    
    https://shibboleth.atlassian.net/browse/JOIDC-222
    
    Initial implementation for federation policy constraints
    - The default map (shibboleth.oidfed.DefaultFederationPolicyConstraints) implements the three constraints specified in the current draft
      - max_path_length: DefaultMaxPathLengthConstraint
      - naming_constraints: DefaultNamingConstraintsConstraint (exploits BouncyCastle)
      - allowed_entity_types: DefaultAllowedEntityTypesConstraint
    - The map is configurable via idp.oidfed.FederationPolicyConstraints -property
    - Initial flow tests, to be improved, also proper unit tests TODO
---
 ...efaultProvidedTrustChainValidationStrategy.java |  47 ++++++++
 .../DefaultTrustChainFetchingStrategy.java         |  43 +++++++-
 .../op/oidfed/metadata/EntityStatementHelper.java  |  83 +++++++++++++++
 .../constraints/FederationPolicyConstraint.java    |  37 +++++++
 .../FederationPolicyConstraintHelper.java          |  73 +++++++++++++
 .../impl/AbstractFederationPolicyConstraint.java   |  79 ++++++++++++++
 .../impl/DefaultAllowedEntityTypesConstraint.java  | 106 ++++++++++++++++++
 .../impl/DefaultMaxPathLengthConstraint.java       |  66 ++++++++++++
 .../impl/DefaultNamingConstraintsConstraint.java   | 118 +++++++++++++++++++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  18 +++-
 .../idp/flows/oidfed/register/register-beans.xml   |   8 +-
 .../flow/oidfed/AbstractFederationFlowTest.java    |  47 ++++++++
 .../AuthorizeFlowAutomaticRegistrationTest.java    |  64 +++++++++++
 .../profile/flow/oidfed/RegistrationFlowTest.java  |  99 +++++++++++++++++
 14 files changed, 883 insertions(+), 5 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
index 65c8eb0d..7e348ac6 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
 
 import java.util.List;
+import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.BiPredicate;
 
@@ -24,8 +25,11 @@ import javax.annotation.Nullable;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraintHelper;
 import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.component.ComponentInitializationException;
@@ -48,6 +52,12 @@ public class DefaultProvidedTrustChainValidationStrategy
     @NonnullAfterInit BiFunction<EntityStatement, MetadataFilterContext, EntityStatement>
         trustAnchorSignatureValidationFilterStrategy;
 
+    /** Map of supported federation policy constraints. */
+    @NonnullAfterInit private Map<String, FederationPolicyConstraint> federationPolicyConstraints;
+
+    /** JSON object mapper used for decoding JSON into Map. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
     /**
      * Set the strategy for validating trust anchor's entity configuration signature.
      * 
@@ -60,6 +70,27 @@ public class DefaultProvidedTrustChainValidationStrategy
                 "TrustAnchorSignatureValidationFilterStrategy cannot be null");
     }
 
+    /**
+     * Set the map of supported federation policy constraints.
+     * 
+     * @param constraints map of supported federation policy constraints.
+     */
+    public void setfederationPolicyConstraints(@Nonnull final Map<String, FederationPolicyConstraint> constraints) {
+        checkSetterPreconditions();
+        federationPolicyConstraints = Constraint.isNotNull(constraints, "Map of policy constraints cannot be null");
+    }
+
+    /**
+     * Set the JSON {@link ObjectMapper} used for decoding JSON into Map.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -67,12 +98,19 @@ public class DefaultProvidedTrustChainValidationStrategy
         if (trustAnchorSignatureValidationFilterStrategy == null) {
             throw new ComponentInitializationException("TrustAnchorSignatureValidationFilterStrategy cannot be null");
         }
+        if (federationPolicyConstraints == null) {
+            throw new ComponentInitializationException("Map of policy constraints cannot be null");
+        }
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
     }
 
     /** {@inheritDoc} */
     @Override
     public boolean test(@Nullable final ProfileRequestContext profileRequestContext,
             @Nullable final List<EntityStatement> trustChain) {
+        checkComponentActive();
         if (trustChain == null || trustChain.size() < 3 || trustChain.contains(null)) {
             log.error("No satisfactory trust chain provided");
             return false;
@@ -100,6 +138,15 @@ public class DefaultProvidedTrustChainValidationStrategy
             }
         }
 
+        for (int i = 1; i < trustChain.size() - 1; i++) {
+            if (!FederationPolicyConstraintHelper.verifyPolicyConstraints(
+                    objectMapper, trustChain.get(i), trustChain.subList(0, i), federationPolicyConstraints)) {
+                log.debug("Subordinate statement {} policy constraints validation failed",
+                        trustChain.get(i).getEntityID());
+                return false;
+            }
+        }
+
         final EntityStatement trustAnchor = trustChain.get(trustChain.size() - 1);
         if (!trustAnchor.equals(trustAnchorSignatureValidationFilterStrategy.apply(trustAnchor, null))) {
             log.debug("Trust anchor {} validation failed", trustAnchor.getEntityID());
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
index 4649dbbf..d92c5476 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
@@ -25,9 +25,12 @@ import javax.annotation.Nullable;
 
 import org.slf4j.Logger;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityID;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraintHelper;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
@@ -65,6 +68,12 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
     /** Cache containing local copies of trusted trust anchor keys. */
     @NonnullAfterInit private MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
 
+    /** Map of supported federation policy constraints. */
+    @NonnullAfterInit private Map<String, FederationPolicyConstraint> federationPolicyConstraints;
+
+    /** JSON object mapper used for decoding JSON into Map. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
     /**
      * Set the strategy for fetching entity ID from the criteria set.
      * 
@@ -106,6 +115,27 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
         localTrustAnchorsCache = Constraint.isNotNull(cache, "Local Trust Anchor cache cannot be null");
     }
 
+    /**
+     * Set the map of supported federation policy constraints.
+     * 
+     * @param constraints map of supported federation policy constraints.
+     */
+    public void setfederationPolicyConstraints(@Nonnull final Map<String, FederationPolicyConstraint> constraints) {
+        checkSetterPreconditions();
+        federationPolicyConstraints = Constraint.isNotNull(constraints, "Map of policy constraints cannot be null");
+    }
+
+    /**
+     * Set the JSON {@link ObjectMapper} used for decoding JSON into Map.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -122,6 +152,12 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
         if (localTrustAnchorsCache == null) {
             throw new ComponentInitializationException("Local Trust Anchor cache cannot be null");
         }
+        if (federationPolicyConstraints == null) {
+            throw new ComponentInitializationException("Map of policy constraints cannot be null");
+        }
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
     }
 
     /** {@inheritDoc} */
@@ -285,9 +321,11 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
                         .filter(id -> preSelectedChain.isEmpty() || preSelectedChain.contains(id))
                         .map(id -> fetchAuthority(entityStatement, id))
                         .filter(pair -> pair != null && pair.getFirst() != null && pair.getSecond() != null)
+                        .filter(pair -> FederationPolicyConstraintHelper.verifyPolicyConstraints(
+                                objectMapper, pair.getSecond(), chain, federationPolicyConstraints))
                         .toList();
                 hints = !authorities.isEmpty();
-                authorities.stream().forEach(authority -> {
+                authorities.forEach(authority -> {
                     final ArrayList<EntityStatement> newChain = new ArrayList<>(chain);
                     newChain.add(authority.getSecond());
                     newChain.add(authority.getFirst());
@@ -326,7 +364,7 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
             final EntityStatement metadata = getFirstIfFound(subordinateStatementCache.get(criteria));
             return new Pair<>(authorityConfiguration, metadata);
         } catch (final MetadataCacheException e) {
-            log.error("Could not resolve authority hint {} for {)", authorityHint, entityId);
+            log.error("Could not resolve authority hint {} for {}", authorityHint, entityId);
             return null;
         }
     }
@@ -340,4 +378,5 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
     @Nullable private EntityStatement getFirstIfFound(@Nonnull final List<EntityStatement> statements) {
         return statements.size() > 0 ? statements.get(0) : null;
     }
+
 }
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
new file mode 100644
index 00000000..de05c5f6
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementHelper.java
@@ -0,0 +1,83 @@
+/*
+ * 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.Map;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+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 net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Static utility method related to entity statements.
+ */
+public class EntityStatementHelper {
+
+    /** Class logger. */
+    @Nonnull private final static Logger log = LoggerFactory.getLogger(EntityStatementHelper.class);
+
+    /**
+     * Parses the given claim as 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, Object> parseClaimAsMap(@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);
+            try {
+                final Map<String, Object> result =
+                        objectMapper.readValue(rawClaim.toString(), objectMapType);
+                if (result != null) {
+                    log.trace("Parsed {} map: {}", claim, result);
+                    return result;
+                }
+            } catch (final JsonProcessingException e) {
+                log.warn("Could not parse " + claim + " from the subordinate statement", e);
+            }
+        }
+        log.trace("Returning empty map");
+        return CollectionSupport.emptyMap();
+    }
+
+    /**
+     * Parses the "metadata" -claim from the given entity statement.
+     * 
+     * @param objectMapper object mapper used for parsing
+     * @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 final EntityStatement entityStatement) {
+        return parseClaimAsMap(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/FederationPolicyConstraint.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraint.java
new file mode 100644
index 00000000..b7ea2062
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraint.java
@@ -0,0 +1,37 @@
+/*
+ * 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.constraints;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+/**
+ * Interface to be implemented by the federation policy constraints.
+ */
+public interface FederationPolicyConstraint {
+
+    /**
+     * Validate whether the given trust chain meets given federation policy constraint value,
+     * 
+     * @param constraint the constraint value
+     * @param trustChain trust chain to be evaluated
+     * @return true if the trust chain is valid for this constraint, false otherwise.
+     */
+    public boolean validate(@Nullable final Object constraint, @Nonnull final List<EntityStatement> trustChain);
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraintHelper.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraintHelper.java
new file mode 100644
index 00000000..01c82bb2
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraintHelper.java
@@ -0,0 +1,73 @@
+/*
+ * 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.constraints;
+
+import java.util.List;
+import java.util.Map;
+
+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.idp.plugin.oidc.op.oidfed.metadata.EntityStatementHelper;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Static utility method related to federation policy constraints.
+ */
+public class FederationPolicyConstraintHelper {
+
+    /** Class logger. */
+    @Nonnull private final static Logger log = LoggerFactory.getLogger(FederationPolicyConstraintHelper.class);
+
+    /**
+     * Parses and verifies federation policy constraints set in the given subordinates statement against the given
+     * trust chain.
+     * 
+     * @param objectMapper object mapper used for parsing the constraints claim
+     * @param subordinateStatement subordinate statement containing the constraints
+     * @param trustChain trust chain to be verified
+     * @param federationPolicyConstraints map of the federation policy constraint implementations
+     * @return true if trust chain meets the constraints. false otherwise
+     */
+    public static boolean verifyPolicyConstraints(@Nonnull final ObjectMapper objectMapper,
+            @Nullable final EntityStatement subordinateStatement, @Nonnull final List<EntityStatement> trustChain,
+            @Nonnull Map<String, FederationPolicyConstraint> federationPolicyConstraints) {
+        if (subordinateStatement == null) {
+            return true;
+        }
+        final Map<String, Object> constraints =
+                EntityStatementHelper.parseClaimAsMap(objectMapper, subordinateStatement, "constraints");
+        for (final String constraint : constraints.keySet()) {
+            final FederationPolicyConstraint validator = federationPolicyConstraints.get(constraint);
+            if (validator != null) {
+                log.trace("Validating federation policy constraint {} with {}", constraint, validator);
+                if (!validator.validate(constraints.get(constraint), trustChain)) {
+                    log.warn("Subordinate statement issued by {} contained constraint {} that failed",
+                            subordinateStatement.getClaimsSet().getIssuer(), constraint);
+                    return false;
+                }
+            } else {
+                log.debug("Ignoring non-recognized federation policy constraint {}", constraint);
+            }
+        }
+        return true;
+    }
+
+}
\ 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/AbstractFederationPolicyConstraint.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/AbstractFederationPolicyConstraint.java
new file mode 100644
index 00000000..e0563b94
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/AbstractFederationPolicyConstraint.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.impl;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Base implementation for the classes implementing {@link FederationPolicyConstraint}.
+ *
+ * @param <T> the data type for the constraint
+ */
+public abstract class AbstractFederationPolicyConstraint<T extends Object>
+    extends AbstractIdentifiableInitializableComponent implements FederationPolicyConstraint {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractFederationPolicyConstraint.class);
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean validate(@Nullable final Object constraint,
+            @Nonnull final List<EntityStatement> trustChain) {
+        checkComponentActive();
+        try {
+            log.trace("Attempting to parse raw constraint value: {}", constraint);
+            final T constraintData = parseConstraint(constraint);
+            if (constraintData != null) {
+                return doValidate(constraintData, trustChain);
+            } else {
+                return true;
+            }
+        } catch (final ConstraintViolationException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Parses the constraint data from the raw claim object value.
+     * 
+     * @param constraint raw object value
+     * @return parsed constraint data
+     * @throws ConstraintViolationException if the parsing was not successful
+     */
+    @Nullable protected abstract T parseConstraint(@Nullable final Object constraint)
+        throws ConstraintViolationException;
+
+    /**
+     * Validate whether the given trust chain meets given federation policy constraint value,
+     * 
+     * @param constraintData the non-null constraint value
+     * @param trustChain trust chain to be evaluated
+     * @return true if the trust chain is valid for this constraint, false otherwise.
+     */
+    protected abstract boolean doValidate(@Nonnull final T constraintData,
+            @Nonnull final List<EntityStatement> trustChain);
+}
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
new file mode 100644
index 00000000..f90c125b
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java
@@ -0,0 +1,106 @@
+/*
+ * 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.constraints.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+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.idp.plugin.oidc.op.oidfed.metadata.EntityStatementHelper;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default implementation for the 'allowed_entity_types' -constraint.
+ */
+public class DefaultAllowedEntityTypesConstraint extends AbstractFederationPolicyConstraint<List<String>> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultAllowedEntityTypesConstraint.class);
+
+    /** JSON object mapper used for decoding JSON into Map. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /**
+     * Set the JSON {@link ObjectMapper} used for decoding JSON into Map.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    protected List<String> parseConstraint(@Nullable final Object constraint)
+            throws ConstraintViolationException {
+        if (constraint instanceof List<?> list) {
+            return list.stream().filter(String.class::isInstance).map(String::valueOf).toList();
+        } else if (constraint != null) {
+            throw new ConstraintViolationException("Unexpected value type for allowed_entity_types: " + constraint);
+        }
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doValidate(@Nonnull final List<String> constraintData,
+            @Nonnull final List<EntityStatement> trustChain) {
+        if (constraintData.contains("federation_entity")) {
+            log.warn("The value 'federation_entity' is not allowed for allowed_entity_types");
+            return false;
+        }
+        final List<String> allowedTypes = new ArrayList<>(constraintData);
+        allowedTypes.add("federation_entity");
+        log.trace("Allowed entity types: {}", allowedTypes);
+        for (final EntityStatement entityStatement : trustChain) {
+            assert entityStatement != null;
+            assert objectMapper != null;
+            final Map<String, Object> metadata =
+                    EntityStatementHelper.parseMetadata(objectMapper, entityStatement);
+            for (final String entityType : metadata.keySet()) {
+                if (!allowedTypes.contains(entityType)) {
+                    log.warn("The entity type {} is not allowed in entity statement {}", entityType,
+                            entityStatement.getEntityID());
+                    return false;
+                }
+                log.trace("The entity type {} is allowed for {}", entityType, entityStatement.getEntityID());
+            }
+        }
+        return true;
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultMaxPathLengthConstraint.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultMaxPathLengthConstraint.java
new file mode 100644
index 00000000..c3dca699
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultMaxPathLengthConstraint.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.impl;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default implementation for the 'max_path_length' -constraint.
+ */
+public class DefaultMaxPathLengthConstraint extends AbstractFederationPolicyConstraint<Integer> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultMaxPathLengthConstraint.class);
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    protected Integer parseConstraint(@Nullable final Object constraint)
+            throws ConstraintViolationException {
+        if (constraint instanceof Integer integer) {
+            return integer;
+        } else if (constraint != null) {
+            throw new ConstraintViolationException("Unexpected value type for max_path_length: " + constraint);
+        }
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doValidate(@Nonnull final Integer constraintData,
+            @Nonnull final List<EntityStatement> trustChain) {
+        final List<EntityStatement> intermediates = trustChain.stream()
+                .filter(es -> !es.getEntityID().getValue().equals(es.getClaimsSet().getIssuer().getValue()))
+                .toList();
+        final int length = intermediates.size();
+        log.trace("Maximum path length: {}, amount of intermediates is {}", constraintData, length);
+        if (constraintData < length) {
+            log.warn("Maximum path length is constricted to {}, amount of intermediates is {}",
+                    constraintData, length);
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultNamingConstraintsConstraint.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultNamingConstraintsConstraint.java
new file mode 100644
index 00000000..c3ff2e21
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultNamingConstraintsConstraint.java
@@ -0,0 +1,118 @@
+/*
+ * 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.constraints.impl;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralSubtree;
+import org.bouncycastle.asn1.x509.NameConstraintValidatorException;
+import org.bouncycastle.asn1.x509.PKIXNameConstraintValidator;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+
+/**
+ * Default implementation for the 'naming_constraints' -constraint.
+ */
+public class DefaultNamingConstraintsConstraint extends AbstractFederationPolicyConstraint<Map<String, List<String>>> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultNamingConstraintsConstraint.class);
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    protected Map<String, List<String>> parseConstraint(@Nullable final Object constraint)
+            throws ConstraintViolationException {
+        if (constraint instanceof Map<?,?> map) {
+            return map.keySet().stream()
+                    .filter(key -> "permitted".equals(key) || "excluded".equals(key))
+                    .map(String::valueOf)
+                    .collect(Collectors.toMap(key -> key, key -> parseListOfStrings(key, map.get(key))));
+        } else if (constraint != null) {
+            throw new ConstraintViolationException("Unexpected value type for naming_constraints: " + constraint);
+        }
+        return null;
+    }
+
+    /**
+     * Parses the raw object value into a list of strings.
+     * 
+     * @param key the key (used solely in a potential exception)
+     * @param raw the value to be parsed
+     * @return the value as list of strings, or null if the input was null
+     * @throws ConstraintViolationException if a non-null value could not be parsed
+     */
+    @Nullable private List<String> parseListOfStrings(@Nullable final String key, @Nullable final Object raw)
+            throws ConstraintViolationException {
+        if (raw instanceof List<?> list) {
+            final List<String> result = list.stream().filter(Objects::nonNull).map(String::valueOf).toList();
+            return result;
+        } else if (raw != null) {
+            throw new ConstraintViolationException("The value for " + key + " is not a list: " + raw);
+        }
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doValidate(@Nonnull final Map<String, List<String>> constraintData,
+            @Nonnull final List<EntityStatement> trustChain) {
+        final PKIXNameConstraintValidator bcValidator = new PKIXNameConstraintValidator();
+
+        Optional.ofNullable(constraintData.get("excluded"))
+            .orElseGet(NonnullSupplier.of(CollectionSupport.emptyList()))
+            .forEach(item -> bcValidator.addExcludedSubtree(
+                    new GeneralSubtree(new GeneralName(GeneralName.dNSName, item))));
+
+        Optional.ofNullable(constraintData.get("permitted"))
+            .orElseGet(NonnullSupplier.of(CollectionSupport.emptyList()))
+            .forEach(item -> bcValidator.intersectPermittedSubtree(
+                    new GeneralSubtree(new GeneralName(GeneralName.dNSName, item))));
+
+        for (final EntityStatement entityStatement : trustChain) {
+            final String host = URI.create(entityStatement.getEntityID().getValue()).getHost();
+            try {
+                bcValidator.checkExcluded(new GeneralName(GeneralName.dNSName, host));
+                log.trace("Validation for 'excluded' was successful for {}", host);
+            } catch (final NameConstraintValidatorException e) {
+                log.warn("Constraint excludes entity name {}", host);
+                return false;
+            }
+            try {
+                bcValidator.checkPermitted(new GeneralName(GeneralName.dNSName, host));
+                log.trace("Validation for 'permitted' was successful for {}", host);
+            } catch (final NameConstraintValidatorException e) {
+                log.warn("Constraint does not permit entity name {}", host);
+                return false;
+            }
+        }
+        return true;
+    }
+
+}
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 ebaefcfa..a4d107a1 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
@@ -1142,10 +1142,26 @@
                 p:criteriaToSubjectEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
                 p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
                 p:subordinateStatementCache-ref="shibboleth.oidfed.SubordinateEntityStatementMetadataCache"
-                p:localTrustAnchorsCache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" />
+                p:localTrustAnchorsCache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache"
+                p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}"
+                p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
         </property>
     </bean>
 
+    <util:map id="shibboleth.oidfed.DefaultFederationPolicyConstraints"
+        value-type="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint">
+        <entry key="max_path_length">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.impl.DefaultMaxPathLengthConstraint" />
+        </entry>
+        <entry key="naming_constraints">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.impl.DefaultNamingConstraintsConstraint" />
+        </entry>
+        <entry key="allowed_entity_types">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.impl.DefaultAllowedEntityTypesConstraint"
+                p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+        </entry>
+    </util:map>
+
     <bean id="shibboleth.oidfed.DefaultLocalTrustAnchorsFilename" class="java.lang.String" factory-method="valueOf">
         <constructor-arg value="%{idp.oidfed.DefaultLocalTrustAnchorsFile:%{idp.home}/conf/oidfed-trust-anchors.json}" />
     </bean>
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 3b34316d..c5424448 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
@@ -43,7 +43,9 @@
         p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.register.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
         p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}">
         <property name="providedTrustChainValidationStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultProvidedTrustChainValidationStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultProvidedTrustChainValidationStrategy"
+                p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}"
+                p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper">
                 <property name="trustEngine">
                     <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
                         <constructor-arg>
@@ -117,7 +119,9 @@
                 p:criteriaToSubjectEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
                 p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
                 p:subordinateStatementCache-ref="shibboleth.oidfed.SubordinateEntityStatementMetadataCache"
-                p:localTrustAnchorsCache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" />
+                p:localTrustAnchorsCache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache"
+                p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}"
+                p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper" />
         </property>
     </bean>
 
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 f17165e7..e15f8217 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
@@ -28,6 +28,7 @@ import java.security.KeyPair;
 import java.security.NoSuchAlgorithmException;
 import java.security.interfaces.RSAPublicKey;
 import java.time.Instant;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -236,6 +237,21 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         return anchorConfiguration.getSignedStatement().serialize();
     }
 
+    protected String trustedAnchorConfiguration(final Map<String, Object> constraints) {
+        final String anchorId = "https://trust-anchor.federation.local";
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(anchorId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(trustedAnchorKey).toJSONObject(true))
+                .claim("metadata", Map.of("federation_entity", Map.of("federation_fetch_endpoint",
+                        anchorFetchEndpoint)))
+                .claim("constraints", constraints)
+                .build();
+        final EntityStatement anchorConfiguration =
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet);
+        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()))
@@ -277,6 +293,24 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, issuerKey, 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 Map<String, Object> constraints,
+            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)
+                .claim("constraints", constraints)
+                .build();
+        final EntityStatement rpConfiguration =
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, issuerKey, claimsSet);
+        return rpConfiguration.getSignedStatement().serialize();
+    }
+    
     protected String uniqueClientId() {
         return String.format(clientIdPattern, clientIndex.getAndIncrement());
     }
@@ -318,6 +352,19 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         }
     }
 
+    protected void configureMockHttpClientWithAnchorConstraints(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)));
+        } 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) {
         final String intermediateId = uniqueIntermediateId();
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
index 1bd9782b..dfba9965 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
@@ -22,6 +22,7 @@ import java.net.URLEncoder;
 import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -312,6 +313,69 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
     }
 
+    @Test
+    public void testWithInvalidTrustChain_entityTypeConstraint_signedRequestObject()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        configureMockHttpClientWithAnchorConstraints(clientId, Map.of("allowed_entity_types", Collections.emptyList()));
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+    }
+
+    @Test
+    public void testWithInvalidTrustChain_namingConstraint_signedRequestObject()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        configureMockHttpClientWithAnchorConstraints(clientId, Map.of("naming_constraints",
+                Map.of("permitted", List.of(".wrongfederation.local"))));
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+    }
+
+    @Test
+    public void testWithValidTrustChain_withConstraints_signedRequestObject()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        configureMockHttpClientWithAnchorConstraints(clientId,
+                Map.of("naming_constraints", Map.of("permitted", List.of(".federation.local")),
+                        "allowed_entity_types", List.of("openid_relying_party")));
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
+    }
+
     @Test
     public void testWithPar_matchingAutoRegisteredTrustChain()
             throws IOException, UnsupportedOperationException, URISyntaxException {
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 a11f16a9..e0717249 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
@@ -21,7 +21,9 @@ import static org.mockito.Mockito.verify;
 
 import java.io.IOException;
 import java.net.URI;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 import org.opensaml.storage.StorageRecord;
 import org.opensaml.storage.StorageService;
@@ -153,6 +155,103 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
     }
 
+    @Test
+    public void testValidTrustChain_validMaxLengthInAnchor() throws Exception {
+        final String clientId = uniqueClientId();
+        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+                subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+                        Map.of("max_path_length", Integer.valueOf(0))) + "\", \"" + trustedAnchorConfiguration() + "\"]";
+        setRequest("POST", trustChain, "application/trust-chain+json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(super.subordinateStatementUrl(anchorFetchEndpoint, clientId))), any());
+        assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
+    }
+
+    @Test
+    public void testValidTrustChain_invalidMaxLengthInAnchor() throws Exception {
+        final String clientId = uniqueClientId();
+        final String intermediateId = uniqueIntermediateId();
+        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+                subordinateStatement(intermediateId, intermediateKey, leafKey, clientId, Collections.emptyMap(),
+                        Collections.emptyMap()) + "\", \"" +
+                subordinateStatement(anchorId, anchorKey, intermediateKey, intermediateId, Collections.emptyMap(),
+                        Map.of("max_path_length", Integer.valueOf(0))) + "\", \"" + trustedAnchorConfiguration() + "\"]";
+        setRequest("POST", trustChain, "application/trust-chain+json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_request");
+    }
+
+    @Test
+    public void testValidTrustChain_validNamingConstraintInAnchor() throws Exception {
+        final String clientId = uniqueClientId();
+        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+                subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+                        Map.of("naming_constraints", Map.of("permitted", List.of(".federation.local")))) +
+                "\", \"" + trustedAnchorConfiguration() + "\"]";
+        setRequest("POST", trustChain, "application/trust-chain+json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(super.subordinateStatementUrl(anchorFetchEndpoint, clientId))), any());
+        assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
+    }
+
+    @Test
+    public void testValidTrustChain_invalidNamingConstraintInAnchor() throws Exception {
+        final String clientId = uniqueClientId();
+        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+                subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+                        Map.of("naming_constraints", Map.of("permitted", List.of(".wrongfederation.local")))) + 
+                "\", \"" + trustedAnchorConfiguration() + "\"]";
+        setRequest("POST", trustChain, "application/trust-chain+json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_request");
+    }
+
+    @Test
+    public void testValidTrustChain_validEntityTypeInAnchor() throws Exception {
+        final String clientId = uniqueClientId();
+        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+                subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+                        Map.of("allowed_entity_types", List.of("openid_relying_party"))) + "\", \""  +
+                        trustedAnchorConfiguration() + "\"]";
+        setRequest("POST", trustChain, "application/trust-chain+json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(entityConfigurationUrl(clientId))), any());
+        verify(federationHttpClient, times(0)).executeOpen(any(),
+                argThat(new RequestUriMatcher(super.subordinateStatementUrl(anchorFetchEndpoint, clientId))), any());
+        assertResponseStatement(parseSuccessResponse(result, ExplicitClientRegistrationResponse.class), clientId);
+    }
+
+    @Test
+    public void testValidTrustChain_invalidEmptyEntityTypeInAnchor() throws Exception {
+        final String clientId = uniqueClientId();
+        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+                subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+                        Map.of("allowed_entity_types", Collections.emptyList())) + "\", \""  +
+                        trustedAnchorConfiguration() + "\"]";
+        setRequest("POST", trustChain, "application/trust-chain+json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_request");
+    }
+
+    @Test
+    public void testValidTrustChain_invalidEntityTypeInAnchor() throws Exception {
+        final String clientId = uniqueClientId();
+        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+                subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+                        Map.of("allowed_entity_types", List.of("openid_provider"))) + "\", \""  +
+                        trustedAnchorConfiguration() + "\"]";
+        setRequest("POST", trustChain, "application/trust-chain+json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_request");
+    }
+
     @Test
     public void testInvalidTrustChain_wrongRpEntityConfigurationSignerKey() throws Exception {
         final String clientId = uniqueClientId();

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


More information about the commits mailing list