[java-idp-oidc] branch main updated: JOIDC-200 - Support for OAuth2 Pushed Authorization Requests (PAR)

Henri Mikkonen henri.mikkonen at iki.fi
Thu May 30 12:52:01 UTC 2024


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

hjmikkon pushed a commit to branch main
in repository java-idp-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=edbaddcead4a9e4c9abad556bc18817a7196c247

The following commit(s) were added to refs/heads/main by this push:
     new edbaddce JOIDC-200 - Support for OAuth2 Pushed Authorization Requests (PAR)
edbaddce is described below

commit edbaddcead4a9e4c9abad556bc18817a7196c247
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu May 30 15:51:45 2024 +0300

    JOIDC-200 - Support for OAuth2 Pushed Authorization Requests (PAR)
    
    https://shibboleth.atlassian.net/browse/JOIDC-200
    
    - Included support for the metadata flag to require pushed authorization request
      - In both static and dynamic client registration
    - Extended SetRequestObjectToResponseContext to support requiring PAR via profile and metadata
    - Tested via flow tests
---
 .../impl/SetRequestObjectToResponseContext.java    |  40 ++++-
 .../impl/AddRemainingClaimsToClientMetadata.java   |   7 +-
 .../pushed-authorization-beans.xml                 |   1 +
 .../idp/flows/oidc/authorize/authorize-beans.xml   |  27 ++-
 .../SetRequestObjectToResponseContextTest.java     |   1 +
 .../oidc/op/profile/flow/AuthorizeFlowTest.java    | 187 +++++++++++++++++++++
 .../oidc/op/profile/flow/RegistrationFlowTest.java |  30 ++++
 .../shibboleth/idp/module/conf/relying-party.xml   |   7 +
 8 files changed, 293 insertions(+), 7 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetRequestObjectToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetRequestObjectToResponseContext.java
index 5ff7d2b5..f3fece49 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetRequestObjectToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetRequestObjectToResponseContext.java
@@ -84,6 +84,9 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
     /** Predicate for enforcing the use of request objects. */
     @NonnullAfterInit private Predicate<ProfileRequestContext> requestObjectEnforcedPredicate;
 
+    /** Predicate for enforcing the use of pushed authorization requests. */
+    @NonnullAfterInit private Predicate<ProfileRequestContext> pushedAuthorizationRequestEnforcedPredicate;
+
     /** Object mapper used for pretty-printing JWT contents. */
     @NonnullAfterInit private ObjectMapper objectMapper;
 
@@ -91,6 +94,9 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
     @Nonnull private List<BiFunction<ProfileRequestContext,URI,Map<String,Object>>>
         pushedAuthorizationRequestUriDeserializers;
 
+    /** Whether to require pushed authorization request to be used. */
+    private boolean requirePushedAuthorization = false;
+
     /**
      * Constructor.
      */
@@ -128,6 +134,19 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
                 "Request object enforced predicate annot be null");
     }
 
