[java-plugin-shibd-oidc] branch main updated: Support configuration of the resource parameter on AuthZ request

Phil Smart philip.smart at jisc.ac.uk
Fri Oct 10 15:55:48 UTC 2025


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

philsmart pushed a commit to branch main
in repository java-plugin-shibd-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-shibd-oidc.git;a=commit;h=125e8746e97d9d1f23e7ca5b13a1dec93f9cc789

The following commit(s) were added to refs/heads/main by this push:
     new 125e874  Support configuration of the resource parameter on AuthZ request
125e874 is described below

commit 125e8746e97d9d1f23e7ca5b13a1dec93f9cc789
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Oct 10 16:55:46 2025 +0100

    Support configuration of the resource parameter on AuthZ request
---
 .../sp/oidc/profile/OIDCInitiatorConstants.java    |   3 +
 .../idp/flows/sp/initiator/oidc/oidc-beans.xml     |   6 ++
 .../sp/oidc/flows/OIDCAuthenticationFlowTest.java  |  62 ++++++++---
 .../oidc/profile/impl/ResourceLookupStrategy.java  | 101 ++++++++++++++++++
 .../profile/request/impl/AddResourceHandler.java   |  54 ++++++++++
 .../profile/impl/ResourceLookupStrategyTest.java   | 117 +++++++++++++++++++++
 6 files changed, 328 insertions(+), 15 deletions(-)

diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCInitiatorConstants.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCInitiatorConstants.java
index 107bd27..033e0fb 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCInitiatorConstants.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCInitiatorConstants.java
@@ -45,6 +45,9 @@ public final class OIDCInitiatorConstants {
     
     /** Scope parameter */
     @Nonnull @NotEmpty public static final String SCOPE = "scope";
+    
+    /** Resource parameter */
+    @Nonnull @NotEmpty public static final String RESOURCE = "resource";
 
     /** authnContextClassRef input parameter. */
     @Nonnull @NotEmpty public static final String AUTHN_CONTEXT_CLASS_REF = AuthnContextClassRef.DEFAULT_ELEMENT_LOCAL_NAME;
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
index 4646a30..04fa62e 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
@@ -175,6 +175,12 @@
                                 <bean class="net.shibboleth.sp.oidc.profile.impl.PromptLookupStrategy" scope="prototype"/>
                             </property>
                         </bean>
+                        <bean id="AddResourceIndicators" scope="prototype"
+                            class="net.shibboleth.sp.oidc.profile.request.impl.AddResourceHandler">
+                            <property name="parameterValueLookupStrategy">
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.ResourceLookupStrategy" scope="prototype"/>
+                            </property>
+                        </bean>
                     </list>
                 </property>
             </bean>
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java
index a4b2d97..ad0a9a4 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java
@@ -14,6 +14,9 @@
 
 package net.shibboleth.sp.oidc.flows;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -120,8 +123,8 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
         assertFlowExecutionOutcome(result.getOutcome());
         
         final AuthenticationRequest req = validateOutputMessage(result);
-        assertValueForParameter(req, "max_age", 0l);
-        assertValueForParameter(req, "prompt", "login");
+        assertValueForParameterFromRequestObject(req, "max_age", 0l);
+        assertValueForParameterFromRequestObject(req, "prompt", "login");
     }
     
     /**
@@ -145,7 +148,7 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
         assertFlowExecutionOutcome(result.getOutcome());
         
         final AuthenticationRequest req = validateOutputMessage(result);
-        assertValueForParameter(req, "max_age", 60l);
+        assertValueForParameterFromRequestObject(req, "max_age", 60l);
     }
     
     /**
@@ -169,7 +172,7 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
         assertFlowExecutionOutcome(result.getOutcome());
         
         final AuthenticationRequest req = validateOutputMessage(result);
-        assertValueForParameter(req, "prompt", "none");
+        assertValueForParameterFromRequestObject(req, "prompt", "none");
     }
     
     /**
@@ -196,7 +199,36 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
         assertFlowExecutionOutcome(result.getOutcome());
         
         final AuthenticationRequest req = validateOutputMessage(result);
-        assertValueForParameter(req, "acr_values", "loa1 loa2");
+        assertValueForParameterFromRequestObject(req, "acr_values", "loa1 loa2");
+    }
+    
+    /**
+     * Basic flow test with two resource indicators.
+     * 
+     * @throws Exception on error
+     */
+    @Test
+    public void testResourceIndicatorsFromAgent() throws Exception {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();        
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        input.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL); 
+        final DDF resourceList = input.addmember(OIDCInitiatorConstants.RESOURCE).list();
+        resourceList.add(new DDF(null).string("https://cal.example.com"));
+        resourceList.add(new DDF(null).string("https://mail.example.com"));
+        
+        setApplicationRequest("test-oidc-application", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        final AuthenticationRequest req = validateOutputMessage(result);
+        final var resources = req.getResources();
+        assertNotNull(resources);
+        assertEquals(resources.size(), 2);
     }
     
     /**
@@ -220,7 +252,7 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
         assertFlowExecutionOutcome(result.getOutcome());
         
         final AuthenticationRequest req = validateOutputMessage(result);
-        assertValueForParameter(req, "scope", "openid email profile");
+        assertValueForParameterFromRequestObject(req, "scope", "openid email profile");
     }
     
     
@@ -245,7 +277,7 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
         assertFlowExecutionOutcome(result.getOutcome());
 
         final AuthenticationRequest req = validateOutputMessage(result);
-        assertValueForParameter(req, "display", "page");
+        assertValueForParameterFromRequestObject(req, "display", "page");
     }
     
     /**
@@ -319,18 +351,17 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
     }
     
     /**
-     * Find the String value for the parameter from the {@link AuthenticationRequest}, either in the params or inside
-     * a claim in the Request Object.
+     * Find the String value for the given parameter from the {@link AuthenticationRequest} inside
+     * a claim in the Request Object, and assert its value is equal to the one given.
      * 
      * @param request the request to locate the parameter from
      * @param claim the claim to find
      * 
-     * @return the value if found.
      * @throws java.text.ParseException 
      */
