[java-idp-plugin-oidc-op-oidfed] branch main updated: Improvements to provided trust chain validation and explicit registration

Codeberg noreply at shibboleth.net
Thu Oct 1 07:07:11 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch main
in repository java-idp-plugin-oidc-op-oidfed.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-oidc-op-oidfed/commit/fc092cc69856316c92b9252435311f25acc08a9f

The following commit(s) were added to refs/heads/main by this push:
     new fc092cc  Improvements to provided trust chain validation and explicit registration
fc092cc is described below

commit fc092cc69856316c92b9252435311f25acc08a9f
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Oct 1 10:06:38 2026 +0300

    Improvements to provided trust chain validation and explicit registration
    
    - Run the entity statement validation filters against each item in the trust chain
      - Configurable properties for leaf, anchor and subordinate statements
        - idp.oidfed.op.automaticRegistration.providedTrustChain.leaf.claimsValidator
        - idp.oidfed.op.automaticRegistration.providedTrustChain.anchor.claimsValidator
        - idp.oidfed.op.automaticRegistration.providedTrustChain.subordinate.claimsValidator
        - idp.oidfed.op.explicitRegistration.providedTrustChain.leaf.claimsValidator
        - idp.oidfed.op.explicitRegistration.providedTrustChain.anchor.claimsValidator
        - idp.oidfed.op.explicitRegistration.providedTrustChain.subordinate.claimsValidator
    - Verify link between entity ID in the request and trust chain leaf
    - Include and test the mandatory audience in the explicit registration response
---
 .../impl/BuildExplicitRegistrationResponse.java    |   1 +
 ...efaultProvidedTrustChainValidationStrategy.java | 165 ++++++++++++++++++++-
 .../oidfed/metadata-lookup-ext-oidfed-beans.xml    |  60 +++++++-
 .../idp/flows/oidfed/register/register-beans.xml   |  57 ++++++-
 .../op/oidfed/conf/oidfed/oidfed-op.properties     |   6 +
 .../AuthorizeFlowAutomaticRegistrationTest.java    |  29 ++++
 .../profile/flow/oidfed/RegistrationFlowTest.java  |  53 +++++--
 7 files changed, 355 insertions(+), 16 deletions(-)

diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildExplicitRegistrationResponse.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildExplicitRegistrationResponse.java
index 6a300a5..36aa568 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildExplicitRegistrationResponse.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildExplicitRegistrationResponse.java
@@ -185,6 +185,7 @@ public class BuildExplicitRegistrationResponse extends AbstractBuildEntityStatem
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;                        
         }
+        builder.claim("aud", clientInformation.getID().getValue());
         builder.claim("metadata",
                 CollectionSupport.singletonMap("openid_relying_party", clientInformation.toJSONObject()));
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/logic/DefaultProvidedTrustChainValidationStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/logic/DefaultProvidedTrustChainValidationStrategy.java
index e95e53e..725e799 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/logic/DefaultProvidedTrustChainValidationStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/logic/DefaultProvidedTrustChainValidationStrategy.java
@@ -14,18 +14,31 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.logic;
 
+import java.time.Instant;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
 import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.SubordinateStatement;
+import net.shibboleth.oidfed.metadata.cache.BaseExpirableStatementContainer;
+import net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer;
+import net.shibboleth.oidfed.metadata.cache.subordinate.SubordinateStatementCacheIdentifier;
+import net.shibboleth.oidfed.metadata.cache.subordinate.SubordinateStatementContainer;
 import net.shibboleth.oidfed.metadata.cache.trustchain.ProvidedTrustChainContainer;
 import net.shibboleth.oidfed.metadata.cache.trustchain.ProvidedTrustChainContainerCriterion;
 import net.shibboleth.oidfed.metadata.cache.trustchain.TrustChainsContainer;
