[java-idp-oidc] branch main updated: JOIDC-72 - Expand the set of supported claims in dynamic client registration

Henri Mikkonen henri.mikkonen at iki.fi
Fri Feb 18 14:07:02 UTC 2022


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

hjmikkon pushed a commit to branch main
in repository java-idp-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=8b258edff55dd9971f824f382d160d29493b813a

The following commit(s) were added to refs/heads/main by this push:
     new 8b258edf JOIDC-72 - Expand the set of supported claims in dynamic client registration
8b258edf is described below

commit 8b258edff55dd9971f824f382d160d29493b813a
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Feb 18 16:05:48 2022 +0200

    JOIDC-72 - Expand the set of supported claims in dynamic client registration
    
    https://shibboleth.atlassian.net/browse/JOIDC-72
    
    Initial version that includes any claims from the request that were not yet
    included in the response, but were mentioned in the metadata policy and can
    thus be considered as recognized/known by the OP.
---
 .../impl/AddRemainingClaimsToClientMetadata.java   | 116 +++++++++++++++++++
 .../idp/flows/oidc/register/register-beans.xml     |   3 +
 .../idp/flows/oidc/register/register-flow.xml      |   1 +
 .../oidc/op/profile/flow/RegistrationFlowTest.java |  70 ++++++++++--
 .../AddRemainingClaimsToClientMetadataTest.java    | 124 +++++++++++++++++++++
 5 files changed, 303 insertions(+), 11 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddRemainingClaimsToClientMetadata.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddRemainingClaimsToClientMetadata.java
