[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