[java-oidfed-common] 01/02: Improve allowed_entity_types constraint to deal with null metadatas

Codeberg noreply at shibboleth.net
Thu Oct 8 11:27:50 UTC 2026


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

codeberg pushed a commit to branch main
in repository java-oidfed-common.

View the commit online:
https://codeberg.org/Shibboleth/java-oidfed-common/commit/2214e41f073d61a3801b1de15f5faec17c1f95f9

commit 2214e41f073d61a3801b1de15f5faec17c1f95f9
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Oct 8 11:13:12 2026 +0300

    Improve allowed_entity_types constraint to deal with null metadatas
    
    - The previous code could not deal with all null metadata in the chain statements
    - Included unit tests
---
 .../impl/DefaultAllowedEntityTypesConstraint.java  |  27 ++-
 .../DefaultAllowedEntityTypesConstraintTest.java   | 218 +++++++++++++++++++++
 2 files changed, 235 insertions(+), 10 deletions(-)

diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java
index ff054af..53dfb6e 100644
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java
@@ -37,6 +37,7 @@ import net.shibboleth.oidfed.metadata.payload.claim.impl.MetadataImpl;
 import net.shibboleth.oidfed.metadata.payload.impl.EntityConfigurationPayloadImpl;
 import net.shibboleth.oidfed.metadata.payload.impl.SubordinateStatementPayloadImpl;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.ConstraintViolationException;