new file mode 100644
index 00000000..626fef17
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddRemainingClaimsToClientMetadata.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.impl;
+
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCClientRegistrationMetadataPolicyContext;
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Adds the remaining claims from the input metadata to the output {@link OIDCClientMetadata}, which haven't been
+ * added there already, but which are recognized in the metadata policy.
+ */
+public class AddRemainingClaimsToClientMetadata extends AbstractOIDCClientMetadataPopulationAction {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(AddRemainingClaimsToClientMetadata.class);
+
+    /** Strategy that will return {@link OIDCClientRegistrationMetadataPolicyContext}. */
+    @Nonnull private Function<ProfileRequestContext, OIDCClientRegistrationMetadataPolicyContext>
+        registrationMetadataPolicyContextLookupStrategy;
+
+    /** The metadata policy used for finding out remaining claims. */
+    @Nullable private Map<String, MetadataPolicy> metadataPolicy;
+
+    public AddRemainingClaimsToClientMetadata() {
+        registrationMetadataPolicyContextLookupStrategy = 
+                new ChildContextLookup<>(OIDCClientRegistrationMetadataPolicyContext.class, false).compose(
+                        new InboundMessageContextLookup());
+    }
+    
+    /**
+     * Set the strategy that will return {@link OIDCClientRegistrationMetadataPolicyContext}.
+     * 
+     * @param strategy Strategy that will return {@link OIDCClientRegistrationMetadataPolicyContext}.
+     */
+    public void setRegistrationMetadataPolicyContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCClientRegistrationMetadataPolicyContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        registrationMetadataPolicyContextLookupStrategy = Constraint.isNotNull(strategy,
+                "Registration metadata policy context lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        final OIDCClientRegistrationMetadataPolicyContext metadataPolicyContext =
+                registrationMetadataPolicyContextLookupStrategy.apply(profileRequestContext);
+        if (metadataPolicyContext == null) {
+            log.debug("{} No OIDCClientRegistrationMetadataPolicyContext found, nothing to do", getLogPrefix());
+            return false;
+        }
+        
+        metadataPolicy = metadataPolicyContext.getMetadataPolicy();
+        if (metadataPolicy == null || metadataPolicy.isEmpty()) {
+            log.debug("{} No metadata policy found, nothing to do", getLogPrefix());
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final JSONObject input = getInputMetadata().toJSONObject();
+        final JSONObject initialOutput = getOutputMetadata().toJSONObject();
+        for (final String claim : input.keySet()) {
+            if (!initialOutput.containsKey(claim)) {
+                if (metadataPolicy.containsKey(claim)) {
+                    log.debug("{} added {} to the output metadata", getLogPrefix(), claim);
+                    getOutputMetadata().setCustomField(claim, input.get(claim));
+                } else {
+                    log.warn("{} input contained claim {} that is not recognized and is thus ignored", getLogPrefix(),
+                            claim);
+                }
+            }
+        }
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
index 07696138..754a5013 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
@@ -138,6 +138,9 @@
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddRequestObjectSecurityConfigurationToClientMetadata"
         p:allowSignatureNone="%{idp.oidc.dynreg.allowNoneForRequestSigning:true}" scope="prototype" />
 
+    <bean id="AddRemainingClaimsToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddRemainingClaimsToClientMetadata" />
+
     <bean id="shibboleth.oidc.ClientInformationManager"
         class="net.shibboleth.oidc.metadata.impl.StorageServiceClientInformationManager"
         scope="prototype" p:id="OIDCClientInformationManager"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-flow.xml
index 14bc0bb6..98a8878e 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-flow.xml
@@ -50,6 +50,7 @@
         <evaluate expression="AddClientNameToClientMetadata" />
         <evaluate expression="AddSecurityConfigurationToClientMetadata" />
         <evaluate expression="AddRequestObjectSecurityConfigurationToClientMetadata" />
+        <evaluate expression="AddRemainingClaimsToClientMetadata" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="BuildResponseMessage" />
     </action-state>
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java
index 110ceaf0..8ef9d7b1 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java
@@ -128,12 +128,36 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
                 .toAuthorizationHeader());
         assertSuccessfulResponse(flowExecutor.launchExecution(FLOW_ID, null, externalContext));
     }
-    
+
+    @Test
+    public void testAccessToken_successCustomClaimIgnored() throws ParseException, IOException, net.minidev.json.parser.ParseException, NoSuchAlgorithmException, DataSealerException, ComponentInitializationException, FlowException {
+        setJsonRequest("POST", buildRequestMessage(redirectUri, "\"customClaim\":\"customValue\""));
+        request.addHeader("Authorization", buildRegistrationAccessToken("[\"https://example.org/cb\"]")
+                .toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertSuccessfulResponse(result);
+        final OIDCClientInformationResponse parsedResponse = 
+                parseSuccessResponse(result, OIDCClientInformationResponse.class);
+        Assert.assertNull(parsedResponse.getOIDCClientInformation().getOIDCMetadata().getCustomField("customClaim"));
+    }
+
+    @Test
+    public void testAccessToken_successCustomClaimInPolicyAdded() throws ParseException, IOException, net.minidev.json.parser.ParseException, NoSuchAlgorithmException, DataSealerException, ComponentInitializationException, FlowException {
+        setJsonRequest("POST", buildRequestMessage(redirectUri, "\"customClaim\":\"customValue\""));
+        request.addHeader("Authorization", buildRegistrationAccessToken("[\"https://example.org/cb\"]", new String[] {"customClaim"})
+                .toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertSuccessfulResponse(result);
+        final OIDCClientInformationResponse parsedResponse = 
+                parseSuccessResponse(result, OIDCClientInformationResponse.class);
+        Assert.assertEquals(parsedResponse.getOIDCClientInformation().getOIDCMetadata().getCustomField("customClaim"), "customValue");
+    }
+
     @Test
     public void testAccessToken_noPolicyNoRedirectUri() throws NoSuchAlgorithmException, DataSealerException,
             ComponentInitializationException {
         setJsonRequest("POST", "{ \"test\":false }");
-        request.addHeader("Authorization", buildRegistrationAccessToken("mockDynRegClient", null)
+        request.addHeader("Authorization", buildRegistrationAccessToken("mockDynRegClient", (String) null, (String[]) null)
                 .toAuthorizationHeader());
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         assertErrorCode(result, "invalid_redirect_uri");
@@ -165,11 +189,27 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
         Assert.assertEquals(storedMetadata.getPolicyURIEntries(), metadata.getPolicyURIEntries());
     }
 
-    protected BearerAccessToken buildRegistrationAccessToken(final String redirectUriSubset) throws NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
-        return buildRegistrationAccessToken("mockRpId", redirectUriSubset);
+    protected BearerAccessToken buildRegistrationAccessToken(final String redirectUriSubset, final String... additionalPolicyClaims) throws NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
+        return buildRegistrationAccessToken("mockRpId", redirectUriSubset, additionalPolicyClaims);
     }
 
-    protected BearerAccessToken buildRegistrationAccessToken(final String rpId, final String redirectUriSubset) throws NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
+    protected BearerAccessToken buildRegistrationAccessToken(final String rpId, final String redirectUriSubset, final String... additionalPolicyClaims) throws NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
+        final StringBuilder metadata = new StringBuilder();
+        if (additionalPolicyClaims != null) {
+            for (int i = 0; i < additionalPolicyClaims.length; i++) {
+                metadata.append("\"" + additionalPolicyClaims[i] + "\":{}");
+                if (i + 1 < additionalPolicyClaims.length) {
+                    metadata.append(",");
+                }
+            }
+        }
+        if (redirectUriSubset != null) {
+            if (metadata.length() > 0) {
+                metadata.append(",");
+            }
+            metadata.append("\"redirect_uris\":{\"subset_of\":" + redirectUriSubset + "}");
+        }
+        
         final String json = "{" +
                 "\"prncpl\":\"jdoe\"," +
                 "\"type\":\"rat\"," +
@@ -177,16 +217,24 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
                 "\"iat\":" + Instant.now().getEpochSecond() + "," +
                 "\"jti\":\"" + idGenerator.generateIdentifier() + "\"," + 
                 "\"rp_id\":\"" + rpId + "\"," + 
-                "\"metadata\":" + (redirectUriSubset != null ? 
-                        "{\"redirect_uris\":{\"subset_of\":" + redirectUriSubset + "}" : "null") +"}}";
+                "\"metadata\":" + (metadata.length() == 0 ? "null" : "{" + metadata.toString()) + "}" +
+                "}";
         return new BearerAccessToken(BaseOIDCResponseActionTest.initializeDataSealer().wrap(json,
                 Instant.now().plusSeconds(30)));
     }
-    
+
     protected String buildRequestMessage(final String redirectUri) {
-        return new StringBuilder("{ \"redirect_uris\":[\"" + redirectUri + "\"],")
+        return buildRequestMessage(redirectUri, null);
+    }
+
+    protected String buildRequestMessage(final String redirectUri, final String customPart) {
+        final StringBuilder result = new StringBuilder("{ \"redirect_uris\":[\"" + redirectUri + "\"],")
                 .append("\"policy_uri#en\":\"https://policy.org/english\",")
-                .append("\"policy_uri#fi\":\"https://policy.org/finnish\"")
-                .append("}").toString();
+                .append("\"policy_uri#fi\":\"https://policy.org/finnish\"");
+        if (customPart == null) {
+            return result.append("}").toString();
+        } else {
+            return result.append("," + customPart + "}").toString();
+        }
     }
 }
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddRemainingClaimsToClientMetadataTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddRemainingClaimsToClientMetadataTest.java
new file mode 100644
index 00000000..eabeecc4
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddRemainingClaimsToClientMetadataTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.impl;
+
+import java.util.Map;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCClientRegistrationMetadataPolicyContext;
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link AddRemainingClaimsToClientMetadata}.
+ */
+public class AddRemainingClaimsToClientMetadataTest extends BaseOIDCClientMetadataPopulationTest {
+
+    AddRemainingClaimsToClientMetadata action;
+    
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        action = new AddRemainingClaimsToClientMetadata();
+        action.initialize();
+    }
+    
+    @Override
+    protected AbstractOIDCClientMetadataPopulationAction constructAction() {
+        return new AddRemainingClaimsToClientMetadata();
+    }
+    
+    protected void setUpContext(final OIDCClientMetadata input, final OIDCClientMetadata output,
+            final Map<String, MetadataPolicy> metadataPolicy) 
+            throws ComponentInitializationException {
+        super.setUpContext(input, output);
+        final OIDCClientRegistrationMetadataPolicyContext policyCtx = profileRequestCtx.getInboundMessageContext()
+                .getSubcontext(OIDCClientRegistrationMetadataPolicyContext.class);;
+        policyCtx.setMetadataPolicy(metadataPolicy);
+    }
+
+    @Test
+    public void testUnknownNotIncludedWhenNullPolicy() throws ComponentInitializationException {
+        final OIDCClientMetadata request = new OIDCClientMetadata();
+        request.setCustomField("nonStandardField", "mockValue");
+        final OIDCClientMetadata result = new OIDCClientMetadata();
+        setUpContext(request, result);
+        Assert.assertNull(action.execute(requestCtx));
+        final JSONObject resultJson = result.toJSONObject(true);
+        Assert.assertFalse(resultJson.containsKey("nonStandardField"));
+    }
+
+    @Test
+    public void testUnknownNotIncludedWhenEmptyPolicy() throws ComponentInitializationException {
+        final OIDCClientMetadata request = new OIDCClientMetadata();
+        request.setCustomField("nonStandardField", "mockValue");
+        final OIDCClientMetadata result = new OIDCClientMetadata();
+        setUpContext(request, result, Map.of());
+        Assert.assertNull(action.execute(requestCtx));
+        final JSONObject resultJson = result.toJSONObject(true);
+        Assert.assertFalse(resultJson.containsKey("nonStandardField"));
+    }
+
+    @Test
+    public void testUnknownNotIncludedWhenNotInPolicy() throws ComponentInitializationException {
+        final OIDCClientMetadata request = new OIDCClientMetadata();
+        request.setCustomField("nonStandardField", "mockValue");
+        final OIDCClientMetadata result = new OIDCClientMetadata();
+        setUpContext(request, result, Map.of("anotherField", new MetadataPolicy()));
+        Assert.assertNull(action.execute(requestCtx));
+        final JSONObject resultJson = result.toJSONObject(true);
+        Assert.assertFalse(resultJson.containsKey("nonStandardField"));
+    }
+
+    @Test
+    public void testKnownIncluded() throws ComponentInitializationException {
+        final OIDCClientMetadata request = new OIDCClientMetadata();
+        request.setCustomField("nonStandardField", "mockValue");
+        final OIDCClientMetadata result = new OIDCClientMetadata();
+        setUpContext(request, result, Map.of("nonStandardField", new MetadataPolicy()));
+        Assert.assertNull(action.execute(requestCtx));
+        final JSONObject resultJson = result.toJSONObject(true);
+        Assert.assertTrue(resultJson.containsKey("nonStandardField"));
+    }
+
+    @Test
+    public void testOnlyKnownOnesAreIncluded() throws ComponentInitializationException {
+        final OIDCClientMetadata request = new OIDCClientMetadata();
+        request.setCustomField("nonStandardField1", "mockValue1");
+        request.setCustomField("nonStandardField2", "mockValue2");
+        request.setCustomField("nonStandardField3", "mockValue3");
+        request.setCustomField("nonStandardField4", "mockValue4");
+        final OIDCClientMetadata result = new OIDCClientMetadata();
+        setUpContext(request, result, Map.of(
+                "nonStandardField1", new MetadataPolicy(),
+                "nonStandardField4", new MetadataPolicy()));
+        Assert.assertNull(action.execute(requestCtx));
+        final JSONObject resultJson = result.toJSONObject(true);
+        Assert.assertTrue(resultJson.containsKey("nonStandardField1"));
+        Assert.assertEquals(resultJson.get("nonStandardField1"), "mockValue1");
+        Assert.assertFalse(resultJson.containsKey("nonStandardField2"));
+        Assert.assertFalse(resultJson.containsKey("nonStandardField3"));
+        Assert.assertTrue(resultJson.containsKey("nonStandardField4"));
+        Assert.assertEquals(resultJson.get("nonStandardField4"), "mockValue4");
+    }
+}
\ 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