[java-oidfed-common] 01/02: Improve metadata policy deserialization especially for scopes
Codeberg
noreply at shibboleth.net
Wed Sep 30 09:43:39 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/214a6eea64a25a7a1c1e042c96a8ce61ffb6612c
commit 214a6eea64a25a7a1c1e042c96a8ce61ffb6612c
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed Sep 30 12:27:12 2026 +0300
Improve metadata policy deserialization especially for scopes
- Also allow the use of lists for scope: split the space-separated string into lists only for strings
- For add, one_of, subset_of and superset_of: require lists as mandated by the spec
- Improve testing for resolve-entity flow
---
.../META-INF/net.shibboleth.idp/postconfig.xml | 6 ++
.../oidfed/flow/ResolveEntityFlowTest.java | 115 +++++++++++++++++++++
.../FederationMetadataPolicyDeserializer.java | 43 +++++---
3 files changed, 147 insertions(+), 17 deletions(-)
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 4c1797c..d5c5b82 100644
--- a/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -1190,6 +1190,12 @@
</key>
<bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_metadata" c:_1="Metadata is invalid or not found for the requested entity types" c:_2="400" />
</entry>
+ <entry>
+ <key>
+ <util:constant static-field="net.shibboleth.oidfed.profile.OidFederationEventIds.INVALID_METADATA_AGAINST_POLICY"/>
+ </key>
+ <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_metadata" c:_1="Metadata is invalid or not found for the requested entity types against the fedeationn metadata policy" c:_2="400" />
+ </entry>
</map>
</property>
</bean>
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/ResolveEntityFlowTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/ResolveEntityFlowTest.java
index 356d39b..299e483 100644
--- a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/ResolveEntityFlowTest.java
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/ResolveEntityFlowTest.java
@@ -47,6 +47,7 @@ import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.ErrorResponse;
import com.nimbusds.oauth2.sdk.Response;
+import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
import net.shibboleth.oidfed.messaging.impl.ResolveEntityResponse;
@@ -176,6 +177,120 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
assertErrorCode(result, "invalid_metadata");
}
+ @Test
+ public void testRPWithTrustedTrustAnchorScopeFilteredAgainstPolicy() throws Exception {
+ request.setMethod("GET");
+ final String clientId = uniqueClientId();
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setScope(Scope.parse("openid profile"));
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(clientId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("openid_relying_party", metadata.toJSONObject()))
+ .claim("metadata_policy", Map.of("openid_relying_party",
+ Map.of("scope", Map.of("subset_of", List.of("profile", "info")))))
+ .build();
+ final String subordinateStatement =
+ FederationJwtSupport.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet).serialize();
+
+ try {
+ mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId, metadata)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final ResolveEntityResponse parsedResponse =
+ parseSuccessResponse(result, ResolveEntityResponse.class);
+ final SignedJWT response = parsedResponse.getJWT();
+ Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+ Assert.assertEquals(response.getJWTClaimsSet().getSubject(), clientId);
+ final Map<String, Object> resultMetadata = response.getJWTClaimsSet().getJSONObjectClaim("metadata");
+ Assert.assertNotNull(resultMetadata);
+ assert resultMetadata != null;
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> rpMetadata = (Map<String, Object>) resultMetadata.get("openid_relying_party");
+ final String resultScope = (String) rpMetadata.get("scope");
+ Assert.assertEquals(resultScope, "profile");
+ Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
+ }
+
+ @Test
+ public void testRPWithTrustedTrustAnchorValidScopeAgainstPolicy() throws Exception {
+ request.setMethod("GET");
+ final String clientId = uniqueClientId();
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setScope(Scope.parse("openid profile"));
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(clientId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("openid_relying_party", metadata.toJSONObject()))
+ .claim("metadata_policy", Map.of("openid_relying_party",
+ Map.of("scope", Map.of("subset_of", List.of("profile", "info", "openid")))))
+ .build();
+ final String subordinateStatement =
+ FederationJwtSupport.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet).serialize();
+
+ try {
+ mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId, metadata)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final ResolveEntityResponse parsedResponse =
+ parseSuccessResponse(result, ResolveEntityResponse.class);
+ final SignedJWT response = parsedResponse.getJWT();
+ Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+ Assert.assertEquals(response.getJWTClaimsSet().getSubject(), clientId);
+ final Map<String, Object> resultMetadata = response.getJWTClaimsSet().getJSONObjectClaim("metadata");
+ Assert.assertNotNull(resultMetadata);
+ assert resultMetadata != null;
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> rpMetadata = (Map<String, Object>) resultMetadata.get("openid_relying_party");
+ final String resultScope = (String) rpMetadata.get("scope");
+ Assert.assertEquals(resultScope, "openid profile");
+ Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
+ }
+
+ @Test
+ public void testRPWithTrustedTrustAnchorInvalidScopeAgainstPolicy() throws Exception {
+ request.setMethod("GET");
+ final String clientId = uniqueClientId();
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setScope(Scope.parse("openid profile"));
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(clientId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("openid_relying_party", metadata.toJSONObject()))
+ .claim("metadata_policy", Map.of("openid_relying_party",
+ Map.of("scope", Map.of("superset_of", List.of("info")))))
+ .build();
+ final String subordinateStatement =
+ FederationJwtSupport.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet).serialize();
+
+ try {
+ mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId, metadata)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_metadata");
+ }
+
@Test
public void testOPWithTrustedTrustAnchor() throws Exception {
request.setMethod("GET");
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/policy/FederationMetadataPolicyDeserializer.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/policy/FederationMetadataPolicyDeserializer.java
index eb7d171..e03af5e 100644
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/policy/FederationMetadataPolicyDeserializer.java
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/policy/FederationMetadataPolicyDeserializer.java
@@ -94,36 +94,40 @@ public class FederationMetadataPolicyDeserializer extends JsonDeserializer<Metad
for (final String key : map.keySet().stream().filter(Objects::nonNull).toList()) {
switch (key) {
case "value":
- final Object value = FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
- arraysAsSpaceSeparatedList, claim, map.get("value"));
+ final Object value;
+ if (map.get("value") instanceof String) {
+ value = FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
+ arraysAsSpaceSeparatedList, claim, map.get("value"));
+ } else {
+ value = map.get("value");
+ }
policy.setValue(value != null ? value : Optional.empty());
break;
case "add":
- policy.setAdd(FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
- arraysAsSpaceSeparatedList, claim, map.get("add")));
+ policy.setAdd(transformObjectIntoList("add", map.get("add")));
break;
case "default":
- policy.setDefaultValue(FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
- arraysAsSpaceSeparatedList, claim, map.get("default")));
+ final Object defaultValue = map.get("default");
+ if (defaultValue instanceof String) {
+ policy.setDefaultValue(
+ FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
+ arraysAsSpaceSeparatedList, claim, defaultValue));
+ } else {
+ policy.setDefaultValue(defaultValue);
+ }
break;
case "essential":
policy.setEssential(map.get("essential") != null ?
Boolean.valueOf(String.valueOf(map.get("essential"))).booleanValue() : false);
break;
case "one_of":
- policy.setOneOfValues(transformObjectIntoList("one_of",
- FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
- arraysAsSpaceSeparatedList, claim, map.get("one_of"))));
+ policy.setOneOfValues(transformObjectIntoList("one_of", map.get("one_of")));
break;
case "subset_of":
- policy.setSubsetOfValues(transformObjectIntoList("subset_of",
- FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
- arraysAsSpaceSeparatedList, claim, map.get("subset_of"))));
+ policy.setSubsetOfValues(transformObjectIntoList("subset_of", map.get("subset_of")));
break;
case "superset_of":
- policy.setSupersetOfValues(transformObjectIntoList("superset_of",
- FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
- arraysAsSpaceSeparatedList, claim, map.get("superset_of"))));
+ policy.setSupersetOfValues(transformObjectIntoList("superset_of", map.get("superset_of")));
break;
case "regexp":
policy.setRegexp(map.get("regexp") == null ? null : "" + map.get("regexp"));
@@ -145,15 +149,20 @@ public class FederationMetadataPolicyDeserializer extends JsonDeserializer<Metad
* @param id the key for the map of objects
* @param object the object value
* @return the value for the key as list or null
- * @throws IOException if a non-null value could not be transformed into a list
+ * @throws IOException if a non-null value could not be transformed into a list or the list contained null elements
*/
@Nullable private List<Object> transformObjectIntoList(@Nonnull final String id,
@Nullable final Object object) throws IOException {
if (object instanceof List<?> list) {
- return list.stream().filter(Object.class::isInstance).map(Object.class::cast).toList();
+ final List<Object> result = list.stream().map(Object.class::cast).toList();
+ if (result.contains(null)) {
+ throw new IOException("The list value '" + object + "' for '" + id + "' contains null elements");
+ }
+ return result;
} else if (object != null) {
throw new IOException("The value '" + object + "' for '" + id + "' is not a list");
}
return null;
}
+
}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list