@@ -91,7 +92,7 @@ public class DefaultAllowedEntityTypesConstraint extends AbstractFederationPolic
     @Override
     protected boolean doValidate(@Nonnull final List<String> constraintData,
             @Nonnull final SubordinateStatement subordinateStatement,
-            @Nonnull final List<EntityStatement<?>> trustChain) {
+            @Nonnull @NonnullElements final List<EntityStatement<?>> trustChain) {
         if (constraintData.contains("federation_entity")) {
             log.warn("The value 'federation_entity' is not allowed for allowed_entity_types");
             return false;
@@ -114,7 +115,9 @@ public class DefaultAllowedEntityTypesConstraint extends AbstractFederationPolic
                         log.warn("The entity type {} is not allowed in entity statement {}", entityType,
                                 entityStatement.getSubject());
                         for (int j = 0; j <= i; j++) {
-                            final EntityStatement<?> replacement = removeMetadata(entityType, trustChain.get(j));
+                            final EntityStatement<?> original = trustChain.get(j);
+                            assert entityType != null; assert original != null;
+                            final EntityStatement<?> replacement = removeMetadata(entityType, original);
                             if (replacement == null) {
                                 log.error("Could not remove the entity type {} from metadata for {}", entityType,
                                         entityStatement.getSubject());
@@ -138,23 +141,27 @@ public class DefaultAllowedEntityTypesConstraint extends AbstractFederationPolic
      * @param entityStatement entity statement holding the metadata
      * @return updated entity statement, or null if the operation failed
      */
-    @Nullable private EntityStatement<?> removeMetadata(final String entityType,
-            final EntityStatement<?> entityStatement) {
-        final Map<String, Map<String, Object>> metadataClaims =
-                new HashMap<>(entityStatement.getParsedPayload().getMetadata().getAllClaims());
+    @Nullable private EntityStatement<?> removeMetadata(@Nonnull final String entityType,
+            @Nonnull final EntityStatement<?> entityStatement) {
+        final Metadata metadata = entityStatement.getParsedPayload().getMetadata();
+        if (metadata == null) {
+            log.trace("Metadata is null, nothing to do");
+            return entityStatement;
+        }
+        final Map<String, Map<String, Object>> metadataClaims = new HashMap<>(metadata.getAllClaims());
         metadataClaims.remove(entityType);
         log.trace("Removed entity type {}", entityType);
-        final MetadataImpl metadata = new MetadataImpl(metadataClaims);
-        log.trace("Metadata contents {}", metadata);
+        final MetadataImpl resultMetadata = new MetadataImpl(metadataClaims);
+        log.trace("Metadata contents {}", resultMetadata);
         if (entityStatement instanceof EntityConfiguration entityConfiguration) {
             final EntityConfigurationPayloadImpl payload =
                     new EntityConfigurationPayloadImpl(entityConfiguration.getParsedPayload());
-            payload.setMetadata(metadata);
+            payload.setMetadata(resultMetadata);
             return new EntityConfigurationImpl(entityStatement.getJwt(), payload);
         } else if (entityStatement instanceof SubordinateStatement subordinateStatement) {
             final SubordinateStatementPayloadImpl payload =
                     new SubordinateStatementPayloadImpl(subordinateStatement.getParsedPayload());
-            payload.setMetadata(metadata);
+            payload.setMetadata(resultMetadata);
             return new SubordinateStatementImpl(entityStatement.getJwt(), payload);
         }
         return null;
diff --git a/oidfed-common-impl/src/test/java/net/shibboleth/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraintTest.java b/oidfed-common-impl/src/test/java/net/shibboleth/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraintTest.java
new file mode 100644
index 0000000..86bd122
--- /dev/null
+++ b/oidfed-common-impl/src/test/java/net/shibboleth/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraintTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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.oidfed.metadata.constraints.impl;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleAbstractTypeResolver;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.SubordinateStatement;
+import net.shibboleth.oidfed.metadata.impl.EntityConfigurationImpl;
+import net.shibboleth.oidfed.metadata.impl.SubordinateStatementImpl;
+import net.shibboleth.oidfed.metadata.jackson.JWKSetDeserializer;
+import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
+import net.shibboleth.oidfed.metadata.payload.claim.impl.MetadataImpl;
+import net.shibboleth.oidfed.testing.FederationJwtSupport;
+import net.shibboleth.oidfed.testing.TestFederationCredentials;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link DefaultAllowedEntityTypesConstraint}.
+ */
+ at SuppressWarnings("null")
+public class DefaultAllowedEntityTypesConstraintTest {
+
+    DefaultAllowedEntityTypesConstraint constraint;
+    ObjectMapper objectMapper;
+
+    String anchor = "https://anchor.example.org";
+    String intermediate = "https://intermediate.example.org";
+    String subject = "https://subject.example.org";
+
+    @BeforeClass
+    public void initMapper() {
+        objectMapper = new ObjectMapper();
+        objectMapper.registerModule(new JavaTimeModule());
+        final SimpleModule jacksonModule = new SimpleModule();
+        jacksonModule.addDeserializer(JWKSet.class, new JWKSetDeserializer());
+        final SimpleAbstractTypeResolver jacksonTypeResolver = new SimpleAbstractTypeResolver();
+        jacksonTypeResolver.addMapping(Metadata.class, MetadataImpl.class);
+        jacksonModule.setAbstractTypes(jacksonTypeResolver);
+        objectMapper.registerModule(jacksonModule);
+    }
+
+    @BeforeMethod
+    public void init() throws ComponentInitializationException {
+        constraint = new DefaultAllowedEntityTypesConstraint();
+        constraint.setObjectMapper(objectMapper);
+        constraint.setId("mockId");
+        constraint.initialize();
+    }
+
+    @Test
+    public void testNullValue_nullMetadata_returnsFalse() throws JsonProcessingException {
+        Assert.assertFalse(runConstraint(null, null, null));
+    }
+
+    @Test
+    public void testWrongValue_nullMetadata_returnsFalse() throws JsonProcessingException {
+        Assert.assertFalse(runConstraint("not a list", null, null));
+    }
+
+    @Test
+    public void testEmptyList_nullMetadata_returnsTrue() throws JsonProcessingException {
+        Assert.assertTrue(runConstraint(CollectionSupport.emptyList(), null, null));
+    }
+
+    @Test
+    public void testNullValue_emptySubordinateMetadata_returnsFalse() throws JsonProcessingException {
+        Assert.assertFalse(runConstraint(null, CollectionSupport.emptyMap(), null));
+    }
+
+    @Test
+    public void testWrongValue_emptySubordinateMetadata_returnsFalse() throws JsonProcessingException {
+        Assert.assertFalse(runConstraint("not a list", CollectionSupport.emptyMap(), null));
+    }
+
+    @Test
+    public void testEmptyList_emptySubordinateMetadata_returnsTrue() throws JsonProcessingException {
+        Assert.assertTrue(runConstraint(CollectionSupport.emptyList(), CollectionSupport.emptyMap(), null));
+    }
+
+    @Test
+    public void testNullValue_emptyConfigurationMetadata_returnsFalse() throws JsonProcessingException {
+        Assert.assertFalse(runConstraint(null, null, CollectionSupport.emptyMap()));
+    }
+
+    @Test
+    public void testWrongValue_emptyConfigurationMetadata_returnsFalse() throws JsonProcessingException {
+        Assert.assertFalse(runConstraint("not a list", null, CollectionSupport.emptyMap()));
+    }
+
+    @Test
+    public void testEmptyList_emptyConfigurationMetadata_returnsTrue() throws JsonProcessingException {
+        Assert.assertTrue(runConstraint(CollectionSupport.emptyList(), null, CollectionSupport.emptyMap()));
+    }
+
+    @Test
+    public void testNullValue_emptyMetadata_returnsFalse() throws JsonProcessingException {
+        Assert.assertFalse(runConstraint(null, CollectionSupport.emptyMap(), CollectionSupport.emptyMap()));
+    }
+
+    @Test
+    public void testWrongValue_emptyMetadata_returnsFalse() throws JsonProcessingException {
+        Assert.assertFalse(runConstraint("not a list", CollectionSupport.emptyMap(), CollectionSupport.emptyMap()));
+    }
+
+    @Test
+    public void testEmptyList_emptyMetadata_returnsTrue() throws JsonProcessingException {
+        Assert.assertTrue(runConstraint(CollectionSupport.emptyList(), CollectionSupport.emptyMap(),
+                CollectionSupport.emptyMap()));
+    }
+
+    protected boolean runConstraint(@Nullable final Object value,
+            @Nullable final Map<String, Map<String,Object>> subordinateMetadata,
+            @Nullable final Map<String, Map<String,Object>> leafMetadata) throws JsonProcessingException {
+        final SubordinateStatement subordinateStatement =
+                subordinateStatement(intermediate, anchor, subordinateMetadata);
+        final List<EntityStatement<?>> trustChain = new ArrayList<>(List.of(
+                entityConfiguration(subject, leafMetadata, TestFederationCredentials.signingRSKey()),
+                subordinateStatement(subject, intermediate, null),
+                subordinateStatement,
+                entityConfiguration(anchor, null, TestFederationCredentials.trustAnchorKey())));
+        return constraint.validate(value, subordinateStatement, trustChain);
+    }
+
+    @Test
+    public void testEmptyList_leafRpOpMetadata_onlyRpAllowed_opMetadataRemoved() throws JsonProcessingException {
+        final SubordinateStatement subordinateStatement =
+                subordinateStatement(intermediate, anchor, null);
+        final List<EntityStatement<?>> trustChain = new ArrayList<>(List.of(
+                entityConfiguration(subject, Map.of(
+                        "openid_provider", new AuthorizationServerMetadata(Issuer.parse(subject)).toJSONObject(),
+                        "openid_relying_party", new OIDCClientInformation(
+                                new ClientID(subject), new OIDCClientMetadata()).toJSONObject()),
+                        TestFederationCredentials.signingRSKey()),
+                subordinateStatement(subject, intermediate, null),
+                subordinateStatement,
+                entityConfiguration(anchor, null, TestFederationCredentials.trustAnchorKey())));
+        Assert.assertTrue(constraint.validate(CollectionSupport.listOf("openid_relying_party"), subordinateStatement,
+                trustChain));
+        Assert.assertNull(trustChain.get(0).getParsedPayload().getMetadata().getOpenidProviderMetadata());
+        Assert.assertNotNull(trustChain.get(0).getParsedPayload().getMetadata().getOpenidRelyingPartyMetadata());
+    }
+
+    @Nonnull
+    protected SubordinateStatement subordinateStatement(@Nonnull final String entityId, @Nonnull final String issuer,
+            @Nullable final Map<String, Map<String,Object>> metadata) throws JsonProcessingException {
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(issuer)
+                .subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(TestFederationCredentials.trustAnchorKey()).toJSONObject(true));
+        if (metadata != null) {
+            builder.claim("metadata", metadata);
+        }
+        final SignedJWT jwt = FederationJwtSupport.entityStatement(
+                JWSAlgorithm.RS256, TestFederationCredentials.trustAnchorKey(), builder.build());
+        assert jwt != null;
+        return SubordinateStatementImpl.parse(jwt, objectMapper);
+    }
+
+    protected EntityConfiguration entityConfiguration(@Nonnull final String entityId,
+            @Nullable final Map<String, Map<String,Object>> metadata, @Nonnull final JWK key)
+            throws JsonProcessingException {
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(key).toJSONObject(true));
+        if (metadata != null) {
+            builder.claim("metadata", metadata);
+        }
+        final SignedJWT jwt = FederationJwtSupport.entityStatement(JWSAlgorithm.RS256, key, builder.build());
+        assert jwt != null;
+        return EntityConfigurationImpl.parse(jwt, objectMapper);
+    }
+
+}

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


More information about the commits mailing list