[java-idp-oidc] branch main updated: JOIDC-296 - The use of resource parameter with PAR is not working correctly
Codeberg
noreply at shibboleth.net
Wed Nov 4 12:46:40 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-idp-oidc.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-oidc/commit/6f29447dbaf578f35076ccbb19773e314ed9bc3d
The following commit(s) were added to refs/heads/main by this push:
new 6f29447d JOIDC-296 - The use of resource parameter with PAR is not working correctly
6f29447d is described below
commit 6f29447dbaf578f35076ccbb19773e314ed9bc3d
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed Nov 4 14:45:19 2026 +0200
JOIDC-296 - The use of resource parameter with PAR is not working correctly
https://shibboleth.atlassian.net/browse/JOIDC-296
- Strip out the custom prefix from the resources for the request_uri claims set within PAR
- Improve testing
---
...mOutbounPushedAuthorizationResponseMessage.java | 26 ++++
.../op/profile/flow/PushedAuthorizeFlowTest.java | 149 ++++++++++++++++++++-
2 files changed, 174 insertions(+), 1 deletion(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutbounPushedAuthorizationResponseMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutbounPushedAuthorizationResponseMessage.java
index d787e0e5..d4618c9d 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutbounPushedAuthorizationResponseMessage.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutbounPushedAuthorizationResponseMessage.java
@@ -19,6 +19,7 @@ import java.text.ParseException;
import java.time.Duration;
import java.util.Collection;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
@@ -38,6 +39,7 @@ import com.nimbusds.oauth2.sdk.PushedAuthorizationRequest;
import com.nimbusds.oauth2.sdk.PushedAuthorizationSuccessResponse;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.DefaultRequestAudienceLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext;
import net.shibboleth.idp.plugin.oidc.op.profile.RequestUriClaimsSetDecorator;
import net.shibboleth.idp.profile.IdPEventIds;
@@ -207,6 +209,16 @@ public class FormOutbounPushedAuthorizationResponseMessage extends AbstractOAuth
if (requestObject == null) {
assert requestMessage != null;
claimsSet = new HashMap<>(requestMessage.getAuthorizationRequest().toJWTClaimsSet().getClaims());
+ if (claimsSet.containsKey("resource")) {
+ if (claimsSet.get("resource") instanceof List<?> list) {
+ claimsSet.put("resource",
+ list.stream().filter(String.class::isInstance).map(String.class::cast)
+ .map(item -> removeResourcePrefixIfExists(item))
+ .toList());
+ } else if (claimsSet.get("resource") instanceof String string) {
+ claimsSet.put("resource", removeResourcePrefixIfExists(string));
+ }
+ }
} else {
claimsSet = new HashMap<>(requestObject.getJWTClaimsSet().getClaims());
}
@@ -244,4 +256,18 @@ public class FormOutbounPushedAuthorizationResponseMessage extends AbstractOAuth
assert claimsSet != null;
return claimsSet;
}
+
+ /**
+ * Removes the custom resource prefix {@link DefaultRequestAudienceLookupFunction#CUSTOM_RESOURCE_PARAM_PREFIX}
+ * if it exists in the resource.
+ *
+ * @param resource resource to be checked
+ * @return the resource without the prefix if it was found
+ */
+ @Nullable private String removeResourcePrefixIfExists(@Nullable final String resource) {
+ return resource == null ? null :
+ resource.startsWith(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_PREFIX) ?
+ resource.substring(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_PREFIX.length()) :
+ resource;
+ }
}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
index 553af396..84dc0582 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
@@ -85,6 +85,9 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
private DefaultPushedAuthorizationRequestUriDeserializationFunction statelessDeserializer;
+ String resource = "https://resource.example.org";
+ String nonUriResource = "resource.example.org";
+
public PushedAuthorizeFlowTest() {
super(FLOW_ID);
}
@@ -272,6 +275,140 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
}
}
+ @Test
+ public void testSuccessWithPostAuthAndResource() throws IOException, SessionException {
+ request.setMethod("GET");
+ for (final String clientId : clientIds) {
+ storeMetadata(storageService, clientId, clientSecret, scope, null, null, null,
+ ClientAuthenticationMethod.CLIENT_SECRET_POST, null, null, true, null, "https://example.org/cb");
+ final Map<String, String> requestParams = createRequestParameters(clientId);
+ requestParams.put("client_secret", clientSecret);
+ requestParams.put("resource", resource);
+ setHttpFormRequest("POST", requestParams);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertSuccessResponse(result, clientId);
+ final PushedAuthorizationSuccessResponse response =
+ parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+ final Map<String,Object> object = statelessDeserializer.apply(null, response.getRequestURI());
+ assert object != null;
+ Assert.assertEquals(object.get("resource"), resource);
+ verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString(), null, null, null,
+ List.of(resource));
+ }
+ }
+
+ @Test
+ public void testSuccessWithPostAuthAndNonUriResource() throws IOException, SessionException {
+ request.setMethod("GET");
+ for (final String clientId : clientIds) {
+ storeMetadata(storageService, clientId, clientSecret, scope, null, null, null,
+ ClientAuthenticationMethod.CLIENT_SECRET_POST, null, null, true, null, "https://example.org/cb");
+ final Map<String, String> requestParams = createRequestParameters(clientId);
+ requestParams.put("client_secret", clientSecret);
+ requestParams.put("resource", nonUriResource);
+ setHttpFormRequest("POST", requestParams);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertSuccessResponse(result, clientId);
+ final PushedAuthorizationSuccessResponse response =
+ parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+ final Map<String,Object> object = statelessDeserializer.apply(null, response.getRequestURI());
+ assert object != null;
+ Assert.assertEquals(object.get("resource"), nonUriResource);
+ verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString(), null, null, null,
+ List.of(nonUriResource));
+ }
+ }
+
+ @Test
+ public void testSuccessWithResourceInRequestObject() throws IOException, SessionException, JOSEException {
+ for (final String clientId : clientIds) {
+ storeMetadata(storageService, clientId, clientSecret, scope, null, null, null,
+ ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null, null, true, null, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ final JWTClaimsSet ro = new JWTClaimsSet.Builder()
+ .claim("iss", clientId)
+ .claim("client_id", clientId)
+ .claim("aud", issuer)
+ .claim("response_type", "code")
+ .claim("redirect_uri", "https://example.org/cb")
+ .claim("scope", "openid profile")
+ .claim("resource", resource)
+ .build();
+ final SignedJWT requestObject = createSecretJWT(ro, clientSecret);
+ setHttpFormRequest("POST", createRequestParameters(clientId, null, null,
+ requestObject.serialize()));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertSuccessResponse(result, clientId);
+ final PushedAuthorizationSuccessResponse response =
+ parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+ final Map<String,Object> object = statelessDeserializer.apply(null, response.getRequestURI());
+ assert object != null;
+ Assert.assertEquals(object.get("resource"), resource);
+ verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString(), null, null, null,
+ List.of(resource));
+ }
+ }
+
+ @Test
+ public void testSuccessWithNonUriResourceInRequestObject() throws IOException, SessionException, JOSEException {
+ for (final String clientId : clientIds) {
+ storeMetadata(storageService, clientId, clientSecret, scope, null, null, null,
+ ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null, null, true, null, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ final JWTClaimsSet ro = new JWTClaimsSet.Builder()
+ .claim("iss", clientId)
+ .claim("client_id", clientId)
+ .claim("aud", issuer)
+ .claim("response_type", "code")
+ .claim("redirect_uri", "https://example.org/cb")
+ .claim("scope", "openid profile")
+ .claim("resource", nonUriResource)
+ .build();
+ final SignedJWT requestObject = createSecretJWT(ro, clientSecret);
+ setHttpFormRequest("POST", createRequestParameters(clientId, null, null,
+ requestObject.serialize()));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertSuccessResponse(result, clientId);
+ final PushedAuthorizationSuccessResponse response =
+ parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+ final Map<String,Object> object = statelessDeserializer.apply(null, response.getRequestURI());
+ assert object != null;
+ Assert.assertEquals(object.get("resource"), nonUriResource);
+ verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString(), null, null, null,
+ List.of(nonUriResource));
+ }
+ }
+
+ @Test
+ public void testSuccessWithResourcesInRequestObject() throws IOException, SessionException, JOSEException {
+ for (final String clientId : clientIds) {
+ storeMetadata(storageService, clientId, clientSecret, scope, null, null, null,
+ ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null, null, true, null, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ final JWTClaimsSet ro = new JWTClaimsSet.Builder()
+ .claim("iss", clientId)
+ .claim("client_id", clientId)
+ .claim("aud", issuer)
+ .claim("response_type", "code")
+ .claim("redirect_uri", "https://example.org/cb")
+ .claim("scope", "openid profile")
+ .claim("resource", List.of(resource, nonUriResource))
+ .build();
+ final SignedJWT requestObject = createSecretJWT(ro, clientSecret);
+ setHttpFormRequest("POST", createRequestParameters(clientId, null, null,
+ requestObject.serialize()));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertSuccessResponse(result, clientId);
+ final PushedAuthorizationSuccessResponse response =
+ parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+ final Map<String,Object> object = statelessDeserializer.apply(null, response.getRequestURI());
+ assert object != null;
+ Assert.assertEquals(object.get("resource"), List.of(resource, nonUriResource));
+ verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString(), null, null, null,
+ List.of(resource, nonUriResource));
+ }
+ }
+
@Test
public void testWithAuthorizationCodeFlowRequestObjectEnforcedNoRO() throws IOException, SessionException {
final String clientId = "mockClientIdRequestObjectEnforced";
@@ -1003,6 +1140,11 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
protected void verifyAuthorizeEndpoint(final String clientId, final String requestUri, final String jkt,
final String nonce, final String essentialAcr) {
+ verifyAuthorizeEndpoint(clientId, requestUri, jkt, nonce, essentialAcr, null);
+ }
+
+ protected void verifyAuthorizeEndpoint(final String clientId, final String requestUri, final String jkt,
+ final String nonce, final String essentialAcr, final List<String> resources) {
initializeMocks();
initializeThreadLocals();
setBasicAuth("jdoe", "changeit");
@@ -1019,7 +1161,7 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
final FlowExecutionResult result = flowExecutor.launchExecution("oidc/authorize", null, externalContext);
Assert.assertEquals(result.getOutcome().getId(), END_STATE_ID);
- if (jkt != null || nonce != null || essentialAcr != null) {
+ if (jkt != null || nonce != null || essentialAcr != null || resource != null) {
final String url = response.getRedirectedUrl();
assert url != null;
final String code = url.substring(url.indexOf("code=") + 5);
@@ -1039,6 +1181,11 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
Assert.assertEquals(acr.getClaimRequirement(), ClaimRequirement.ESSENTIAL);
Assert.assertEquals(acr.getValuesAsListOfStrings(), List.of(essentialAcr));
}
+ if (resources != null) {
+ final JWTClaimsSet jwtClaimsSet = claimsSet.getClaimsSet();
+ assert jwtClaimsSet != null;
+ Assert.assertEquals(jwtClaimsSet.getAudience(), resources);
+ }
} catch (ParseException | DataSealerException e) {
Assert.fail(e.getMessage());
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list