[java-idp-oidc] 02/04: JOIDC-296 - The use of resource parameter with PAR is not working correctly

Codeberg noreply at shibboleth.net
Thu Nov 5 16:58:37 UTC 2026


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

codeberg pushed a commit to branch maint-4.3
in repository java-idp-oidc.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-oidc/commit/8b4768853d2af1f02a68e69dd24be304f74fc15e

commit 8b4768853d2af1f02a68e69dd24be304f74fc15e
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 c7aa5f00..eec3748b 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
@@ -18,6 +18,7 @@ import java.net.URI;
 import java.text.ParseException;
 import java.time.Duration;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.BiFunction;
@@ -36,6 +37,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.profile.IdPEventIds;
 import net.shibboleth.oidc.profile.config.navigate.PushedAuthorizationRequestUriClaimsSetManipulationStrategyLookupFunction;
@@ -193,6 +195,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());
         }
@@ -227,4 +239,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;
+    }
 }
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 8e616d17..8034efaa 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";
@@ -921,6 +1058,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");
@@ -937,7 +1079,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);
@@ -957,6 +1099,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