@@ -38,7 +51,8 @@ import net.shibboleth.shared.resolver.CriteriaSet;
 
 /**
  * Default validation strategy for provided trust chain. A {@link MetadataCache} supporting
- * {@link ProvidedTrustChainContainerCriterion} is exploited for validation.
+ * {@link ProvidedTrustChainContainerCriterion} is exploited for validation. Optionally the incoming request client ID
+ * is verified with a configurable lookup strategy. If it's set to null, the validation is skipped.
  */
 public class DefaultProvidedTrustChainValidationStrategy extends AbstractIdentifiableInitializableComponent
     implements BiPredicate<ProfileRequestContext, List<EntityStatement<?>>> {
@@ -49,6 +63,21 @@ public class DefaultProvidedTrustChainValidationStrategy extends AbstractIdentif
     /** Metadata cache for trust chain validation. */
     @NonnullAfterInit private MetadataCache<TrustChainsContainer> metadataCache;
 
+    /** Validation strategy for leaf entity configurations in the provided trust chain. */
+    @NonnullAfterInit private Predicate<BaseExpirableStatementContainer<?>> leafEntityConfigurationValidationStrategy;
+
+    /** Validation strategy for trust anchor entity configuration in the provided trust chain. */
+    @NonnullAfterInit private Predicate<BaseExpirableStatementContainer<?>> anchorEntityConfigurationValidationStrategy;
+
+    /** Validation strategy for subordinate statements in the provided trust chain. */
+    @NonnullAfterInit private Predicate<BaseExpirableStatementContainer<?>> subordinateStatementValidationStrategy;
+
+    /**
+     * Strategy used to obtain the client id value for validating trust chain subject. May be null: in that case the
+     * client id is not verified.
+     */
+    @Nullable private Function<MessageContext, ClientID> clientIDLookupStrategy;
+
     /**
      * Set the metadata cache for trust chain validation.
      * 
@@ -59,6 +88,53 @@ public class DefaultProvidedTrustChainValidationStrategy extends AbstractIdentif
         metadataCache = Constraint.isNotNull(cache, "Metadata cache cannot be null");
     }
 
+    /**
+     * Set the strategy used to locate the client id for validating trust chain subject. May be null: in that case the
+     * client id is not verified.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setClientIDLookupStrategy(@Nullable final Function<MessageContext, ClientID> strategy) {
+        checkSetterPreconditions();
+        clientIDLookupStrategy = strategy;
+    }
+
+    /**
+     * Set the validation strategy for leaf entity configurations in the provided trust chain.
+     * 
+     * @param strategy validation strategy
+     */
+    public void setLeafEntityConfigurationValidationStrategy(
+            @Nonnull final Predicate<BaseExpirableStatementContainer<?>> strategy) {
+        checkSetterPreconditions();
+        leafEntityConfigurationValidationStrategy =
+                Constraint.isNotNull(strategy, "LeafEntityConfigurationValidationStrategy cannot be null");
+    }
+
+    /**
+     * Set the validation strategy for trust anchor entity configurations in the provided trust chain.
+     * 
+     * @param strategy validation strategy
+     */
+    public void setAnchorEntityConfigurationValidationStrategy(
+            @Nonnull final Predicate<BaseExpirableStatementContainer<?>> strategy) {
+        checkSetterPreconditions();
+        anchorEntityConfigurationValidationStrategy =
+                Constraint.isNotNull(strategy, "AnchorEntityConfigurationValidationStrategy cannot be null");
+    }
+
+    /**
+     * Set the validation strategy for subordinate statements in the provided trust chain.
+     * 
+     * @param strategy validation strategy
+     */
+    public void setSubordinateStatementValidationStrategy(
+            @Nonnull final Predicate<BaseExpirableStatementContainer<?>> strategy) {
+        checkSetterPreconditions();
+        subordinateStatementValidationStrategy =
+                Constraint.isNotNull(strategy, "SubordinateStatementValidationStrategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -67,6 +143,15 @@ public class DefaultProvidedTrustChainValidationStrategy extends AbstractIdentif
         if (metadataCache == null) {
             throw new ComponentInitializationException("Metadata cache cannot be null");
         }
+        if (leafEntityConfigurationValidationStrategy == null) {
+            throw new ComponentInitializationException("LeafEntityConfigurationValidationStrategy cannot be null");
+        }
+        if (anchorEntityConfigurationValidationStrategy == null) {
+            throw new ComponentInitializationException("AnchorEntityConfigurationValidationStrategy cannot be null");
+        }
+        if (subordinateStatementValidationStrategy == null) {
+            throw new ComponentInitializationException("SubordinateStatementValidationStrategy cannot be null");
+        }
     }
 
     /** {@inheritDoc} */
@@ -74,6 +159,37 @@ public class DefaultProvidedTrustChainValidationStrategy extends AbstractIdentif
     public boolean test(@Nullable final ProfileRequestContext profileRequestContext,
             @Nullable final List<EntityStatement<?>> trustChain) {
         if (trustChain != null && !trustChain.isEmpty()) {
+            if (clientIDLookupStrategy != null) {
+                final Function<MessageContext, ClientID> nonNullStrategy = clientIDLookupStrategy;
+                assert nonNullStrategy != null;
+                final String clientId = Optional.ofNullable(profileRequestContext)
+                        .map(prc -> nonNullStrategy.apply(prc.getInboundMessageContext()))
+                        .map(id -> id.getValue())
+                        .orElse(null);
+                final String subject = trustChain.get(0).getSubject();
+                if (!subject.equals(clientId)) {
+                    log.warn("The requested clientId {} does not match the trust chain subject {}", clientId, subject);
+                    return false;
+                }
+            }
+            if (!validateEntityConfiguration(trustChain.get(0), true)) {
+                log.warn("Leaf entity configuration {} did not pass the validation", trustChain.get(0).getSubject());
+                return false;
+            }
+            for (int i = 1; i < trustChain.size() - 1; i++) {
+                final EntityStatement<?> subordinate = trustChain.get(i);
+                if (!validateSubordinateStatement(subordinate)) {
+                    log.warn("Subordinate statement for {} issued by {} did not pass the validation",
+                            subordinate.getSubject(), subordinate.getIssuer());
+                    return false;
+                }
+            }
+            if (!validateEntityConfiguration(trustChain.get(trustChain.size() - 1), false)) {
+                log.warn("Trust anchor entity configuration {} did not pass the validation",
+                        trustChain.get(trustChain.size() - 1).getSubject());
+                return false;
+            }
+
             final ProvidedTrustChainContainer container = new ProvidedTrustChainContainer(trustChain,
                     trustChain.get(0).getSubject());
             final ProvidedTrustChainContainerCriterion criterion =
@@ -93,4 +209,51 @@ public class DefaultProvidedTrustChainValidationStrategy extends AbstractIdentif
         return false;
     }
 
+    /**
+     * Validates the given entity statement against leaf or trust anchor entity configuration validation strategy.
+     * 
+     * @param statement entity statement to be validated
+     * @param leaf flag indicating which validation strategy to use
+     * @return true if entity statement was successfully validated, false otherwise
+     */
+    protected boolean validateEntityConfiguration(final EntityStatement<?> statement, final boolean leaf) {
+        if (statement instanceof EntityConfiguration configuration) {
+            final Instant expiration = configuration.getParsedPayload().getExpiration();
+            if (expiration == null) {
+                log.warn("Entity configuration {} is missing mandatory expiration time", statement.getSubject());
+                return false;
+            }
+            final EntityConfigurationContainer container = new EntityConfigurationContainer(
+                    configuration.getSubject(), configuration, expiration, expiration);
+            return leaf ? leafEntityConfigurationValidationStrategy.test(container) :
+                anchorEntityConfigurationValidationStrategy.test(container);
+        }
+        log.warn("Entity statement {}, issued by {} is not an entity configuration", statement.getSubject(),
+                statement.getIssuer());
+        return false;
+    }
+
+    /**
+     * Validates the given entity statement against subordinate statement validation strategy.
+     * 
+     * @param statement entity statement to be validated
+     * @return true if entity statement was successfully validated, false otherwise
+     */
+    protected boolean validateSubordinateStatement(final EntityStatement<?> statement) {
+        if (statement instanceof SubordinateStatement subordinate) {
+            final Instant expiration = subordinate.getParsedPayload().getExpiration();
+            if (expiration == null) {
+                log.warn("Subordinate statement {}, issued by {} is missing mandatory expiration time",
+                        statement.getSubject(), statement.getIssuer());
+                return false;
+            }
+            final SubordinateStatementContainer container = new SubordinateStatementContainer(
+                    new SubordinateStatementCacheIdentifier(subordinate.getIssuer(), subordinate.getSubject()),
+                    subordinate, expiration, expiration);
+            return subordinateStatementValidationStrategy.test(container);
+        }
+        log.warn("Entity statement {}, issued by {} is not a subordinate statement", statement.getSubject(),
+                statement.getIssuer());
+        return false;
+    }
 }
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
index 6264bf4..34e7b45 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
@@ -38,7 +38,65 @@
         </property>
         <property name="providedTrustChainValidationStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.logic.DefaultProvidedTrustChainValidationStrategy"
-                p:metadataCache-ref="#{'%{idp.oidfed.op.automaticRegistration.ProvidedTrustChainMetadataCache:DefaultAutoRegisterFetchThroughProvidedTrustChainMetadataCache}'.trim()}" />
+                p:metadataCache-ref="#{'%{idp.oidfed.op.automaticRegistration.ProvidedTrustChainMetadataCache:DefaultAutoRegisterFetchThroughProvidedTrustChainMetadataCache}'.trim()}">
+                <property name="leafEntityConfigurationValidationStrategy">
+                    <bean parent="shibboleth.Conditions.Expression" c:expression="#custom.test(#input, null)"
+                        p:inputType="net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer">
+                        <property name="customObject">
+                            <bean class="net.shibboleth.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
+                                p:claimsValidator-ref="%{idp.oidfed.op.automaticRegistration.providedTrustChain.leaf.claimsValidator:shibboleth.oidfed.DefaultEntityConfigurationClaimsValidator}"
+                                p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                                <property name="headerValidator">
+                                    <bean class="net.shibboleth.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
+                                        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                                </property>
+                                <property name="containerIdValidationStrategy">
+                                    <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                                </property>
+                            </bean>
+                        </property>
+                    </bean>
+                </property>
+                <property name="anchorEntityConfigurationValidationStrategy">
+                    <bean parent="shibboleth.Conditions.Expression" c:expression="#custom.test(#input, null)"
+                        p:inputType="net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer">
+                        <property name="customObject">
+                            <bean class="net.shibboleth.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
+                                p:claimsValidator-ref="%{idp.oidfed.op.automaticRegistration.providedTrustChain.anchor.claimsValidator:shibboleth.oidfed.DefaultEntityConfigurationClaimsValidator}"
+                                p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                                <property name="headerValidator">
+                                    <bean class="net.shibboleth.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
+                                        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                                </property>
+                                <property name="containerIdValidationStrategy">
+                                    <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                                </property>
+                            </bean>
+                        </property>
+                    </bean>
+                </property>
+                <property name="subordinateStatementValidationStrategy">
+                    <bean parent="shibboleth.Conditions.Expression" c:expression="#custom.test(#input, null)"
+                        p:inputType="net.shibboleth.oidfed.metadata.cache.subordinate.SubordinateStatementContainer">
+                        <property name="customObject">
+                            <bean class="net.shibboleth.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
+                                p:claimsValidator-ref="%{idp.oidfed.op.automaticRegistration.providedTrustChain.subordinate.claimsValidator:shibboleth.oidfed.DefaultSubordinateStatementClaimsValidator}"
+                                p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                                <property name="headerValidator">
+                                    <bean class="net.shibboleth.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
+                                        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                                </property>
+                                <property name="containerIdValidationStrategy">
+                                    <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                                </property>
+                            </bean>
+                        </property>
+                    </bean>
+                </property>
+                <property name="clientIDLookupStrategy">
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultMetadataLookupExtensionContextClientIDLookupFunction" />
+                </property>
+            </bean>
         </property>
     </bean>
 
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
index 935557b..00bdd9c 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
@@ -55,7 +55,62 @@
         p:ignoreProvidedTrustChainCondition-ref="shibboleth.Conditions.FALSE">
         <property name="providedTrustChainValidationStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.logic.DefaultProvidedTrustChainValidationStrategy"
-                p:metadataCache-ref="#{'%{idp.oidfed.op.explicitRegistration.ProvidedTrustChainMetadataCache:DefaultFetchThroughProvidedTrustChainMetadataCache}'.trim()}" />
+                p:metadataCache-ref="#{'%{idp.oidfed.op.explicitRegistration.ProvidedTrustChainMetadataCache:DefaultFetchThroughProvidedTrustChainMetadataCache}'.trim()}">
+                <property name="leafEntityConfigurationValidationStrategy">
+                    <bean parent="shibboleth.Conditions.Expression" c:expression="#custom.test(#input, null)"
+                        p:inputType="net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer">
+                        <property name="customObject">
+                            <bean class="net.shibboleth.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
+                                p:claimsValidator-ref="%{idp.oidfed.op.explicitRegistration.providedTrustChain.leaf.claimsValidator:shibboleth.oidfed.DefaultExplicitRegistrationRequestClaimsValidator}"
+                                p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                                <property name="headerValidator">
+                                    <bean class="net.shibboleth.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
+                                        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                                </property>
+                                <property name="containerIdValidationStrategy">
+                                    <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                                </property>
+                            </bean>
+                        </property>
+                    </bean>
+                </property>
+                <property name="anchorEntityConfigurationValidationStrategy">
+                    <bean parent="shibboleth.Conditions.Expression" c:expression="#custom.test(#input, null)"
+                        p:inputType="net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer">
+                        <property name="customObject">
+                            <bean class="net.shibboleth.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
+                                p:claimsValidator-ref="%{idp.oidfed.op.explicitRegistration.providedTrustChain.anchor.claimsValidator:shibboleth.oidfed.DefaultEntityConfigurationClaimsValidator}"
+                                p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                                <property name="headerValidator">
+                                    <bean class="net.shibboleth.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
+                                        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                                </property>
+                                <property name="containerIdValidationStrategy">
+                                    <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                                </property>
+                            </bean>
+                        </property>
+                    </bean>
+                </property>
+                <property name="subordinateStatementValidationStrategy">
+                    <bean parent="shibboleth.Conditions.Expression" c:expression="#custom.test(#input, null)"
+                        p:inputType="net.shibboleth.oidfed.metadata.cache.subordinate.SubordinateStatementContainer">
+                        <property name="customObject">
+                            <bean class="net.shibboleth.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
+                                p:claimsValidator-ref="%{idp.oidfed.op.explicitRegistration.providedTrustChain.subordinate.claimsValidator:shibboleth.oidfed.DefaultSubordinateStatementClaimsValidator}"
+                                p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                                <property name="headerValidator">
+                                    <bean class="net.shibboleth.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
+                                        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                                </property>
+                                <property name="containerIdValidationStrategy">
+                                    <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                                </property>
+                            </bean>
+                        </property>
+                    </bean>
+                </property>
+            </bean>
         </property>
         <property name="providedTrustChainResolver">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.ExplicitClientRegistrationRequestTrustChainResolver" />
diff --git a/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-op.properties b/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-op.properties
index 650088f..e07b461 100644
--- a/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-op.properties
+++ b/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-op.properties
@@ -13,6 +13,9 @@ idp.oidfed.op.automaticRegistration.providedClaimsSetDecorationType = store
 idp.oidfed.op.automaticRegistration.clientInformationManager = shibboleth.oidc.ClientInformationManager
 idp.oidfed.op.automaticRegistration.FederationPolicyConstraints = shibboleth.oidfed.DefaultFederationPolicyConstraints
 idp.oidfed.op.automaticRegistration.AutoRegisteredTokenClaimsSetDecorators = 
+idp.oidfed.op.automaticRegistration.providedTrustChain.leaf.claimsValidator = shibboleth.oidfed.DefaultEntityConfigurationClaimsValidator
+idp.oidfed.op.automaticRegistration.providedTrustChain.anchor.claimsValidator = shibboleth.oidfed.DefaultEntityConfigurationClaimsValidator
+idp.oidfed.op.automaticRegistration.providedTrustChain.subordinate.claimsValidator = shibboleth.oidfed.DefaultSubordinateStatementClaimsValidator
 
 # metadata-lookup-ext-oidfed-beans
 idp.oidfed.op.automaticRegistration.ProvidedTrustChainMetadataCache = DefaultAutoRegisterFetchThroughProvidedTrustChainMetadataCache
@@ -64,6 +67,9 @@ idp.oidfed.op.explicitRegistration.validateRemoteJwks = shibboleth.Conditions.TR
 idp.oidfed.op.explicitRegistration.clientInformationManager = shibboleth.oidc.ClientInformationManager
 idp.oidfed.op.explicitRegistration.StorageService = shibboleth.StorageService
 idp.oidfed.op.explicitRegistration.FederationPolicyConstraints = shibboleth.oidfed.DefaultFederationPolicyConstraints
+idp.oidfed.op.explicitRegistration.providedTrustChain.leaf.claimsValidator = shibboleth.oidfed.DefaultExplicitRegistrationRequestClaimsValidator
+idp.oidfed.op.explicitRegistration.providedTrustChain.anchor.claimsValidator = shibboleth.oidfed.DefaultEntityConfigurationClaimsValidator
+idp.oidfed.op.explicitRegistration.providedTrustChain.subordinate.claimsValidator = shibboleth.oidfed.DefaultSubordinateStatementClaimsValidator
 
 # signed-keyset-beans
 idp.service.logging.oidfedconfig = OIDFED.SignedKeyset
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
index cbaaf12..8f41d3b 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
@@ -604,6 +604,35 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
     }
 
+    @Test
+    public void testWithPushedValidTrustChain_signedRequestObject_differentEntityIds()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        final String entityId = uniqueClientId();
+        final List<String> trustChain = List.of(
+                rpEntityConfiguration(entityId),
+                subordinateStatement(entityId, Map.of("openid_relying_party",
+                        new OIDCClientMetadata().toJSONObject())),
+                trustedAnchorConfiguration());
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("entity-statement+jwt"))
+                .keyID(rpKey.getKeyID())
+                .customParam("trust_chain", trustChain)
+                .build();
+
+        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), header, rpKey));
+        Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+    }
+
     @Test
     public void testWithPushedInvalidTrustChain_signedRequestObject()
             throws IOException, UnsupportedOperationException, URISyntaxException {
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
index fbfa99e..b9ad3d4 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
@@ -285,13 +285,24 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     }
 
     @Test
