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