[java-idp-oidc] branch main updated: JOIDC-288 - Pushed Authorization Requests (PAR) do not support claims parameter

Codeberg noreply at shibboleth.net
Tue Sep 1 16:05:20 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/6ac321e6cd48ad8ac713d2ecdca13ae8c915c659

The following commit(s) were added to refs/heads/main by this push:
     new 6ac321e6 JOIDC-288 - Pushed Authorization Requests (PAR) do not support claims parameter
6ac321e6 is described below

commit 6ac321e6cd48ad8ac713d2ecdca13ae8c915c659
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue Sep 1 19:04:51 2026 +0300

    JOIDC-288 - Pushed Authorization Requests (PAR) do not support claims parameter
    
    https://shibboleth.atlassian.net/browse/JOIDC-288
    
    - Modified the DefaultRequestedClaimsLookupFunction to parse OIDCClaimsRequest from String when the request object is produced by PAR
    - The change is covered both in unit and flow tests
---
 .../DefaultRequestedClaimsLookupFunction.java      |  8 +++-
 .../DefaultRequestedClaimsLookupFunctionTest.java  | 56 ++++++++++++++++++++++
 .../op/profile/flow/PushedAuthorizeFlowTest.java   | 55 +++++++++++++++++++--
 3 files changed, 114 insertions(+), 5 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultRequestedClaimsLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultRequestedClaimsLookupFunction.java
index 0619e900..9a24a4a7 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultRequestedClaimsLookupFunction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultRequestedClaimsLookupFunction.java
@@ -56,11 +56,15 @@ public class DefaultRequestedClaimsLookupFunction
             if (requestObject != null && requestObject.getJWTClaimsSet().getClaim("claims") != null) {
                 final Object claims = requestObject.getJWTClaimsSet().getClaim("claims");
                 if (claims instanceof Map) {
-                    log.debug("claims claim is a map, converting it into a JSONObject");
+                    log.debug("claims claim is a map, converting it into a OIDCClaimsRequest");
                     // the casting is safe as Nimbus shouldn't allow other than String-keyed maps to exist here
                     return OIDCClaimsRequest.parse(new JSONObject((Map<String, ?>) claims));
+                } else if (isRequestObjectFromPar && claims instanceof String string){
+                    log.debug("claims claim is a string from PAR, converting it into a OIDCClaimsRequest");
+                    return OIDCClaimsRequest.parse(string);
                 } else {
-                    log.error("claims claim is not of expected type (java.util.Map), it's: {}", claims.getClass());
+                    log.error("claims claim is not of expected type (Map or String from PAR), it's: {}, from PAR: {}",
+                            claims.getClass(), isRequestObjectFromPar);
                     return null;
                 }
             }
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultRequestedClaimsLookupFunctionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultRequestedClaimsLookupFunctionTest.java
index 2611ce1f..6cbd6fad 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultRequestedClaimsLookupFunctionTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultRequestedClaimsLookupFunctionTest.java
@@ -90,4 +90,60 @@ public class DefaultRequestedClaimsLookupFunctionTest extends AbstractAuthentica
         assert result != null;
         Assert.assertEquals(crRequestObject.toJSONObject(), result.toJSONObject());
     }
+
+    @Test
+    public void testSuccessReqObject_stringValueNotFromPar() throws ParseException {
+        final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest()
+                .add(new ClaimsSetRequest.Entry(IDTokenClaimsSet.SUB_CLAIM_NAME)
+                .withClaimRequirement(ClaimRequirement.ESSENTIAL));
+        final OIDCClaimsRequest crParameter = new OIDCClaimsRequest()
+                .withIDTokenClaimsRequest(idTokenClaims);
+
+        final ClaimsSetRequest userInfoClaims = new ClaimsSetRequest()
+                .add(new ClaimsSetRequest.Entry(UserInfo.BIRTHDATE_CLAIM_NAME)
+                .withClaimRequirement(ClaimRequirement.ESSENTIAL));
+        final OIDCClaimsRequest crRequestObject = new OIDCClaimsRequest()
+                .withIDTokenClaimsRequest(idTokenClaims)
+                .withUserInfoClaimsRequest(userInfoClaims);
+
+        final JWTClaimsSet ro = new JWTClaimsSet.Builder().claim("claims", crRequestObject.toJSONString()).build();
+        final AuthenticationRequest req = new AuthenticationRequest.Builder(
+                new ResponseType("code"), new Scope("openid"), new ClientID("000123"),
+                URI.create("https://example.com/callback")).claims(crParameter)
+                    .requestObject(PlainJWT.parse(new PlainJWT(ro).serialize())).state(new State()).build();
+        // request object JWT is serialized and parsed in order to simulate incoming authentication request better 
+        msgCtx.setMessage(req);
+        oidcCtx.setRequestObject(req.getRequestObject());
+        Assert.assertNull(lookup.apply(prc));
+    }
+
+    @Test
+    public void testSuccessReqObject_stringValueFromPar() throws ParseException {
+        final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest()
+                .add(new ClaimsSetRequest.Entry(IDTokenClaimsSet.SUB_CLAIM_NAME)
+                .withClaimRequirement(ClaimRequirement.ESSENTIAL));
+        final OIDCClaimsRequest crParameter = new OIDCClaimsRequest()
+                .withIDTokenClaimsRequest(idTokenClaims);
+
+        final ClaimsSetRequest userInfoClaims = new ClaimsSetRequest()
+                .add(new ClaimsSetRequest.Entry(UserInfo.BIRTHDATE_CLAIM_NAME)
+                .withClaimRequirement(ClaimRequirement.ESSENTIAL));
+        final OIDCClaimsRequest crRequestObject = new OIDCClaimsRequest()
+                .withIDTokenClaimsRequest(idTokenClaims)
+                .withUserInfoClaimsRequest(userInfoClaims);
+
+        final JWTClaimsSet ro = new JWTClaimsSet.Builder().claim("claims", crRequestObject.toJSONString()).build();
+        final AuthenticationRequest req = new AuthenticationRequest.Builder(
+                new ResponseType("code"), new Scope("openid"), new ClientID("000123"),
+                URI.create("https://example.com/callback")).claims(crParameter)
+                    .requestObject(PlainJWT.parse(new PlainJWT(ro).serialize())).state(new State()).build();
+        // request object JWT is serialized and parsed in order to simulate incoming authentication request better 
+        msgCtx.setMessage(req);
+        oidcCtx.setRequestObject(req.getRequestObject());
+        oidcCtx.setRequestObjectFromPar(true);
+        final OIDCClaimsRequest result = lookup.apply(prc);
+        assert result != null;
+        Assert.assertEquals(crRequestObject.toJSONObject(), result.toJSONObject());
+    }
+
 }