-    public void testValidTrustChain() throws Exception {
+    public void testInvalidTrustChain_noAudienceInLeaf() throws Exception {
         final String clientId = uniqueClientId();
         final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
                 subordinateStatement(clientId, Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
                 + "\", \"" + trustedAnchorConfiguration() + "\"]";
         setRequest("POST", trustChain, "application/trust-chain+json");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_request");
+    }
+
+    @Test
+    public void testValidTrustChain() throws Exception {
+        final String clientId = uniqueClientId();
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId) + "\", \"" +
+                subordinateStatement(clientId, Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
+                + "\", \"" + 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(),
@@ -304,7 +315,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     public void testValidTrustChain_resolveApi() throws Exception {
         final String clientId = uniqueClientId();
         request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId) + "\", \"" +
                 subordinateStatement(clientId, Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
                 + "\", \"" + trustedAnchorConfiguration() + "\"]";
         setRequest("POST", trustChain, "application/trust-chain+json");
@@ -322,7 +333,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testValidTrustChain_validMaxLengthInAnchor() throws Exception {
         final String clientId = uniqueClientId();
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId) + "\", \"" +
                 subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
                         Map.of("max_path_length", Integer.valueOf(0))) + "\", \"" + trustedAnchorConfiguration() + "\"]";
         setRequest("POST", trustChain, "application/trust-chain+json");
@@ -339,7 +350,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     public void testValidTrustChain_invalidMaxLengthInAnchor() throws Exception {
         final String clientId = uniqueClientId();
         final String intermediateId = uniqueIntermediateId();
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId) + "\", \"" +
                 subordinateStatement(intermediateId, intermediateKey, leafKey, clientId, Collections.emptyMap(),
                         Collections.emptyMap()) + "\", \"" +
                 subordinateStatement(anchorId, trustedAnchorKey, intermediateKey, intermediateId, Collections.emptyMap(),
@@ -354,7 +365,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         final String clientId = uniqueClientId();
         final String intermediateId1 = uniqueIntermediateId();
         final String intermediateId2 = uniqueIntermediateId();
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId) + "\", \"" +
                 subordinateStatement(intermediateId1, intermediateKey, leafKey, clientId, Collections.emptyMap(),
                         Collections.emptyMap()) + "\", \"" +
                 subordinateStatement(intermediateId2, intermediateKey, intermediateKey, intermediateId1, Collections.emptyMap(),
@@ -369,7 +380,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testValidTrustChain_validNamingConstraintInAnchor() throws Exception {
         final String clientId = uniqueClientId();
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId) + "\", \"" +
                 subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
                         Map.of("naming_constraints", Map.of("permitted", List.of(".federation.local")))) +
                 "\", \"" + trustedAnchorConfiguration() + "\"]";
@@ -386,7 +397,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testValidTrustChain_invalidNamingConstraintInAnchor() throws Exception {
         final String clientId = uniqueClientId();
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId) + "\", \"" +
                 subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
                         Map.of("naming_constraints", Map.of("permitted", List.of(".wrongfederation.local")))) + 
                 "\", \"" + trustedAnchorConfiguration() + "\"]";
@@ -398,7 +409,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testValidTrustChain_validEntityTypeInAnchor() throws Exception {
         final String clientId = uniqueClientId();
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId) + "\", \"" +
                 subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
                         Map.of("allowed_entity_types", List.of("openid_relying_party"))) + "\", \""  +
                         trustedAnchorConfiguration() + "\"]";
@@ -415,7 +426,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testValidTrustChain_invalidEmptyEntityTypeInAnchor() throws Exception {
         final String clientId = uniqueClientId();
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId) + "\", \"" +
                 subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
                         Map.of("allowed_entity_types", Collections.emptyList())) + "\", \""  +
                         trustedAnchorConfiguration() + "\"]";