+    /**
+     * Set the predicate for enforcing the use of pushed authorization requests.
+     * 
+     * @param predicate the predicate for enforcing the use of pushed authorization requests
+     * 
+     * @since 4.2.0
+     */
+    public void setPushedAuthorizationRequestEnforcedPredicate(
+            @Nonnull final Predicate<ProfileRequestContext> predicate) {
+        pushedAuthorizationRequestEnforcedPredicate = Constraint.isNotNull(predicate,
+                "Pushed authorization request enforced predicate annot be null");
+    }
+
     /**
      * Set the object mapper used for pretty-printing JWT contents.
      * 
@@ -180,6 +199,10 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
         if (requestObjectEnforcedPredicate == null) {
             throw new ComponentInitializationException("Request object enforced predicate cannot be null");
         }
+        if (pushedAuthorizationRequestEnforcedPredicate == null) {
+            throw new ComponentInitializationException(
+                    "Pushed authorization request enforced predicate cannot be null");
+        }
         if (objectMapper == null) {
             throw new ComponentInitializationException("Object mapper cannot be null");
         }
@@ -199,11 +222,16 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
             return false;
         }
 
+        requirePushedAuthorization = pushedAuthorizationRequestEnforcedPredicate.test(profileRequestContext);
         if (!authorizationRequest.specifiesRequestObject()) {
             if (requestObjectEnforcedPredicate.test(profileRequestContext)) {
                 log.warn("{} No request_uri or request by value, even though it's enforced for {}", getLogPrefix(),
                         authorizationRequest.getClientID().getValue());
-                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.MISSING_MANDATORY_REQUEST_OBJECT);
+                if (requirePushedAuthorization) {
+                    ActionSupport.buildEvent(profileRequestContext, OidcEventIds.MISSING_MANDATORY_PAR_REQUEST_URI);
+                } else {
+                    ActionSupport.buildEvent(profileRequestContext, OidcEventIds.MISSING_MANDATORY_REQUEST_OBJECT);
+                }
             } else {
                 log.debug("{} No request_uri or request by value, nothing to do", getLogPrefix());
             }
@@ -228,7 +256,7 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
         final AuthorizationRequest authorizationRequest = getAuthorizationRequest();
         assert authorizationRequest != null;
         final JWT requestObject = authorizationRequest.getRequestObject();
-        if (requestObject != null) {
+        if (requestObject != null && !requirePushedAuthorization) {
             oidcResponseContext.setRequestObject(requestObject);
             log.debug("{} Request object {} by value stored to oidc response context", getLogPrefix(),
                     requestObject.serialize());
@@ -275,7 +303,13 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REQUEST_URI);
             return;
         }
-        
+
+        if (requirePushedAuthorization) {
+            log.warn("{} Pushed authorization request required but not used", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.MISSING_MANDATORY_PAR_REQUEST_URI);
+            return;
+        }
+
         final HttpGet httpRequest = new HttpGet(authorizationRequest.getRequestURI());
         final HttpClientContext httpContext = buildHttpContext(httpRequest);
         try (final ClassicHttpResponse response = httpClient.executeOpen(null, httpRequest, httpContext)) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddRemainingClaimsToClientMetadata.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddRemainingClaimsToClientMetadata.java
index 2ef21a9f..d27356c1 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddRemainingClaimsToClientMetadata.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddRemainingClaimsToClientMetadata.java
@@ -35,8 +35,9 @@ import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
  * Adds the remaining recognized claims from the input metadata to the output {@link OIDCClientMetadata}. The static
- * set of remaining recognized claims contain only 'dpop_bound_access_tokens'. The metadata policy offers a dynamic
- * way to add any other claims to be recognized and thus included in the output metadata.
+ * set of remaining recognized claims contain 'dpop_bound_access_tokens' and 'require_pushed_authorization_requests'.
+ * The metadata policy offers a dynamic way to add any other claims to be recognized and thus included in the output
+ * metadata.
  */
 public class AddRemainingClaimsToClientMetadata extends AbstractOIDCClientMetadataPopulationAction {
 
@@ -103,6 +104,8 @@ public class AddRemainingClaimsToClientMetadata extends AbstractOIDCClientMetada
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         getOutputMetadata().setDPoPBoundAccessTokens(getInputMetadata().getDPoPBoundAccessTokens());
+        getOutputMetadata().requiresPushedAuthorizationRequests(
+                getInputMetadata().requiresPushedAuthorizationRequests());
         final JSONObject input = getInputMetadata().toJSONObject();
         final JSONObject initialOutput = getOutputMetadata().toJSONObject();
         for (final String claim : input.keySet()) {
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
index d9e43abd..4d9c4461 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
@@ -83,6 +83,7 @@
         p:httpClient="#{getObject('shibboleth.oidc.NonBrowser.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
         p:httpClientSecurityParameters="#{getObject('shibboleth.oidc.NonBrowser.HttpClientSecurityParameters')}"
         p:requestObjectEnforcedPredicate-ref="UseRequestObjectPredicate"
+        p:pushedAuthorizationRequestEnforcedPredicate-ref="shibboleth.Conditions.FALSE"
         p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}" />
 
     <bean id="RequestObjectEncryptedCondition" parent="shibboleth.Conditions.Expression"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
index 8cb6f71c..66bf520b 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
@@ -123,9 +123,20 @@
         class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetRequestObjectToResponseContext" scope="prototype"
         p:httpClient="#{getObject('shibboleth.oidc.NonBrowser.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
         p:httpClientSecurityParameters="#{getObject('shibboleth.oidc.NonBrowser.HttpClientSecurityParameters')}"
-        p:requestObjectEnforcedPredicate-ref="UseRequestObjectPredicate"
+        p:pushedAuthorizationRequestEnforcedPredicate-ref="RequirePushedAuthorizationRequestPredicate"
         p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}"