\ 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 c28dd237..71e32ea9 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
@@ -47,6 +47,9 @@ import com.nimbusds.oauth2.sdk.OAuth2Error;
 import com.nimbusds.oauth2.sdk.PushedAuthorizationSuccessResponse;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.openid.connect.sdk.Nonce;
+import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
+import com.nimbusds.openid.connect.sdk.claims.ClaimRequirement;
 
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.support.TestRequestUriClaimsSetDecorator;
 import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultPushedAuthorizationRequestUriDeserializationFunction;
@@ -193,6 +196,26 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
         }
     }
 
+    @Test
+    public void testSuccessWithOpenIdParams() throws IOException, NoSuchAlgorithmException, URISyntaxException,
+            DataSealerException, ComponentInitializationException {
+        for (final String clientId : clientIds) {
+            storeMetadata(storageService, clientId, clientSecret, scope, "https://example.org/cb");
+            setBasicAuth(clientId, clientSecret);
+            final var requestParameters = createRequestParameters(clientId);
+            final String nonce = idGenerator.generateIdentifier();
+            requestParameters.put("nonce", nonce);
+            requestParameters.put("claims", 
+                    "{\"id_token\":{\"acr\":{\"essential\":true,\"values\":[\"password\"]}}}");
+            setHttpFormRequest("POST", requestParameters);
+            final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+            assertSuccessResponse(result, clientId);
+            final PushedAuthorizationSuccessResponse response =
+                    parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+            verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString(), null, nonce, "password");
+        }
+    }
+
     @Test
     public void testFailure_unregisteredScope() throws IOException, NoSuchAlgorithmException, URISyntaxException, DataSealerException,
             ComponentInitializationException {
@@ -968,18 +991,33 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
     }
 
     protected void verifyAuthorizeEndpoint(final String clientId, final String requestUri, final String jkt) {
+        verifyAuthorizeEndpoint(clientId, requestUri, jkt, null);
+    }
+
+    protected void verifyAuthorizeEndpoint(final String clientId, final String requestUri, final String jkt,
+            final String nonce) {
+        verifyAuthorizeEndpoint(clientId, requestUri, jkt, nonce, null);
+    }
+
+    protected void verifyAuthorizeEndpoint(final String clientId, final String requestUri, final String jkt,
+            final String nonce, final String essentialAcr) {
         initializeMocks();
         initializeThreadLocals();
         setBasicAuth("jdoe", "changeit");
         request.setMethod("GET");
         final String redirectUri = "https://example.org/cb";
 
-        AuthorizeFlowTest.setRequestParameters(request, List.of(new Pair<>("client_id", clientId),
-                new Pair<>("request_uri", requestUri)));
+        if (scope.contains("openid")) {
+            AuthorizeFlowTest.setRequestParameters(request, List.of(new Pair<>("client_id", clientId),
+                    new Pair<>("request_uri", requestUri), new Pair<>("scope", scope.toString())));
+        } else {
+            AuthorizeFlowTest.setRequestParameters(request, List.of(new Pair<>("client_id", clientId),
+                    new Pair<>("request_uri", requestUri)));
+        }
 
         final FlowExecutionResult result = flowExecutor.launchExecution("oidc/authorize", null, externalContext);
         Assert.assertEquals(result.getOutcome().getId(), END_STATE_ID);
-        if (jkt != null) {
+        if (jkt != null || nonce != null || essentialAcr != null) {
             final String url = response.getRedirectedUrl();
             assert url != null;
             final String code = url.substring(url.indexOf("code=") + 5);
@@ -988,6 +1026,17 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
                 final AuthorizeCodeClaimsSet claimsSet = AuthorizeCodeClaimsSet.parse(code, getDataSealer());
                 assert claimsSet != null;
                 Assert.assertEquals(claimsSet.getDpopProofJwkThumbprint(), jkt);
+                Assert.assertEquals(claimsSet.getNonce(), nonce != null ? new Nonce(nonce) : null);
+                if (essentialAcr != null) {
+                    final OIDCClaimsRequest claimsRequest = claimsSet.getClaimsRequest();
+                    Assert.assertNotNull(claimsRequest);
+                    assert claimsRequest != null;
+                    final var acr = claimsRequest.getIDTokenClaimsRequest().get("acr");
+                    Assert.assertNotNull(acr);
+                    assert acr != null;
+                    Assert.assertEquals(acr.getClaimRequirement(), ClaimRequirement.ESSENTIAL);
+                    Assert.assertEquals(acr.getValuesAsListOfStrings(), List.of(essentialAcr));
+                }
             } 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