@@ -427,7 +438,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testValidTrustChain_invalidEntityTypeInAnchor() throws Exception {
         final String clientId = uniqueClientId();
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId) + "\", \"" +
                 subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
                         Map.of("allowed_entity_types", List.of("openid_provider"))) + "\", \""  +
                         trustedAnchorConfiguration() + "\"]";
@@ -439,7 +450,8 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testInvalidTrustChain_subordinateKeyNotMatchingEntityConfiguration() throws Exception {
         final String clientId = uniqueClientId();
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId, initializeNewJwk("RSA", 2048, "mockNewKey"))
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId,
+                initializeNewJwk("RSA", 2048, "mockNewKey"))
                 + "\", \"" + subordinateStatement(clientId, Map.of("openid_relying_party", 
                         new OIDCClientMetadata().toJSONObject())) + "\", \"" + trustedAnchorConfiguration() + "\"]";
         setRequest("POST", trustChain, "application/trust-chain+json");
@@ -474,7 +486,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         final Scope scope = Scope.parse("openid profile email custom");
         metadata.setScope(scope);
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId, metadata) + "\", \"" +
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId, metadata) + "\", \"" +
                 subordinateStatement(clientId, Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
                 + "\", \"" + trustedAnchorConfiguration() + "\"]";
         setRequest("POST", trustChain, "application/trust-chain+json");
@@ -492,7 +504,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
     @Test
     public void testValidTrustChain_repeat() throws Exception {
         final String clientId = uniqueClientId();
-        final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
+        final String trustChain = "[\"" + explicitRegistrationRequest(clientId) + "\", \"" +
                 subordinateStatement(clientId, Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
                 + "\", \"" + trustedAnchorConfiguration() + "\"]";
         for (int i = 0; i < 2; i++) {
@@ -522,6 +534,13 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         assert providedClientId != null;
         Assert.assertEquals(providedClientId, expectedClientId);
         Assert.assertEquals(entityStatement.getSubject(), expectedClientId);
+        try {
+            final JWTClaimsSet claimsSet = entityStatement.getJwt().getJWTClaimsSet();
+            assert claimsSet != null;
+            Assert.assertEquals(claimsSet.getAudience(), List.of(providedClientId));
+        } catch (final java.text.ParseException e) {
+            Assert.fail("Could not parse the JWT claims set", e);
+        }
         assert storageService != null;
         final StorageRecord<String> storageRecord =
                 storageService.read(BaseStorageServiceClientInformationComponent.CONTEXT_NAME, providedClientId);
@@ -587,6 +606,14 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
         return explicitRegistrationRequest(clientId, leafKey, null, List.of(issuer), metadata, authorityHints);
     }
 
+    protected String explicitRegistrationRequest(final String clientId, final JWK jwk, final String... authorityHints)
+            throws URISyntaxException {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        return explicitRegistrationRequest(clientId, jwk, null, List.of(issuer), metadata, authorityHints);
+    }
+
     protected String explicitRegistrationRequest(final String clientId, final OIDCClientMetadata metadata,
             final String... authorityHints) throws URISyntaxException {
         return explicitRegistrationRequest(clientId, leafKey, null, List.of(issuer), metadata, authorityHints);

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


More information about the commits mailing list