-        p:pushedAuthorizationRequestUriDeserializers-ref="#{'%{idp.oauth2.par.deserializationStrategies:shibboleth.oidc.DefaultPushedAuthorizationRequestUriDeserializers}'.trim()}" />
+        p:pushedAuthorizationRequestUriDeserializers-ref="#{'%{idp.oauth2.par.deserializationStrategies:shibboleth.oidc.DefaultPushedAuthorizationRequestUriDeserializers}'.trim()}">
+        <property name="requestObjectEnforcedPredicate">
+            <bean parent="shibboleth.Conditions.OR">
+                <constructor-arg>
+                    <ref bean="UseRequestObjectPredicate" />
+                </constructor-arg>
+                <constructor-arg>
+                    <ref bean="RequirePushedAuthorizationRequestPredicate"/>
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
 
     <util:list id="shibboleth.oidc.DefaultPushedAuthorizationRequestUriDeserializers">
         <bean factory-bean="DefaultStatelessPushedAuthorizationRequestUriDeserializerFactory" factory-method="getBean" />
@@ -222,6 +233,18 @@
 
     <bean id="UseRequestObjectPredicate" class="net.shibboleth.oidc.profile.config.logic.UseRequestObjectPredicate" />
 
+    <bean id="RequirePushedAuthorizationRequestPredicate" parent="shibboleth.Conditions.OR">
+        <constructor-arg>
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.MetadataValueEqualsCondition"
+                p:key="require_pushed_authorization_requests"
+                p:value="#{true}"
+                p:defaultValue="#{false}"/>
+        </constructor-arg>
+        <constructor-arg>
+            <bean class="net.shibboleth.oidc.profile.config.logic.RequirePushedAuthorizationRequestPredicate" />
+        </constructor-arg>
+    </bean>
+
     <bean id="SignRequestObjectPredicate" class="net.shibboleth.oidc.profile.config.logic.SignRequestObjectPredicate" />
 
     <bean id="ValidateRequestObjectSignature" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetRequestObjectToResponseContextTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetRequestObjectToResponseContextTest.java
index 0270f784..14768c4c 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetRequestObjectToResponseContextTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetRequestObjectToResponseContextTest.java
@@ -63,6 +63,7 @@ public class SetRequestObjectToResponseContextTest extends BaseOIDCResponseActio
         assert enforceRequestObjects != null;
         action.setRequestObjectEnforcedPredicate(enforceRequestObjects);
         action.setObjectMapper(new ObjectMapper());
+        action.setPushedAuthorizationRequestEnforcedPredicate(prc -> false);
         action.initialize();
     }
 
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
index 120aa96f..aff3da2d 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
@@ -16,6 +16,8 @@ package net.shibboleth.idp.plugin.oidc.op.profile.flow;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URLEncoder;
 import java.security.NoSuchAlgorithmException;
 import java.security.PublicKey;
@@ -26,6 +28,8 @@ import java.time.Instant;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import org.opensaml.storage.StorageService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -38,6 +42,7 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Factory;
 import org.testng.annotations.Test;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jose.EncryptionMethod;
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWEAlgorithm;
@@ -58,13 +63,17 @@ import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
 import com.nimbusds.openid.connect.sdk.claims.ClaimRequirement;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest;
 import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
+import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultPushedAuthorizationRequestUriSerializationFunction;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
 import net.shibboleth.idp.session.SessionException;
 import net.shibboleth.oidc.profile.core.OidcError;
 import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.security.DataSealerException;
