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

Henri Mikkonen henri.mikkonen at iki.fi
Wed Mar 26 15:05:09 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=029aee73e64287715da97fb710b4c68de47e170a

commit 029aee73e64287715da97fb710b4c68de47e170a
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed Mar 26 17:03:50 2025 +0200

    JOIDC-222 - Support for OpenID Federation
    
    https://shibboleth.atlassian.net/browse/JOIDC-222
    
    - Initial support for "pushed" trust chain validation in the explicit registration flow
      - Trust chain resolution process (via metadata caches) is avoided
      - Trust anchor needs to be locally trusted
---
 ...ctTrustEngineSignatureValidationComponent.java} |  41 +---
 ...StatementSignatureValidationFilterStrategy.java |  65 +-----
 ...efaultProvidedTrustChainValidationStrategy.java | 111 +++++++++
 .../profile/impl/ValidateProvidedTrustChain.java   | 253 +++++++++++++++++++++
 ...egistrationRequestTrustChainLookupFunction.java |  50 ++++
 .../idp/flows/oidfed/register/register-beans.xml   |  35 +++
 .../idp/flows/oidfed/register/register-flow.xml    |  13 +-
 .../profile/flow/oidfed/RegistrationFlowTest.java  |  35 +++
 8 files changed, 511 insertions(+), 92 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
similarity index 70%
copy from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
copy to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
index a40a1279..45c847cf 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
@@ -14,8 +14,6 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
 
-import java.util.function.BiFunction;
-
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
@@ -26,9 +24,7 @@ import org.slf4j.Logger;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
-import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
 import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
@@ -36,16 +32,12 @@ import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.resolver.CriteriaSet;
 
 /**
- * Default signature validating filter for entity statement. The signature validation is performed via configurable
- * {@link TrustEngine}.
+ * Abstract component performing signature validation via {@link TrustEngine}.
  */
- at ThreadSafeAfterInit
-public class DefaultEntityStatementSignatureValidationFilterStrategy extends AbstractIdentifiableInitializableComponent
-        implements BiFunction<EntityStatement, MetadataFilterContext, EntityStatement> {
+public class AbstractTrustEngineSignatureValidationComponent extends AbstractIdentifiableInitializableComponent {
 
     /** Class logger. */
-    @Nonnull private Logger log =
-            LoggerFactory.getLogger(DefaultEntityStatementSignatureValidationFilterStrategy.class);
+    @Nonnull private Logger log = LoggerFactory.getLogger(AbstractTrustEngineSignatureValidationComponent.class);
 
     /** Trust engine used to validate a signature. */
     @NonnullAfterInit private TrustEngine<SignedJWT> trustEngine;
@@ -82,24 +74,14 @@ public class DefaultEntityStatementSignatureValidationFilterStrategy extends Abs
         }
     }
 
-    /** {@inheritDoc} */
-    @Override @Nullable
-    public EntityStatement apply(@Nullable final EntityStatement entityStatement,
-            @Nullable final MetadataFilterContext filterContext) {
-        checkComponentActive();
-        if (entityStatement == null) {
-            return null;
-        }
-
-        final String entityId = entityStatement.getEntityID().getValue();
-        log.trace("Starting signature validation of entity statement for {}", entityId);
-        final CriteriaSet criteria = new CriteriaSet(new SubjectEntityStatementCriterion(entityStatement));
-        if (validateStatement(entityStatement, criteria, entityId)) {
-            return entityStatement;
-        }
-        return null;
-    }
-
+    /**
+     * Validates the given entity statement via trust engine and the given criteria.
+     * 
+     * @param entityStatement the entity statement to be validated
+     * @param criteria the criteria (expanded with the optional default criteria)
+     * @param entityId the entity ID used for logging
+     * @return true if validation was successful, false otherwise
+     */
     protected boolean validateStatement(@Nonnull final EntityStatement entityStatement,
             @Nonnull final CriteriaSet criteria, @Nullable final String entityId) {
         if (defaultCriteria != null && !defaultCriteria.isEmpty()) {
@@ -118,4 +100,5 @@ public class DefaultEntityStatementSignatureValidationFilterStrategy extends Abs
         log.warn("Trust Engine validation failed for {}", entityId);
         return false;
     }
+
 }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
index a40a1279..a3a06e46 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
@@ -19,19 +19,13 @@ import java.util.function.BiFunction;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.opensaml.security.SecurityException;
 import org.opensaml.security.trust.TrustEngine;
 import org.slf4j.Logger;
 
-import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
-import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.resolver.CriteriaSet;
 