-    private void assertValueForParameter(final AuthenticationRequest request, @Nonnull final String claim,
-            final Object expectedClaimValue) 
-                throws java.text.ParseException {
+    private void assertValueForParameterFromRequestObject(final AuthenticationRequest request, 
+            @Nonnull final String claim, final Object expectedClaimValue) throws java.text.ParseException {
+        
         JWTClaimsSet requestObjectClaims = null;
         if (request.getRequestObject() instanceof final SignedJWT signed) {
             requestObjectClaims = signed.getJWTClaimsSet();
@@ -354,8 +385,9 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
      * @return decoded message
      * @throws MessageDecodingException 
      */
-    @Nonnull protected AuthenticationRequest decodeRedirect(@Nullable final String url, @Nullable final String relayState)
-            throws MessageDecodingException {
+    @Nonnull protected AuthenticationRequest decodeRedirect(@Nullable final String url, 
+            @Nullable final String relayState) throws MessageDecodingException {
+        
         if (url == null) {
             throw new MessageDecodingException("URl is null");
         }
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ResourceLookupStrategy.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ResourceLookupStrategy.java
new file mode 100644
index 0000000..169ccdf
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ResourceLookupStrategy.java
@@ -0,0 +1,101 @@
+/*
+ * 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.sp.oidc.profile.impl;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.oidc.profile.config.OIDCAuthenticationRelyingPartyProfileConfiguration;
+import net.shibboleth.oidc.profile.config.OIDCAuthorizationConfiguration;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
+
+/**
+ * A strategy function that extracts and validates resource indicators from the inbound {@link MessageContext} and 
+ * returns them as a list of {@link URI} objects. Takes them from the inbound {@link DDF} object if they exist, if 
+ * they do not exist or they were disallowed by the Hub configuration they are taken from the relying party 
+ * configuration.
+ * 
+ * <p>Validation ensures the URIs are absolute and do not contain a fragment component, per RFC. Any that aren't
+ * are filtered out.</p>
+ */
+public class ResourceLookupStrategy extends AbstractAgentAndRelyingPartyContextLookupFunction<Set<URI>>{
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ResourceLookupStrategy.class);
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<URI> apply(final MessageContext messageContext) {
+        final DDF input = getDDF(messageContext, DDFDirection.INPUT);
+        if (input == null) {
+            return CollectionSupport.emptySet();
+        }
+        final OIDCAuthenticationRelyingPartyProfileConfiguration rpConfig = 
+                getOIDCRelyingPartyProfileConfiguration(messageContext);
+        
+        Set<String> resources = input.getmember(OIDCInitiatorConstants.RESOURCE).asList().stream()
+                .map(DDF::string)
+                .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableSet())).get();
+        
+        if (!resources.isEmpty() && isFeatureDisallowed(OIDCAuthorizationConfiguration.FEATURE_RESOURCE_INDICATOR, 
+                messageContext)) {
+            log.warn("Agent disallowed from overriding resource indicators");
+            resources = CollectionSupport.emptySet();
+        }
+        
+        if (rpConfig != null && resources.isEmpty()) {
+            final Set<String> profileResources = rpConfig.getResourceIndicators(PRC_LOOKUP.apply(messageContext));
+            if (profileResources != null) {
+                resources = profileResources;
+            }
+        }       
+
+        return resources.stream()
+            .map(resourceString -> {
+                try {
+                    return new URI(resourceString);
+                } catch (final URISyntaxException e) {
+                    log.debug("Invalid URI '{}'", resourceString);
+                    return null;
+                }
+            })
+            .filter(Objects::nonNull)
+            .filter(uri -> {
+                if (!uri.isAbsolute()) {
+                    log.debug("Resource URI '{}' is not absolute, excluded", uri);
+                    return false;
+                }
+                if (uri.getFragment() != null) {
+                    log.debug("Resource URI '{}' has a fragment component, excluded", uri);
+                    return false;
+                }
+                return true;
+            })
+            .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableSet())).get();
+
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/request/impl/AddResourceHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/request/impl/AddResourceHandler.java
new file mode 100644
index 0000000..a3ee739
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/request/impl/AddResourceHandler.java
@@ -0,0 +1,54 @@
+/*
+ * 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.sp.oidc.profile.request.impl;
+
+import java.net.URI;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.oidc.profile.impl.AbstractAuthenticationRequestParameterValueMessageHandler;
+
+
+/** 
+ * A message handler that adds OAuth 2.0 resource indicators to the authentication request.
+ */
+public class AddResourceHandler extends AbstractAuthenticationRequestParameterValueMessageHandler<Set<URI>> {    
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddResourceHandler.class);
+    
+    /**
+     * Constructor.
+     */
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    protected AddResourceHandler() {
+        super((Class)Set.class);
+    }
+    
+    @Override
+    protected void doInvoke(@Nonnull final MessageContext messageContext) 
+            throws MessageHandlerException {
+        final Set<URI> resources = getParameterValue(messageContext);
+        log.debug("{} Adding resource indicators '{}'", getLogPrefix(), resources);
+        getAuthenticationRequest().setResources(resources);
+        
+    }
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ResourceLookupStrategyTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ResourceLookupStrategyTest.java
new file mode 100644
index 0000000..43a5e2b
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ResourceLookupStrategyTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.sp.oidc.profile.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.net.URI;
+import java.util.Set;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.oidc.profile.config.OIDCAuthorizationConfiguration;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
+import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
+import net.shibboleth.sp.profile.InitiatorConstants;
+import net.shibboleth.sp.profile.SPConstants;
+
+/**
+ * Tests for {@link ResourceLookupStrategyTest}.
+ */
+public class ResourceLookupStrategyTest extends AbstractAuthenticationLookupStrategyTest {
+    
+    /** The strategy to test.*/
+    private ResourceLookupStrategy strategy;
+
+    /** The DDF to store input parameters.*/
+    private DDF ddf;
+
+    @Override
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        super.setUp();
+        strategy = new ResourceLookupStrategy();
+
+        ddf = new DDF(null).structure();
+        ddf.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
+        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        ddf.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        addDDFToAgentRequestContext(ddf);
+    }
+    
+    @Test
+    public void testTwoResources() {
+        final DDF resourceList = ddf.addmember(OIDCInitiatorConstants.RESOURCE).list();
+        resourceList.add(new DDF(null).string("https://cal.example.com"));
+        resourceList.add(new DDF(null).string("https://mail.example.com"));
+        
+        final Set<URI> result = strategy.apply(mc);
+        assertNotNull(result);
+        assert result != null;
+        assertEquals(result.size(), 2);
+    }
+    
+
+    @Test
+    public void testApply_DisallowedFeature_ResourcesCleared() {
+        final DDF resourceList = ddf.addmember(OIDCInitiatorConstants.RESOURCE).list();
+        resourceList.add(new DDF(null).string("https://cal.example.com"));
+        rpConfig.setDisallowedFeatures(OIDCAuthorizationConfiguration.FEATURE_RESOURCE_INDICATOR);
+
+        final Set<URI> result = strategy.apply(mc);
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    public void testApply_InvalidURI_FilteredOut()  {
+        final DDF resourceList = ddf.addmember(OIDCInitiatorConstants.RESOURCE).list();
+        resourceList.add(new DDF(null).string("invalid"));
+        
+        final Set<URI> result = strategy.apply(mc);
+        assertTrue(result.isEmpty());
+    }
+    
+    @Test
+    public void testApply_InvalidURI_NotAbsolute_FilteredOut()  {
+        final DDF resourceList = ddf.addmember(OIDCInitiatorConstants.RESOURCE).list();
+        resourceList.add(new DDF(null).string("/calendar"));
+        
+        final Set<URI> result = strategy.apply(mc);
+        assertTrue(result.isEmpty());
+    }
+    
+    @Test
+    public void testApply_InvalidFragmentInURI_FilteredOut() {
+        final DDF resourceList = ddf.addmember(OIDCInitiatorConstants.RESOURCE).list();
+        resourceList.add(new DDF(null).string("https://cal.example.com/test#fragment"));
+        
+        final Set<URI> result = strategy.apply(mc);
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    public void testApply_UsesProfileResourcesWhenInputEmpty() {
+        rpConfig.setResourceIndicators(Set.of("https://cal.example.com/"));
+
+        final Set<URI> result = strategy.apply(mc);
+        assertEquals(result.size(), 0);
+    }
+
+}

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


More information about the commits mailing list