[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