@@ -40,48 +34,14 @@ import net.shibboleth.shared.resolver.CriteriaSet;
  * {@link TrustEngine}.
  */
 @ThreadSafeAfterInit
-public class DefaultEntityStatementSignatureValidationFilterStrategy extends AbstractIdentifiableInitializableComponent
-        implements BiFunction<EntityStatement, MetadataFilterContext, EntityStatement> {
+public class DefaultEntityStatementSignatureValidationFilterStrategy
+    extends AbstractTrustEngineSignatureValidationComponent
+    implements BiFunction<EntityStatement, MetadataFilterContext, EntityStatement> {
 
     /** Class logger. */
     @Nonnull private Logger log =
             LoggerFactory.getLogger(DefaultEntityStatementSignatureValidationFilterStrategy.class);
 
-    /** Trust engine used to validate a signature. */
-    @NonnullAfterInit private TrustEngine<SignedJWT> trustEngine;
-
-    /** Set of externally specified default criteria for input to the trust engine. */
-    @Nullable private CriteriaSet defaultCriteria;
-
-    /**
-     * Set trust engine used to validate a signature.
-     * 
-     * @param engine trust engine
-     */
-    public void setTrustEngine(@Nonnull final TrustEngine<SignedJWT> engine) {
-        checkSetterPreconditions();
-        trustEngine = Constraint.isNotNull(engine, "Trust Engine cannot be null");
-    }
-
-    /**
-     * Set the optional set of default criteria used as input to the trust engine.
-     * 
-     * @param criteria criteria set to use
-     */
-    public void setDefaultCriteria(@Nullable final CriteriaSet criteria) {
-        checkSetterPreconditions();
-        defaultCriteria = criteria;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        if (trustEngine == null) {
-            throw new ComponentInitializationException("Trust Engine cannot be null");
-        }
-    }
-
     /** {@inheritDoc} */
     @Override @Nullable
     public EntityStatement apply(@Nullable final EntityStatement entityStatement,
@@ -99,23 +59,4 @@ public class DefaultEntityStatementSignatureValidationFilterStrategy extends Abs
         }
         return null;
     }
-
-    protected boolean validateStatement(@Nonnull final EntityStatement entityStatement,
-            @Nonnull final CriteriaSet criteria, @Nullable final String entityId) {
-        if (defaultCriteria != null && !defaultCriteria.isEmpty()) {
-            criteria.addAll(defaultCriteria);
-        }
-        try {
-            final SignedJWT jwt = entityStatement.getSignedStatement();
-            assert jwt != null;
-            if (trustEngine.validate(jwt, criteria)) {
-                log.debug("Successfully validated entity statement for {}", entityId);
-                return true;
-            }
-        } catch (final SecurityException e) {
-            log.debug("Could not validate entity statement for {}", entityId, e);
-        }
-        log.warn("Trust Engine validation failed for {}", entityId);
-        return false;
-    }
 }
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
new file mode 100644
index 00000000..65c8eb0d
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default strategy for validating provided trust chain signatures via {@link TrustEngine} and configurable
+ * trust anchor signature validation filter.
+ */
+public class DefaultProvidedTrustChainValidationStrategy
+    extends AbstractTrustEngineSignatureValidationComponent 
+    implements BiPredicate<ProfileRequestContext, List<EntityStatement>> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultProvidedTrustChainValidationStrategy.class);
+
+    /** Strategy for validating trust anchor's entity configuration signature. */
+    @NonnullAfterInit BiFunction<EntityStatement, MetadataFilterContext, EntityStatement>
+        trustAnchorSignatureValidationFilterStrategy;
+
+    /**
+     * Set the strategy for validating trust anchor's entity configuration signature.
+     * 
+     * @param strategy validation strategy
+     */
+    public void setTrustAnchorSignatureValidationFilterStrategy(
+            @Nonnull final BiFunction<EntityStatement, MetadataFilterContext, EntityStatement> strategy) {
+        checkSetterPreconditions();
+        trustAnchorSignatureValidationFilterStrategy = Constraint.isNotNull(strategy,
+                "TrustAnchorSignatureValidationFilterStrategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (trustAnchorSignatureValidationFilterStrategy == null) {
+            throw new ComponentInitializationException("TrustAnchorSignatureValidationFilterStrategy cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nullable final List<EntityStatement> trustChain) {
+        if (trustChain == null || trustChain.size() < 3 || trustChain.contains(null)) {
+            log.error("No satisfactory trust chain provided");
+            return false;
+        }
+
+        final EntityStatement entityConfiguration = trustChain.get(0);
+        assert entityConfiguration != null;
+        if (!validateStatement(entityConfiguration,
+                new CriteriaSet(new SubjectEntityStatementCriterion(entityConfiguration)),
+                entityConfiguration.getEntityID().getValue())) {
+            log.debug("Entity configuration signature validation failed");
+            return false;
+        }
+        for (int i = 1; i < trustChain.size() - 2; i++) {
+            final EntityStatement subordinateStatement = trustChain.get(i);
+            final EntityStatement issuerStatement = trustChain.get(i + 1);
+            assert subordinateStatement != null;
+            assert issuerStatement != null;
+            final CriteriaSet criteria = new CriteriaSet();
+            criteria.add(new IssuerEntityStatementCriterion(issuerStatement));
+            criteria.add(new SubjectEntityStatementCriterion(subordinateStatement));
+            if (!validateStatement(subordinateStatement, criteria, subordinateStatement.getEntityID().getValue())) {
+                log.debug("Subordinate statement {} signature validation failed", subordinateStatement.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());
+            return false;
+        }
+
+        return true;
+    }
+}
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
new file mode 100644
index 00000000..22bb359c
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java
@@ -0,0 +1,253 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+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;
+import net.shibboleth.shared.collection.Pair;
+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;
+
+/**
+ * Validates the provided trust chain and enforces metadata policy merging
+ * strategy and enforcer. The data is populated to the {@link RelyingPartyTrustChainContext}.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * 
+ * @since 4.3.0
+ */
+public class ValidateProvidedTrustChain extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidateProvidedTrustChain.class);
+
+    /** Strategy used to validate provided trust chain. */
+    @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.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setProvidedTrustChainLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, List<EntityStatement>> strategy) {
+        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.
+     * 
+     * @param strategy validation strategy
+     */
+    public void setProvidedTrustChainValidationStrategy(
+            @Nonnull final BiPredicate<ProfileRequestContext, List<EntityStatement>> strategy) {
+        checkSetterPreconditions();
+        providedTrustChainValidationStrategy =
+                Constraint.isNotNull(strategy, "ProvidedTrustChainValidationStrategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (providedTrustChainLookupStrategy == null) {
+            throw new ComponentInitializationException("ProvidedTrustChainLookupStrategy cannot be null");
+        }
+        if (providedTrustChainValidationStrategy == null) {
+            throw new ComponentInitializationException("ProvidedTrustChainValidationStrategy cannot be null");
+        }
+        if (metadataPolicyEnforcer == null) {
+            throw new ComponentInitializationException("MetadataPolicyEnforcer cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        trustChain = providedTrustChainLookupStrategy.apply(profileRequestContext);
+        if (trustChain == null) {
+            log.error("{} Unable to fetch trust chain", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!providedTrustChainValidationStrategy.test(profileRequestContext, trustChain)) {
+            log.error("{} The trust chain validation failed", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return;
+        }
+
+        final RelyingPartyTrustChainContext trustChainContext =
+                trustChainContextCreationStrategy.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) {
+            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);
+            return;
+        }
+
+        log.debug("{} Setting the policy compliant trust chains into the context: {}", getLogPrefix(),
+                policyCompliantChains);
+        trustChainContext.setPolicyCompliantTrustChains(policyCompliantChains);
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainLookupFunction.java
new file mode 100644
index 00000000..283ab0bb
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainLookupFunction.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate;
+
+import java.util.List;
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationRequest;
+
+/**
+ * A function that returns pushed trust chain set in the explicit registration request.
+ * 
+ * @since 4.3.0
+ */
+ at ThreadSafe
+public class ExplicitClientRegistrationRequestTrustChainLookupFunction
+    implements ContextDataLookupFunction<ProfileRequestContext, List<EntityStatement>> {
+
+    /** {@inheritDoc} */
+    @Nullable
+    public List<EntityStatement> apply(@Nullable final ProfileRequestContext input) {
+        return Optional.ofNullable(input)
+                .map(profileRequestContext -> profileRequestContext.getInboundMessageContext())
+                .map(messageContext -> messageContext.getMessage())
+                .filter(ExplicitClientRegistrationRequest.class::isInstance)
+                .map(ExplicitClientRegistrationRequest.class::cast)
+                .map(request -> request.getTrustChain())
+                .orElse(null);
+    }
+}
\ No newline at end of file
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 0845f640..5a60fb0e 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
@@ -37,6 +37,41 @@
     <bean id="ExplicitRegistrationRelyingPartyCreationStrategy" parent="shibboleth.Functions.Expression"
         c:expression="#input.ensureSubcontext(T(net.shibboleth.profile.context.RelyingPartyContext))" />
 
+    <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()}">
+        <property name="providedTrustChainValidationStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultProvidedTrustChainValidationStrategy">
+                <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>
+                <property name="trustAnchorSignatureValidationFilterStrategy">
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementSignatureValidationFilterStrategy">
+                        <property name="trustEngine">
+                            <bean class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
+                                <constructor-arg index="0">
+                                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultLocalTrustAnchorCredentialResolver"
+                                        c:cache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" />
+                                 </constructor-arg>
+                                 <constructor-arg index="1">
+                                     <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementCredentialResolver" />
+                                 </constructor-arg>
+                            </bean>
+                        </property>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+        <property name="providedTrustChainLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.ExplicitClientRegistrationRequestTrustChainLookupFunction" />
+        </property>
+    </bean>
+ 
     <bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
         scope="prototype"
         p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-flow.xml
index 493fae0c..6c4a51d7 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-flow.xml
@@ -6,10 +6,21 @@
     <action-state id="InitializeMandatoryContexts">
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="DecodeMessage">
-            <set name="flowScope.transitionAfterDecode" value="'ResolveTrustChains'" />
+            <set name="flowScope.transitionAfterDecode" value="'SelectTrustChainResolution'" />
          </transition>
     </action-state>
 
+    <decision-state id="SelectTrustChainResolution">
+        <if test="opensamlProfileRequestContext.ensureInboundMessageContext().getMessage().getTrustChain() != null"
+            then="ValidateProvidedTrustChain" else="ResolveTrustChains" />
+    </decision-state>
+
+    <action-state id="ValidateProvidedTrustChain">
+        <evaluate expression="ValidateProvidedTrustChain"/>
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="SelectTrustChain" />
+    </action-state>
+
     <action-state id="ResolveTrustChains">
         <evaluate expression="ResolveTrustChains" />
         <evaluate expression="'proceed'" />
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 c41d1361..22cd0b55 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
@@ -103,6 +103,41 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         Assert.assertEquals(storedInfo.getOIDCMetadata().getRedirectionURIStrings(),
                 metadata.getRedirectionURIStrings());
         Assert.assertTrue(metadata.getRedirectionURIStrings().contains(redirectUri));
+    }
+
+    @Test
+    public void testValidTrustChain() throws Exception {
+        final String clientId = uniqueClientId();
+        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+                subordinateStatement(clientId) + "\", \"" + trustedAnchorConfiguration() + "\"]";
+        setRequest("POST", trustChain, "application/trust-chain+json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final ExplicitClientRegistrationResponse parsedResponse =
+                parseSuccessResponse(result, ExplicitClientRegistrationResponse.class);
+        final EntityStatement entityStatement = parsedResponse.getEntityStatement();
+        final EntityStatementClaimsSet statementClaims = entityStatement.getClaimsSet();
+        Assert.assertEquals(statementClaims.getAuthorityHints().stream().map(id -> id.getValue()).toList(),
+                List.of(anchorId));
+        Assert.assertEquals(statementClaims.getClaim("trust_anchor"), anchorId);
+        final OIDCClientInformation clientInfo = entityStatement.getClaimsSet().getRPInformation();
+        final OIDCClientMetadata metadata = clientInfo.getOIDCMetadata();
+        final String providedClientId = clientInfo.getID().getValue();
+        assert providedClientId != null;
+        assert storageService != null;
+        final StorageRecord<String> storageRecord =
+                storageService.read(BaseStorageServiceClientInformationComponent.CONTEXT_NAME, providedClientId);
+        Assert.assertNotNull(storageRecord, "Record with clientId " + providedClientId + " was null");
+        assert storageRecord != null;
+        final String record = storageRecord.getValue();
+        Assert.assertNotNull(record);
+        final JSONParser parser = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE);
+        final OIDCClientInformation storedInfo = OIDCClientInformation.parse((JSONObject) parser.parse(record));
+        Assert.assertEquals(storedInfo.getID(), clientInfo.getID());
+        Assert.assertEquals(storedInfo.getSecret(), clientInfo.getSecret());
+        Assert.assertEquals(storedInfo.getOIDCMetadata().getRedirectionURIStrings(),
+                metadata.getRedirectionURIStrings());
+        Assert.assertTrue(metadata.getRedirectionURIStrings().contains(redirectUri));
         
     }
+
 }

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


More information about the commits mailing list