+import net.shibboleth.shared.testing.RepositorySupport;
 
 /**
  * Tests for the authorize-flow.
@@ -84,6 +93,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     String clientIdQueryResponseMode = "mockClientIdQueryResponseMode";
     String clientIdFragmentResponseMode = "mockClientIdFragmentResponseMode";
     String clientIdQueryFragmentResponseMode = "mockClientIdQueryFragmentResponseMode";
+    String clientIdRequirePAR = "mockClientIdRequirePAR";
     String clientSecret = "mockClientSecretmockClientSecretmockClientSecretmockClientSecretmockClientSecret";
     Scope scope = Scope.parse("openid profile email");
     
@@ -161,6 +171,182 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(successResponse.getIssuer());
     }
 
+    @Test
+    public void testWithAuthorizationCodeFlow_parRequiredByProfileNotProvided() throws IOException, SessionException {
+        setRequestParameters(List.of(new Pair<>("client_id", clientIdRequirePAR),
+                new Pair<>("response_type", "code"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("redirect_uri", redirectUri)));
+        request.setMethod("GET");
+        storeMetadata(storageService, clientIdRequirePAR, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        Assert.assertEquals("ErrorView", result.getOutcome().getId());
+    }
+
+    @Test
+    public void testWithAuthorizationCodeFlow_parRequiredByProfileROProvided() throws IOException, SessionException {
+        final JWTClaimsSet ro = new JWTClaimsSet.Builder()
+                .claim("redirect_uri", redirectUri)
+                .build();
+        final PlainJWT requestObject = new PlainJWT(ro);
+        setRequestParameters(List.of(new Pair<>("client_id", clientIdRequirePAR),
+                new Pair<>("response_type", "code"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("request", requestObject.serialize()),
+                new Pair<>("redirect_uri", redirectUri)));
+        request.setMethod("GET");
+        storeMetadata(storageService, clientIdRequirePAR, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        Assert.assertEquals("ErrorView", result.getOutcome().getId());
+    }
+
+    @Test
+    public void testWithAuthorizationCodeFlow_parRequiredByProfileUriProvided() throws IOException, SessionException,
+            URISyntaxException {
+        final String clientId = "s6BhdRkqt3";
+        final String uri = RepositorySupport.buildHTTPSResourceURL("java-idp-oidc",
+                "idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/oidc/profile/impl/oidc-authreq.json");
+        setRequestParameters(List.of(new Pair<>("client_id", clientId),
+                new Pair<>("response_type", "code"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("request_uri", uri)));
+        request.setMethod("GET");
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        metadata.setRedirectionURIs(Set.of(new URI("https://client.example.org/cb")));
+        metadata.setScope(scope);
+        metadata.setRequestObjectURIs(Set.of(new URI(uri)));
+        storeMetadataObject(storageService, clientId, clientSecret, metadata);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        Assert.assertEquals("ErrorView", result.getOutcome().getId());
+        removeMetadata(storageService, clientId);
+    }
+
+    @Test
+    public void testWithAuthorizationCodeFlow_parRequiredByProfile() throws IOException, SessionException,
+            URISyntaxException {
+        final String clientId = clientIdRequirePAR;
+        setRequestParameters(List.of(new Pair<>("client_id", clientId),
+                new Pair<>("response_type", "code"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("request_uri", createParGeneratedRequestUri(clientId).toString()),
+                new Pair<>("redirect_uri", redirectUri)));
+        request.setMethod("GET");
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        metadata.setRedirectionURIs(Set.of(new URI(redirectUri)));
+        metadata.setScope(scope);
+        metadata.requiresPushedAuthorizationRequests(true);
+        storeMetadataObject(storageService, clientId, clientSecret, metadata);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertNotNull(getSidFromAuthorizeCodeClaimsSet(successResponse));
+        Assert.assertNull(successResponse.getIssuer());
+    }
+
+    @Test
+    public void testWithAuthorizationCodeFlow_parRequiredByMetadataNotProvided() throws IOException, SessionException,
+            URISyntaxException {
+        setRequestParameters(List.of(new Pair<>("client_id", clientId),
+                new Pair<>("response_type", "code"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("redirect_uri", redirectUri)));
+        request.setMethod("GET");
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        metadata.setRedirectionURIs(Set.of(new URI(redirectUri)));
+        metadata.setScope(scope);
+        metadata.requiresPushedAuthorizationRequests(true);
+        storeMetadataObject(storageService, clientId, clientSecret, metadata);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        Assert.assertEquals("ErrorView", result.getOutcome().getId());
+    }
+
+    @Test
+    public void testWithAuthorizationCodeFlow_parRequiredByMetadataROProvided() throws IOException, SessionException,
+            URISyntaxException {
+        final JWTClaimsSet ro = new JWTClaimsSet.Builder()
+                .claim("redirect_uri", redirectUri)
+                .build();
+        final PlainJWT requestObject = new PlainJWT(ro);
+        setRequestParameters(List.of(new Pair<>("client_id", clientId),
+                new Pair<>("response_type", "code"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("request", requestObject.serialize()),
+                new Pair<>("redirect_uri", redirectUri)));
+        request.setMethod("GET");
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        metadata.setRedirectionURIs(Set.of(new URI(redirectUri)));
+        metadata.setScope(scope);
+        metadata.requiresPushedAuthorizationRequests(true);
+        storeMetadataObject(storageService, clientId, clientSecret, metadata);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        Assert.assertEquals("ErrorView", result.getOutcome().getId());
+    }
+
+    protected URI createParGeneratedRequestUri(final String clientId) {
+        final DefaultPushedAuthorizationRequestUriSerializationFunction parGenerator =
+                new DefaultPushedAuthorizationRequestUriSerializationFunction();
+        parGenerator.setObjectMapper(new ObjectMapper());
+        parGenerator.setDataSealer(getDataSealer());
+        parGenerator.setIdentifierGeneratorLookupStrategy(prc -> idGenerator);
+        parGenerator.setId("mockPar");
+        try {
+            parGenerator.initialize();
+        } catch (ComponentInitializationException e) {
+            Assert.fail("Could not initialize PAR generator", e);
+        }
+        return parGenerator.apply(null, Map.of("client_id", clientId, "redirect_uri", redirectUri));
+    }
+    
+    @Test
+    public void testWithAuthorizationCodeFlow_parRequiredByMetadata() throws IOException, SessionException,
+            URISyntaxException {
+        setRequestParameters(List.of(new Pair<>("client_id", clientId),
+                new Pair<>("response_type", "code"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("request_uri", createParGeneratedRequestUri(clientId).toString()),
+                new Pair<>("redirect_uri", redirectUri)));
+        request.setMethod("GET");
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        metadata.setRedirectionURIs(Set.of(new URI(redirectUri)));
+        metadata.setScope(scope);
+        metadata.requiresPushedAuthorizationRequests(true);
+        storeMetadataObject(storageService, clientId, clientSecret, metadata);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertNotNull(getSidFromAuthorizeCodeClaimsSet(successResponse));
+        Assert.assertNull(successResponse.getIssuer());
+    }
+
     @Test
     public void testWithAuthorizationCodeFlow_defaultResponseModeNotAllowed() throws IOException, SessionException {
         setRequestParameters(List.of(new Pair<>("client_id", clientIdFragmentResponseMode),
@@ -2139,6 +2325,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         removeMetadata(storageService, clientIdQueryResponseMode);
         removeMetadata(storageService, clientIdQueryFragmentResponseMode);
         removeMetadata(storageService, clientIdFragmentResponseMode);
+        removeMetadata(storageService, clientIdRequirePAR);
     }
 
 }
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java
index b1aeacd4..1de268fa 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java
@@ -198,6 +198,36 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
         Assert.assertEquals(storedInfo.getOIDCMetadata().getRequestObjectURIs(), Set.of(new URI(requestUri)));
         Assert.assertTrue(metadata.getRedirectionURIStrings().contains(redirectUri));
         Assert.assertTrue(metadata.getDPoPBoundAccessTokens());
+        Assert.assertFalse(metadata.requiresPushedAuthorizationRequests());
+    }
+
+    @Test
+    public void testUnauthenticated_successWithPARRequired() throws Exception {
+        final String requestUri = "https://client.example.org/rf.txt#qpXaRLh_n93TTR9F252ValdatUQvQiJi5BDub2BeznA";
+        setJsonRequest("POST", "{ \"redirect_uris\":[\"" + redirectUri + "\"], \"request_uris\":[\"" + requestUri + "\"]," 
+                + "\"require_pushed_authorization_requests\":true }");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final OIDCClientInformationResponse parsedResponse =
+                parseSuccessResponse(result, OIDCClientInformationResponse.class);
+        final OIDCClientInformation clientInfo = parsedResponse.getOIDCClientInformation();
+        final OIDCClientMetadata metadata = clientInfo.getOIDCMetadata();
+        final String clientId = clientInfo.getID().getValue();
+        assert clientId != null;
+        assert storageService != null;
+        final StorageRecord<String> storageRecord = storageService.read(BaseStorageServiceClientInformationComponent.CONTEXT_NAME, 
+                clientId);
+        assert storageRecord != null;
+        final String record = storageRecord.getValue();
+        Assert.assertNotNull(record);
+        final JSONParser parser = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE);
+        final OIDCClientInformation storedInfo = OIDCClientInformation.parse((JSONObject) parser.parse(record));
+        Assert.assertEquals(storedInfo.getID(), clientInfo.getID());
+        Assert.assertEquals(storedInfo.getSecret(), clientInfo.getSecret());
+        Assert.assertEquals(storedInfo.getOIDCMetadata().getRedirectionURIStrings(), metadata.getRedirectionURIStrings());
+        Assert.assertEquals(storedInfo.getOIDCMetadata().getRequestObjectURIs(), Set.of(new URI(requestUri)));
+        Assert.assertTrue(metadata.getRedirectionURIStrings().contains(redirectUri));
+        Assert.assertTrue(metadata.requiresPushedAuthorizationRequests());
+        Assert.assertFalse(metadata.getDPoPBoundAccessTokens());
     }
 
     @Test
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 94eb2130..c4c44acc 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -83,6 +83,13 @@
                  </list>
             </property>
         </bean>
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="#{{'mockClientIdRequirePAR','s6BhdRkqt3'}}">
+            <property name="profileConfigurations">
+                 <list>
+                     <bean parent="OIDC.SSO" p:requirePushedAuthorizationRequest="true" p:encryptionOptional="true"/>
+                 </list>
+            </property>
+        </bean>
         <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdQueryFragmentResponseMode">
             <property name="profileConfigurations">
                  <list>

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


More information about the commits mailing list