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

Henri Mikkonen henri.mikkonen at iki.fi
Mon Apr 29 15:28:12 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=3305653b2009d4959edd7969c78184caf486499e

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

commit 3305653b2009d4959edd7969c78184caf486499e
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Mon Apr 29 18:27:49 2024 +0300

    JOIDC-200 - Support for OAuth2 Pushed Authorization Requests (PAR)
    
    https://shibboleth.atlassian.net/browse/JOIDC-200
    
    - Changed OAUTH2.PAR into extending OIDC.SSO
    - Included support for request object in the PAR request
      - The enforcement of use/signing/encryption works in the same way as in OIDC.SSO
    - Included request object security tests to the flow tests
    - Some minor code and Javadoc improvements
---
 ...mOutbounPushedAuthorizationResponseMessage.java |  37 ++-
 ...bstractPushedAuthorizationRequestComponent.java |  38 +++
 ...orizationRequestUriDeserializationFunction.java |  12 +-
 ...thorizationRequestUriSerializationFunction.java |  27 +-
 ...orizationRequestUriDeserializationFunction.java |  38 +--
 ...thorizationRequestUriSerializationFunction.java |  37 ++-
 .../pushed-authorization-beans.xml                 |   2 +
 .../idp/service/relying-party/postconfig.xml       |   8 +-
 .../op/profile/flow/PushedAuthorizeFlowTest.java   |  27 +-
 .../flow/PushedAuthorizeRequestObjectJWETest.java  | 366 +++++++++++++++++++++
 .../flow/PushedAuthorizeRequestObjectJWSTest.java  | 208 ++++++++++++
 .../shibboleth/idp/module/conf/relying-party.xml   |   3 +
 12 files changed, 738 insertions(+), 65 deletions(-)

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 eda82c9a..f9e0422e 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
@@ -17,6 +17,8 @@ package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 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.function.BiFunction;
 import java.util.function.Function;
@@ -30,6 +32,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 import com.nimbusds.jwt.JWT;
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
 import com.nimbusds.oauth2.sdk.PushedAuthorizationRequest;
 import com.nimbusds.oauth2.sdk.PushedAuthorizationSuccessResponse;
 
@@ -155,13 +158,20 @@ public class FormOutbounPushedAuthorizationResponseMessage extends AbstractOAuth
 
     }
 
+    /**
+     * Build the claims set by combining the contents of the pushed authorization request form parameters with the ones
+     * defined in request object (if included).
+     * 
+     * @param profileRequestContext the profile request context to operate on
+     * @return the claims set
+     */
     @Nonnull
     protected Map<String,Object> buildClaimsSet(@Nonnull final ProfileRequestContext profileRequestContext) {
         final OIDCAuthenticationResponseContext oidcContext = getOidcResponseContext();
         assert oidcContext != null;
 
         assert requestMessage != null;
-        final Map<String, Object> claimsSet = requestMessage.getAuthorizationRequest().toJWTClaimsSet().getClaims();
+        final Map<String, Object> claimsSet = getRequestClaimsSetWithoutRequestObject(requestMessage);
         final JWT requestObject = oidcContext.getRequestObject();
         if (requestObject != null) {
             try {
@@ -191,4 +201,29 @@ public class FormOutbounPushedAuthorizationResponseMessage extends AbstractOAuth
         assert claimsSet != null;
         return claimsSet;
     }
+
+    /**
+     * Get the claims set included in the pushed authorization request form parameters, not including the request
+     * object if it exists.
+     * 
+     * @param requestMessage the request message to operate on
+     * @return the claims set
+     */
+    @Nonnull protected Map<String,Object> getRequestClaimsSetWithoutRequestObject(
+            @Nonnull final PushedAuthorizationRequest requestMessage) {
+        final AuthorizationRequest authorizationRequest = requestMessage.getAuthorizationRequest();
+        final Map<String, Object> result = new HashMap<>();
+        if (authorizationRequest.specifiesRequestObject()) {
+            final Map<String, List<String>> parameters = authorizationRequest.toParameters();
+            parameters.remove("request");
+            try {
+                result.putAll(AuthorizationRequest.parse(parameters).toJWTClaimsSet().getClaims());
+            } catch (final com.nimbusds.oauth2.sdk.ParseException e) {
+                log.error("{} Could not rebuild authorization request without request object", getLogPrefix());
+            }
+        } else {
+            result.putAll(authorizationRequest.toJWTClaimsSet().getClaims());
+        }
+        return result;
+    }
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AbstractPushedAuthorizationRequestComponent.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AbstractPushedAuthorizationRequestComponent.java
new file mode 100644
index 00000000..c4371889
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AbstractPushedAuthorizationRequestComponent.java
@@ -0,0 +1,38 @@
+/*
+ * 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.idp.plugin.oidc.op.profile.logic;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.storage.StorageService;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+
+/**
+ * Base component for the pushed authorization request URI serializer and deserializer functions.
+ */
+public class AbstractPushedAuthorizationRequestComponent extends AbstractIdentifiableInitializableComponent {
+
+    /** The prefix value used with the request_uri values built via PAR. */
+    @Nonnull @NotEmpty public static final String PAR_REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:";
+
+    /** The prefix value used with the request_uri values buikt via PAR with storage service serializer. */
+    @Nonnull @NotEmpty public static final String PAR_REQUEST_URI_STORAGE_PREFIX = PAR_REQUEST_URI_PREFIX + "ss:";
+
+    /** The context name in the {@link StorageService}. */
+    @Nonnull @NotEmpty public static final String STORAGE_CONTEXT_NAME = "oidcPushedAuthorizationRequests";
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPushedAuthorizationRequestUriDeserializationFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPushedAuthorizationRequestUriDeserializationFunction.java
index f8a24629..89de84ac 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPushedAuthorizationRequestUriDeserializationFunction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPushedAuthorizationRequestUriDeserializationFunction.java
@@ -33,7 +33,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.component.AbstractInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -44,7 +43,8 @@ import net.shibboleth.shared.security.DataSealerException;
 /**
  * Default deserialization function for decoding the request URI into claims set within OAuth2 PAR.
  */
-public class DefaultPushedAuthorizationRequestUriDeserializationFunction extends AbstractInitializableComponent
+public class DefaultPushedAuthorizationRequestUriDeserializationFunction
+    extends AbstractPushedAuthorizationRequestComponent
     implements BiFunction<ProfileRequestContext,URI,Map<String,Object>> {
 
     /** Class logger. */
@@ -60,7 +60,7 @@ public class DefaultPushedAuthorizationRequestUriDeserializationFunction extends
     /** Message replay cache instance to use. */
     @NonnullAfterInit private ReplayCache replayCache;
 
-    /** The lifetime for the sealed object. */
+    /** The lifetime for replay cache record. */
     @Nonnull private Duration lifetime;
 
     /**
@@ -103,9 +103,9 @@ public class DefaultPushedAuthorizationRequestUriDeserializationFunction extends
     }
 
     /**
-     * Set the object mapper used for serializing the claims set.
+     * Set the lifetime for replay cache record.
      * 
-     * @param mapper What to set.
+     * @param duration What to set.
      */
     public void setLifetime(@Nonnull final Duration duration) {
         checkSetterPreconditions();
@@ -134,7 +134,7 @@ public class DefaultPushedAuthorizationRequestUriDeserializationFunction extends
     public Map<String,Object> apply(@Nullable final ProfileRequestContext profileRequestContext,
             @Nullable final URI uri) {
         if (uri != null) {
-            final String sealedValue = uri.toString().replace("urn:ietf:params:oauth:request_uri:", "");
+            final String sealedValue = uri.toString().replace(PAR_REQUEST_URI_PREFIX, "");
             assert sealedValue != null;
             try {
                 final String unsealedValue = dataSealer.unwrap(sealedValue);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPushedAuthorizationRequestUriSerializationFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPushedAuthorizationRequestUriSerializationFunction.java
index 68d4eb62..fe37bcd1 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPushedAuthorizationRequestUriSerializationFunction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPushedAuthorizationRequestUriSerializationFunction.java
@@ -33,7 +33,6 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.component.AbstractInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.FunctionSupport;
@@ -46,7 +45,8 @@ import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrat
 /**
  * Default serialization function for the request URI claims set within OAuth2 PAR.
  */
-public class DefaultPushedAuthorizationRequestUriSerializationFunction extends AbstractInitializableComponent
+public class DefaultPushedAuthorizationRequestUriSerializationFunction
+    extends AbstractPushedAuthorizationRequestComponent
     implements BiFunction<ProfileRequestContext,Map<String,Object>,URI> {
 
     /** Class logger. */
@@ -62,6 +62,9 @@ public class DefaultPushedAuthorizationRequestUriSerializationFunction extends A
     /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
     @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
 
+    /** The xmlSafe-flag passed to the identifier generator. */
+    private boolean xmlSafeIdentifier;
+
     /** The lifetime for the sealed object. */
     @Nonnull private Duration lifetime;
 
@@ -73,6 +76,7 @@ public class DefaultPushedAuthorizationRequestUriSerializationFunction extends A
         assert fiveMins != null;
         lifetime = fiveMins;
         idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
+        xmlSafeIdentifier = false;
     }
     
     /**
@@ -96,9 +100,9 @@ public class DefaultPushedAuthorizationRequestUriSerializationFunction extends A
     }
 
     /**
-     * Set the object mapper used for serializing the claims set.
+     * Set the lifetime for the sealed object.
      * 
-     * @param mapper What to set.
+     * @param duration What to set.
      */
     public void setLifetime(@Nonnull final Duration duration) {
         checkSetterPreconditions();
@@ -119,6 +123,17 @@ public class DefaultPushedAuthorizationRequestUriSerializationFunction extends A
                 Constraint.isNotNull(strategy, "Identifier generation strategy cannot be null");
     }
 
+    /**
+     * Set the xmlSafe-flag passed to the identifier generator
+     * 
+     * @param flag xmlSafe-flag
+     */
+    public void setXmlSafeIdentifier(final boolean flag) {
+        checkSetterPreconditions();
+
+        xmlSafeIdentifier = flag;
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -144,13 +159,13 @@ public class DefaultPushedAuthorizationRequestUriSerializationFunction extends A
                 return null;
             }
             final Map<String, Object> input = new HashMap<String, Object>(claimsSet);
-            input.put("jti", idGenerator.generateIdentifier(false)); //TODO flag?
+            input.put("jti", idGenerator.generateIdentifier(xmlSafeIdentifier));
             assert expiration != null;
             try {
                 final String serializedClaimsSet = objectMapper.writeValueAsString(input);
                 assert serializedClaimsSet != null;
                 final String sealedClaimsSet = dataSealer.wrap(serializedClaimsSet, expiration);
-                final String result = "urn:ietf:params:oauth:request_uri:" + sealedClaimsSet;
+                final String result = PAR_REQUEST_URI_PREFIX + sealedClaimsSet;
                 return new URI(result);
             } catch (final JsonProcessingException e) {
                 log.error("Could not transform the given claims set into JSON", e);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/StorageServicePushedAuthorizationRequestUriDeserializationFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/StorageServicePushedAuthorizationRequestUriDeserializationFunction.java
index b1b93431..81f0b38b 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/StorageServicePushedAuthorizationRequestUriDeserializationFunction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/StorageServicePushedAuthorizationRequestUriDeserializationFunction.java
@@ -16,7 +16,6 @@ package net.shibboleth.idp.plugin.oidc.op.profile.logic;
 
 import java.io.IOException;
 import java.net.URI;
-import java.time.Duration;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
@@ -34,8 +33,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.NotEmpty;
-import net.shibboleth.shared.component.AbstractInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -43,12 +40,10 @@ import net.shibboleth.shared.primitive.LoggerFactory;
 /**
  * Default deserialization function for decoding the request URI into claims set within OAuth2 PAR.
  */
-public class StorageServicePushedAuthorizationRequestUriDeserializationFunction extends AbstractInitializableComponent
+public class StorageServicePushedAuthorizationRequestUriDeserializationFunction
+    extends AbstractPushedAuthorizationRequestComponent
     implements BiFunction<ProfileRequestContext,URI,Map<String,Object>> {
 
-    /** The context name in the {@link StorageService}. */
-    @Nonnull @NotEmpty public static final String CONTEXT_NAME = "oidcPushedAuthorizationRequests";
-
     /** Class logger. */
     @Nonnull private final Logger log =
             LoggerFactory.getLogger(StorageServicePushedAuthorizationRequestUriDeserializationFunction.class);
@@ -59,18 +54,6 @@ public class StorageServicePushedAuthorizationRequestUriDeserializationFunction
     /** Storage service used for storing the claims set on server-side. */
     @NonnullAfterInit private StorageService storageService;
 
-    /** The lifetime for the sealed object. */
-    @Nonnull private Duration lifetime;
-
-    /**
-     * Constructor.
-     */
-    public StorageServicePushedAuthorizationRequestUriDeserializationFunction() {
-        final Duration fiveMins = Duration.ofMinutes(5);
-        assert fiveMins != null;
-        lifetime = fiveMins;
-    }
-
     /**
      * Set the object mapper used for deserializing the claims set.
      * 
@@ -91,17 +74,6 @@ public class StorageServicePushedAuthorizationRequestUriDeserializationFunction
         storageService = Constraint.isNotNull(storage, "StorageService cannot be null");
     }
 
-    /**
-     * Set the object mapper used for serializing the claims set.
-     * 
-     * @param mapper What to set.
-     */
-    public void setLifetime(@Nonnull final Duration duration) {
-        checkSetterPreconditions();
-        lifetime = Constraint.isNotNull(duration, "Lifetime cannot be null");
-        Constraint.isTrue(!lifetime.isZero() && !lifetime.isNegative(), "Lifetime must be greater than 0");
-    }
-
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -120,15 +92,15 @@ public class StorageServicePushedAuthorizationRequestUriDeserializationFunction
     public Map<String,Object> apply(@Nullable final ProfileRequestContext profileRequestContext,
             @Nullable final URI uri) {
         if (uri != null) {
-            final String jti = uri.toString().replace("urn:ietf:params:oauth:request_uri:ss:", "");
+            final String jti = uri.toString().replace(PAR_REQUEST_URI_STORAGE_PREFIX, "");
             assert jti != null;
             try {
-                final StorageRecord<?> storageRecord = storageService.read(CONTEXT_NAME, jti);
+                final StorageRecord<?> storageRecord = storageService.read(STORAGE_CONTEXT_NAME, jti);
                 if (storageRecord == null) {
                     log.debug("Could not find any records with jti {}", jti);
                     return null;
                 }
-                if (storageService.delete(CONTEXT_NAME, jti)) {
+                if (storageService.delete(STORAGE_CONTEXT_NAME, jti)) {
                     log.debug("Storage record {} successfully deleted", jti);
                 }
                 final Map<String,Object> result = objectMapper.readValue(storageRecord.getValue(),
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/StorageServicePushedAuthorizationRequestUriSerializationFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/StorageServicePushedAuthorizationRequestUriSerializationFunction.java
index 03f9c112..e62abb11 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/StorageServicePushedAuthorizationRequestUriSerializationFunction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/StorageServicePushedAuthorizationRequestUriSerializationFunction.java
@@ -35,14 +35,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.NotEmpty;
-import net.shibboleth.shared.component.AbstractInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.FunctionSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.shared.security.DataSealer;
-import net.shibboleth.shared.security.DataSealerException;
 import net.shibboleth.shared.security.IdentifierGenerationStrategy;
 import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrategy;
 
@@ -50,12 +46,10 @@ import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrat
  * A serialization function for the request URI claims set within OAuth2 PAR. The claims set is stored in the
  * configured {@link StorageService} with the token identifier.
  */
-public class StorageServicePushedAuthorizationRequestUriSerializationFunction extends AbstractInitializableComponent
+public class StorageServicePushedAuthorizationRequestUriSerializationFunction
+    extends AbstractPushedAuthorizationRequestComponent
     implements BiFunction<ProfileRequestContext,Map<String,Object>,URI> {
 
-    /** The context name in the {@link StorageService}. */
-    @Nonnull @NotEmpty public static final String CONTEXT_NAME = "oidcPushedAuthorizationRequests";
-
     /** Class logger. */
     @Nonnull private final Logger log =
             LoggerFactory.getLogger(StorageServicePushedAuthorizationRequestUriSerializationFunction.class);
@@ -69,7 +63,10 @@ public class StorageServicePushedAuthorizationRequestUriSerializationFunction ex
     /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
     @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
 
-    /** The lifetime for the sealed object. */
+    /** The xmlSafe-flag passed to the identifier generator. */
+    private boolean xmlSafeIdentifier;
+
+    /** The lifetime for the storage record. */
     @Nonnull private Duration lifetime;
 
     /**
@@ -80,6 +77,7 @@ public class StorageServicePushedAuthorizationRequestUriSerializationFunction ex
         assert fiveMins != null;
         lifetime = fiveMins;
         idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
+        xmlSafeIdentifier = false;
     }
     
     /**
@@ -103,9 +101,9 @@ public class StorageServicePushedAuthorizationRequestUriSerializationFunction ex
     }
 
     /**
-     * Set the object mapper used for serializing the claims set.
+     * Set the lifetime for the storage record.
      * 
-     * @param mapper What to set.
+     * @param duration What to set.
      */
     public void setLifetime(@Nonnull final Duration duration) {
         checkSetterPreconditions();
@@ -113,6 +111,17 @@ public class StorageServicePushedAuthorizationRequestUriSerializationFunction ex
         Constraint.isTrue(!lifetime.isZero() && !lifetime.isNegative(), "Lifetime must be greater than 0");
     }
 
+    /**
+     * Set the xmlSafe-flag passed to the identifier generator
+     * 
+     * @param flag xmlSafe-flag
+     */
+    public void setXmlSafeIdentifier(final boolean flag) {
+        checkSetterPreconditions();
+
+        xmlSafeIdentifier = flag;
+    }
+
     /**
      * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
      * 
@@ -151,14 +160,14 @@ public class StorageServicePushedAuthorizationRequestUriSerializationFunction ex
                 return null;
             }
             final Map<String, Object> input = new HashMap<String, Object>(claimsSet);
-            final String jti = idGenerator.generateIdentifier(false); //TODO flag?
+            final String jti = idGenerator.generateIdentifier(xmlSafeIdentifier);
             input.put("jti", jti);
             assert expiration != null;
             try {
                 final String serializedClaimsSet = objectMapper.writeValueAsString(input);
                 assert serializedClaimsSet != null;
-                if (storageService.create(CONTEXT_NAME, jti, serializedClaimsSet, expiration.toEpochMilli())) {
-                    final String result = "urn:ietf:params:oauth:request_uri:ss:" + jti;
+                if (storageService.create(STORAGE_CONTEXT_NAME, jti, serializedClaimsSet, expiration.toEpochMilli())) {
+                    final String result = PAR_REQUEST_URI_STORAGE_PREFIX + jti;
                     return new URI(result);
                 }
                 log.error("Existing record with id {} already found in the storage service", jti);
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 71c86905..31667fd0 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
@@ -316,6 +316,7 @@
         scope="prototype"
         p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
         p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
+        p:xmlSafeIdentifier="%{idp.oidc.xmlSafeIdentifiers:true}"
         p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy"/>
 
     <bean id="DefaultStorageServiceRequestUriSerializerFunction"
@@ -323,6 +324,7 @@
         scope="prototype"
         p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
         p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy"
+        p:xmlSafeIdentifier="%{idp.oidc.xmlSafeIdentifiers:true}"
         p:storageService-ref="#{'%{idp.oauth2.par.StorageService:shibboleth.StorageService}'.trim()}" />
 
     <bean id="BuildErrorResponseFromEvent"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index f05dffcc..13a2aac4 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -83,10 +83,10 @@
           p:requireIdTokenHint="%{idp.oidc.logout.requireIdTokenHint:true}"
           p:encryptionOptional="%{idp.oidc.logout.encryptionOptional:true}"/>
 
-    <bean id="OAUTH2.PAR" parent="AbstractOIDCProfile" lazy-init="true"
+    <bean id="OAUTH2.PAR" parent="OIDC.SSO" lazy-init="true"
         class="net.shibboleth.oidc.profile.oauth2.config.impl.DefaultOAuth2PushedAuthorizationRequestConfiguration"
         p:issuer-ref="shibboleth.oidc.issuer"
-        p:tokenEndpointAuthMethods="%{idp.oidc.dynreg.tokenEndpointAuthMethods:client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt}"
+        p:tokenEndpointAuthMethods="%{idp.oidc.par.tokenEndpointAuthMethods:client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt}"
         p:claimsValidator-ref="DefaultJWTClaimsValidator"
         p:unregisteredClientPolicy="#{getObject('shibboleth.oidc.DefaultUnregisteredClientPolicy')}" />
 
@@ -640,7 +640,7 @@
         </property>
     </bean>
 
-    <bean id="OAUTH2.PAR.MDDriven" parent="AbstractMDDrivenOAuthClientAuthenticatableProfile" lazy-init="true"
+    <bean id="OAUTH2.PAR.MDDriven" parent="OIDC.SSO.MDDriven" lazy-init="true"
             class="net.shibboleth.oidc.profile.oauth2.config.impl.DefaultOAuth2PushedAuthorizationRequestConfiguration">
         <property name="issuerLookupStrategy">
             <bean parent="shibboleth.MDDrivenStringProperty" p:propertyName="issuer"
@@ -651,7 +651,7 @@
                 <property name="defaultValue">
                     <bean parent="shibboleth.CommaDelimStringArray">
                         <constructor-arg type="java.lang.String"
-                            value="%{idp.oidc.dynreg.tokenEndpointAuthMethods:client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt}" />
+                            value="%{idp.oidc.par.tokenEndpointAuthMethods:client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt}" />
                     </bean>
                 </property>
             </bean>
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 9585535a..ea3ec55c 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
@@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.webflow.executor.FlowExecutionResult;
 import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Factory;
 import org.testng.annotations.Test;
 
 import com.nimbusds.jose.JWSAlgorithm;
@@ -38,6 +39,7 @@ import com.nimbusds.oauth2.sdk.PushedAuthorizationSuccessResponse;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
 
+import net.shibboleth.idp.session.SessionException;
 import net.shibboleth.oidc.security.credential.JWKCredential;
 import net.shibboleth.shared.collection.Pair;
 import net.shibboleth.shared.component.ComponentInitializationException;
@@ -179,6 +181,29 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
         verifyAuthorizeEndpoint(clientIdSaml, response.getRequestURI().toString());
     }
 
+    @Test
+    public void testWithAuthorizationCodeFlowRequestObjectEnforcedNoRO() throws IOException, SessionException {
+        final String clientId = "mockClientIdRequestObjectEnforced";
+        storeMetadata(storageService, clientId, clientSecret, scope, "https://example.org/cb");
+        setBasicAuth(clientId, clientSecret);
+        setHttpFormRequest("POST", createRequestParameters(clientId));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+        removeMetadata(storageService, "mockClientIdRequestObjectEnforced");
+    }
+
+    @Factory
+    public Object[] createRequestObjectSecurityTests() {
+        return new Object[] {
+                new PushedAuthorizeRequestObjectJWSTest(true),
+                new PushedAuthorizeRequestObjectJWSTest(false),
+                new PushedAuthorizeRequestObjectJWETest(false, false),
+                new PushedAuthorizeRequestObjectJWETest(false, true),
+                new PushedAuthorizeRequestObjectJWETest(true, false),
+                new PushedAuthorizeRequestObjectJWETest(true, true)
+                };
+    }
+
     protected Pair<String, String> getErrorDetaisForJWTValidation() {
         return new Pair<>("invalid_client", "Client authentication failed");
     }
@@ -222,7 +247,7 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
         request.removeHeader("Authorization");
 
     }
-    protected Map<String,String> createRequestParameters(final String id) {
+    protected static Map<String,String> createRequestParameters(final String id) {
         final Map<String,String> result = new HashMap<>();
         result.put("client_id", id);
         result.put("response_type", "code");
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeRequestObjectJWETest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeRequestObjectJWETest.java
new file mode 100644
index 00000000..a2c0840e
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeRequestObjectJWETest.java
@@ -0,0 +1,366 @@
+/*
+ * 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.idp.plugin.oidc.op.profile.flow;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.text.ParseException;
+import java.util.Map;
+
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+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.rp.OIDCClientMetadata;
+
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+
+public class PushedAuthorizeRequestObjectJWETest extends IssuedEncryptedJWTTest {
+
+    String defaultClientIdEncryptionEnforced = "mockClientIdRequestObjectEncryptionEnforced";
+
+    public PushedAuthorizeRequestObjectJWETest(final boolean testSigned, final boolean encryptionOptional) {
+        super(JWT_FETCHING_TYPE.REQUEST_OBJECT, PushedAuthorizeFlowTest.FLOW_ID, testSigned, encryptionOptional);
+    }
+
+    @Override @Test
+    public void testJwtEncryption_noSigAlgNorEncSpecified() throws Exception {
+        // use plain request object
+        final JWT jwt = obtainRequestObject(null, null, null, null, null, null);
+        if (encryptionOptional) {
+            assertSuccessRequestObjectResponse(jwt.serialize(), null, null, null, defaultClientSecret64B, null);
+        } else {
+            assertErrorRequestObjectResponse(jwt.serialize(), null, null, null, defaultClientSecret64B, null);
+        }
+    }
+
+    @Test
+    public void testJwtEncryption_noSigAlgNorEncSpecified_noRequestObject() throws Exception {
+        if (encryptionOptional) {
+            assertSuccessRequestObjectResponse("", null, null, null, defaultClientSecret64B, null);
+        } else {
+            assertErrorRequestObjectResponse("", null, null, null, defaultClientSecret64B, null);
+        }
+    }
+
+    @Test
+    public void testJwtEncryption_noSigAlgNorEncSpecified_signedRequestObject() throws Exception {
+        final JWT jwt = obtainRequestObject(defaultClientSecret64B, null, null, JWSAlgorithm.HS256, null, null);
+        if (encryptionOptional) {
+            assertSuccessRequestObjectResponse(jwt.serialize(), null, null, null, defaultClientSecret64B, null);
+        } else {
+            assertErrorRequestObjectResponse(jwt.serialize(), null, null, null, defaultClientSecret64B, null);
+        }
+    }
+
+    @Test
+    public void testJwtEncryption_noSigAlgNorEncSpecified_encryptedRequestObject() throws Exception {
+        if (testSignedJwt) {
+            for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+                for (final JWEAlgorithm jwe : JWE_ALGORITHMS) {
+                    for (final EncryptionMethod method : ENCRYPTION_METHODS) {
+                        final JWT jwt = obtainRequestObject(defaultClientSecret64B, getProviderEncryptionKey(jwe),
+                                getSigningKey(jwsAlgorithm), jwsAlgorithm, jwe, method);
+                        assertSuccessRequestObjectResponse(jwt.serialize(), null, null, null, defaultClientSecret64B,
+                                getSignatureVerificationKey(jwsAlgorithm));
+                    }
+                }
+            }
+        } else {
+            for (final JWEAlgorithm jwe : JWE_ALGORITHMS) {
+                for (final EncryptionMethod method : ENCRYPTION_METHODS) {
+                    final JWT jwt = obtainRequestObject(defaultClientSecret64B, getProviderEncryptionKey(jwe), null,
+                            null, jwe, method);
+                    assertSuccessRequestObjectResponse(jwt.serialize(), null, null, null, defaultClientSecret64B, null);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testRequestObjectEncryption_onlySigAlgNoEncSpecified() throws Exception {
+        final JWT jwt = obtainRequestObject(null, null, rsaPrivateKey, JWSAlgorithm.RS256, null, null);
+        if (encryptionOptional) {
+            assertSuccessRequestObjectResponse(jwt.serialize(), JWSAlgorithm.RS256, null, null, null, rsaPublicKey);
+        } else {
+            assertErrorRequestObjectResponse(jwt.serialize(), JWSAlgorithm.RS256, null, null, null, rsaPublicKey);
+        }
+    }
+
+    protected void assertSecretBasedEncryption(final JWEAlgorithm jweAlgorithm, final EncryptionMethod method) {
+        if (testSignedJwt) {
+            for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+                final JWT jwt = obtainRequestObject(defaultClientSecret64B, null, getSigningKey(jwsAlgorithm),
+                        jwsAlgorithm, jweAlgorithm, method);
+                assertSuccessRequestObjectResponse(jwt.serialize(), jwsAlgorithm, jweAlgorithm, method,
+                        defaultClientSecret64B, getSignatureVerificationKey(jwsAlgorithm));
+            }
+        } else {
+            final JWT jwt = obtainRequestObject(defaultClientSecret, null, null, null, jweAlgorithm, method);
+            assertSuccessRequestObjectResponse(jwt.serialize(), null, jweAlgorithm, method, defaultClientSecret, null);
+        }
+    }
+    
+    protected PublicKey getProviderEncryptionKeyViaKeyType(final PublicKey publicKey) {
+        if (publicKey instanceof ECPublicKey) {
+            return loadCredential("/credentials/idp-encryption-ec.jwk").getPublicKey();
+        } else {
+            return loadEncryptionCredential().getPublicKey();
+        }
+    }
+
+    protected PublicKey getRandomEncryptionKey(final JWEAlgorithm jweAlgorithm) {
+        if (JWEAlgorithm.Family.ECDH_ES.contains(jweAlgorithm)) {
+            try {
+                return ecKey.toPublicKey();
+            } catch (JOSEException e) {
+                Assert.fail("Could not obtain a public key from the ECKey object", e);
+            }
+        }
+        return rsaPublicKey;
+    }
+
+    protected PublicKey getProviderEncryptionKey(final JWEAlgorithm jweAlgorithm) {
+        if (JWEAlgorithm.Family.ECDH_ES.contains(jweAlgorithm)) {
+            return loadCredential("/credentials/idp-encryption-ec.jwk").getPublicKey();
+        }
+        return loadEncryptionCredential().getPublicKey();
+    }
+
+    protected void assertPublicKeyBasedEncryption(final PublicKey publicKey, final PrivateKey privateKey,
+            final JWEAlgorithm jweAlgorithm, final EncryptionMethod method) {
+        final PublicKey encryptionKey = getProviderEncryptionKeyViaKeyType(publicKey);
+        if (testSignedJwt) {
+            for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+                final JWT jwt = obtainRequestObject(defaultClientSecret64B, encryptionKey, getSigningKey(jwsAlgorithm),
+                        jwsAlgorithm, jweAlgorithm, method);
+                assertSuccessRequestObjectResponse(jwt.serialize(), jwsAlgorithm, jweAlgorithm, method,
+                        defaultClientSecret64B, getSignatureVerificationKey(jwsAlgorithm));
+            }
+        } else {
+            final JWT jwt = obtainRequestObject(defaultClientSecret64B, encryptionKey, privateKey, null, jweAlgorithm,
+                    method);
+            assertSuccessRequestObjectResponse(jwt.serialize(), null, jweAlgorithm, method, defaultClientSecret64B,
+                    publicKey);
+        }
+    }
+    
+    @Override
+    protected void assertNoSymmetricKeyResponse(final JWEAlgorithm jweAlgorithm, final EncryptionMethod method)
+            throws Exception {
+        final PublicKey encryptionKey = getProviderEncryptionKey(jweAlgorithm);
+        if (testSignedJwt) {
+            for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+                final JWT jwt = obtainRequestObject(defaultClientSecret64B, rsaPublicKey, getSigningKey(jwsAlgorithm),
+                        jwsAlgorithm, jweAlgorithm, method);
+                Assert.assertNotNull(jwt, "The JWT could not be obtained with JWS alg " + jwsAlgorithm);
+                assertErrorRequestObjectResponse(jwt.serialize(), jwsAlgorithm, jweAlgorithm, method, null,
+                        getSignatureVerificationKey(jwsAlgorithm));
+            }
+        } else {
+            final JWT jwt = obtainRequestObject(defaultClientSecret64B, encryptionKey, rsaPrivateKey, null,
+                    jweAlgorithm, method);
+            assertErrorRequestObjectResponse(jwt.serialize(),
+                    null, jweAlgorithm, method, null, encryptionKey);
+        }
+    }
+
+    @Override
+    protected void assertExcludedAlgorithm(final JWEAlgorithm jweAlgorithm, final EncryptionMethod method) {
+        if (testSignedJwt) {
+            for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+                final JWT jwt = obtainRequestObject(defaultClientSecret64B, getProviderEncryptionKey(jweAlgorithm),
+                        getSigningKey(jwsAlgorithm), jwsAlgorithm, jweAlgorithm, method);
+                assertErrorRequestObjectResponse(jwt.serialize(), jwsAlgorithm, jweAlgorithm, method,
+                        defaultClientSecret64B, getSignatureVerificationKey(jwsAlgorithm));
+            }
+        } else {
+            final JWT jwt = obtainRequestObject(defaultClientSecret64B, getProviderEncryptionKey(jweAlgorithm), null,
+                    null, jweAlgorithm, method);
+            Assert.assertTrue(jwt instanceof EncryptedJWT, "Was not encrypted " + jweAlgorithm);
+            assertErrorRequestObjectResponse(jwt.serialize(), null, jweAlgorithm, method, defaultClientSecret64B,
+                    rsaPublicKey);
+        }
+    }
+    
+    @Override
+    protected void assertNoPublicKeyResponse(final JWEAlgorithm jweAlgorithm, final EncryptionMethod method)
+ {
+        final PublicKey encryptionKey = getRandomEncryptionKey(jweAlgorithm);
+        if (testSignedJwt) {
+            for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+                final JWT jwt = obtainRequestObject(defaultClientSecret64B, encryptionKey, getSigningKey(jwsAlgorithm),
+                        jwsAlgorithm, jweAlgorithm, method);
+                assertErrorRequestObjectResponse(jwt.serialize(),
+                            jwsAlgorithm, jweAlgorithm, method, defaultClientSecret64B, null);
+            }
+        } else {
+            final JWT jwt = obtainRequestObject(defaultClientSecret64B, encryptionKey, rsaPrivateKey, null,
+                    jweAlgorithm, method);
+            assertErrorRequestObjectResponse(jwt.serialize(), null, jweAlgorithm, method, defaultClientSecret64B, null);
+        }
+    }
+
+    protected void assertErrorRequestObjectResponse(final String requestObject,
+            final JWSAlgorithm requestObjectSigAlg, final JWEAlgorithm requestObjectEncAlg,
+            final EncryptionMethod requestObjectEncMethod, final String clientSecret, final PublicKey publicKey) {
+        request.setMethod("GET");
+        final String clientId = encryptionOptional ? defaultClientId : defaultClientIdEncryptionEnforced;
+        final Map<String, String> requestParams = PushedAuthorizeFlowTest.createRequestParameters(clientId);
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        if (clientSecret != null) {
+            requestParams.put("client_secret", clientSecret);
+            metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST);
+        } else {
+            return;
+        }
+        requestParams.put("request", requestObject);
+
+        metadata.setScope(new Scope("openid"));
+        metadata.setRequestObjectJWSAlg(requestObjectSigAlg);
+        metadata.setRequestObjectJWEAlg(requestObjectEncAlg);
+        metadata.setRequestObjectJWEEnc(requestObjectEncMethod);
+        if (publicKey != null) {
+            metadata.setJWKSet(super.buildJWKSet(publicKey));
+        }
+        try {
+            metadata.setRedirectionURI(new URI("https://example.org/cb"));
+            storeMetadataObject(storageService, clientId, clientSecret, metadata);
+            setHttpFormRequest("POST", requestParams);
+            final FlowExecutionResult result = flowExecutor.launchExecution(flowId, null, externalContext);
+            removeMetadata(storageService, clientId);
+            assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+        } catch (final IOException | URISyntaxException e) {
+            Assert.fail();
+        }
+    }
+    
+    protected void assertSuccessRequestObjectResponse(final String requestObject,
+            final JWSAlgorithm requestObjectSigAlg, final JWEAlgorithm requestObjectEncAlg,
+            final EncryptionMethod requestObjectEncMethod, final String clientSecret, final PublicKey publicKey) {
+        request.setMethod("GET");
+        final String clientId = encryptionOptional ? defaultClientId : defaultClientIdEncryptionEnforced;
+        final Map<String, String> requestParams = PushedAuthorizeFlowTest.createRequestParameters(clientId);
+        requestParams.put("request", requestObject);
+
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        if (clientSecret != null) {
+            requestParams.put("client_secret", clientSecret);
+            metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST);
+        } else {
+            return;
+        }
+        metadata.setScope(new Scope("openid"));
+        metadata.setRequestObjectJWSAlg(requestObjectSigAlg);
+        metadata.setRequestObjectJWEAlg(requestObjectEncAlg);
+        metadata.setRequestObjectJWEEnc(requestObjectEncMethod);
+        if (publicKey != null) {
+            metadata.setJWKSet(super.buildJWKSet(publicKey));
+        }
+        final FlowExecutionResult result;
+        try {
+            metadata.setRedirectionURI(new URI("https://example.org/cb"));
+            storeMetadataObject(storageService, clientId, clientSecret, metadata);
+            setHttpFormRequest("POST", requestParams);
+            result = flowExecutor.launchExecution(flowId, null, externalContext);
+            removeMetadata(storageService, clientId);
+        } catch (final IOException | URISyntaxException e) {
+            Assert.fail();
+            return;
+        }
+
+        final PushedAuthorizationSuccessResponse responseMessage = parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+        Assert.assertNotNull(responseMessage);
+        Assert.assertNotNull(responseMessage.getRequestURI());
+        Assert.assertNotNull(responseMessage.getLifetime());
+    }
+
+    protected void assertEncryptedSignedJwt(final JWT jwt, final JWSAlgorithm jwsAlg, final JWEAlgorithm jweAlg,
+            final EncryptionMethod method, final String clientSecret, final PrivateKey privateKey,
+            final PublicKey publicKey, final PublicKey jwsValidationKey) {
+        assertSuccessRequestObjectResponse(jwt.serialize(), jwsAlg, jweAlg, method, clientSecret, jwsValidationKey);
+    }
+
+    protected void assertSignedJwt(final JWT jwt, final JWSAlgorithm algorithm, final PublicKey publicKey,
+            final String clientSecret) {
+        assertSuccessRequestObjectResponse(jwt.serialize(), algorithm, null, null, clientSecret, publicKey);
+    }
+
+    protected JWT obtainRequestObject(final String clientSecret, final PublicKey publicKey,
+            final PrivateKey signingKey, final JWSAlgorithm storedJwsAlgorithm, final JWEAlgorithm storedJweAlgorithm,
+            final EncryptionMethod storedJweMethod) {
+        final String clientId = encryptionOptional ? defaultClientId : defaultClientIdEncryptionEnforced;
+        final String payload = AuthorizeFlowTest.getRequestObjectWithClaimsRequestPayload(clientId,
+                "https://example.org/cb");
+        final JWT jwt = processJwsForRequestObject(storedJwsAlgorithm, payload, clientSecret, signingKey);
+        
+        try {
+            if (storedJweAlgorithm != null) {
+                if (publicKey != null) {
+                    final BasicJWKCredential credential = new BasicJWKCredential();
+                    credential.setPublicKey(publicKey);
+                    return createEncryptedJWT(jwt.serialize(), storedJweAlgorithm, storedJweMethod, credential,
+                        clientSecret);
+                } else if (clientSecret != null) {
+                    return createEncryptedJWT(jwt.serialize(), storedJweAlgorithm, storedJweMethod, null, clientSecret,
+                            false);
+                }
+            }
+        } catch (JOSEException | ParseException e) {
+            Assert.fail("Could not encrypt the JWT", e);
+        }
+        return jwt;
+    }
+
+    protected JWT processJwsForRequestObject(final JWSAlgorithm storedJwsAlgorithm, final String payload,
+            final String clientSecret, final PrivateKey signingKey) {
+        try {
+            if (storedJwsAlgorithm == null) {
+                return new PlainJWT(JWTClaimsSet.parse(payload));
+            } else if (JWSAlgorithm.Family.EC.contains(storedJwsAlgorithm)) {
+                return createPrivateKeyJWT(JWTClaimsSet.parse(payload), (ECPrivateKey) signingKey, storedJwsAlgorithm);
+            } else if (JWSAlgorithm.Family.RSA.contains(storedJwsAlgorithm)) {
+                return createPrivateKeyJWT(JWTClaimsSet.parse(payload), (RSAPrivateKey) signingKey, storedJwsAlgorithm);
+            } else if (JWSAlgorithm.Family.HMAC_SHA.contains(storedJwsAlgorithm)) {
+                if (clientSecret == null) {
+                    return null;
+                }
+                return createSecretJWT(JWTClaimsSet.parse(payload), clientSecret, storedJwsAlgorithm);
+            }
+        } catch (JOSEException | ParseException e) {
+            Assert.fail(e.getMessage(), e);
+        }
+        return null;
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeRequestObjectJWSTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeRequestObjectJWSTest.java
new file mode 100644
index 00000000..63d38a81
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeRequestObjectJWSTest.java
@@ -0,0 +1,208 @@
+/*
+ * 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.idp.plugin.oidc.op.profile.flow;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.text.ParseException;
+import java.util.Map;
+
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+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.rp.OIDCClientMetadata;
+
+import net.shibboleth.shared.security.DataSealerException;
+
+public class PushedAuthorizeRequestObjectJWSTest extends IssuedSignedJWTTest {
+
+    String defaultClientIdSigningEnforced = "mockClientIdRequestObjectSigningEnforced";
+
+    private final boolean signingOptional;
+    
+    public PushedAuthorizeRequestObjectJWSTest(final boolean optionalSigning) {
+        super(JWT_FETCHING_TYPE.REQUEST_OBJECT, PushedAuthorizeFlowTest.FLOW_ID);
+        signingOptional = optionalSigning;
+    }
+
+    @Override @Test
+    public void testJwtSecurity_jwtSigAlgAndEncNotSpecified() throws Exception {
+        final JWT jwt = obtainJwt(null);
+        if (signingOptional) {
+            assertSuccessRequestObjectResponse(jwt.serialize(), null, defaultClientSecret64B, null);
+        } else {
+            assertErrorRequestObjectResponse(jwt.serialize(), null, defaultClientSecret64B, null);
+
+        }
+    }
+
+    @Override
+    protected JWT obtainJwt(final JWSAlgorithm jwsAlgorithm) {
+        return obtainJwt(defaultClientSecret64B, jwsAlgorithm);
+    }
+    
+    @Override
+    protected JWT obtainJwt(final String clientSecret, final JWSAlgorithm jwsAlgorithm) {
+        return obtainRequestObject(clientSecret, jwsAlgorithm);
+    }
+
+    @Override
+    protected void assertNoJwtResponse(final String clientId, final String clientSecret,
+            final PublicKey publicKey, final JWSAlgorithm jwsAlgorithm, final JWEAlgorithm jweAlgorithm,
+            final EncryptionMethod method, final JWT_FETCHING_TYPE fetchingType) {
+        final JWT jwt = obtainRequestObject(defaultClientSecret64B, jwsAlgorithm);
+        assertErrorRequestObjectResponse(jwt.serialize(), jwsAlgorithm, clientSecret, publicKey);
+    }
+
+    @Override
+    protected void assertExcludedAlgorithm(final String clientId, final String clientSecret, final PublicKey publicKey,
+            final JWSAlgorithm jwsAlgorithm) throws ParseException, DataSealerException, IOException {
+        final JWT jwt = obtainJwt(clientSecret, jwsAlgorithm);
+        assertErrorRequestObjectResponse(jwt.serialize(), jwsAlgorithm, clientSecret, publicKey);
+    }
+
+    protected void assertErrorRequestObjectResponse(final String requestObject,
+            final JWSAlgorithm requestObjectSigAlg, final String clientSecret, final PublicKey publicKey) {
+        request.setMethod("GET");
+        final String clientId = signingOptional ? defaultClientId : defaultClientIdSigningEnforced;
+
+        final Map<String, String> requestParams = PushedAuthorizeFlowTest.createRequestParameters(clientId);
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        if (clientSecret != null) {
+            requestParams.put("client_secret", clientSecret);
+            metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST);
+        } else {
+            return;
+        }
+        requestParams.put("request", requestObject);
+        metadata.setScope(new Scope("openid"));
+        metadata.setRequestObjectJWSAlg(requestObjectSigAlg);
+        if (publicKey != null) {
+            metadata.setJWKSet(super.buildJWKSet(publicKey));
+        }
+        setHttpFormRequest("POST", requestParams);
+        try {
+            metadata.setRedirectionURI(new URI("https://example.org/cb"));
+            storeMetadataObject(storageService, clientId, clientSecret, metadata);
+            final FlowExecutionResult result = flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+            removeMetadata(storageService, clientId);
+            assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+        } catch (final IOException | URISyntaxException e) {
+            Assert.fail();
+        }
+    }
+    
+    @SuppressWarnings("null")
+    protected void assertSuccessRequestObjectResponse(final String requestObject,
+            final JWSAlgorithm requestObjectSigAlg, final String clientSecret, final PublicKey publicKey) {
+        request.setMethod("GET");
+        final String clientId = signingOptional ? defaultClientId : defaultClientIdSigningEnforced;
+        final Map<String, String> requestParams = PushedAuthorizeFlowTest.createRequestParameters(clientId);
+
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        metadata.setScope(new Scope("openid"));
+        metadata.setRequestObjectJWSAlg(requestObjectSigAlg);
+        if (clientSecret != null) {
+            requestParams.put("client_secret", clientSecret);
+            metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST);
+        } else {
+            return;
+        }
+        if (publicKey != null) {
+            metadata.setJWKSet(super.buildJWKSet(publicKey));
+        }
+        requestParams.put("request", requestObject);
+        setHttpFormRequest("POST", requestParams);
+        final FlowExecutionResult result;
+        try {
+            metadata.setRedirectionURI(new URI("https://example.org/cb"));
+            storeMetadataObject(storageService, clientId, clientSecret != null ? clientSecret : defaultClientSecret64B, metadata);
+            result = flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+            removeMetadata(storageService, clientId);
+        } catch (final IOException | URISyntaxException e) {
+            Assert.fail();
+            return;
+        }
+
+        final PushedAuthorizationSuccessResponse responseMessage = parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+        Assert.assertNotNull(responseMessage);
+        Assert.assertNotNull(responseMessage.getRequestURI());
+        Assert.assertNotNull(responseMessage.getLifetime());
+    }
+
+    protected void assertSignedJwt(final JWT jwt, final JWSAlgorithm algorithm, final PublicKey publicKey,
+            final String clientSecret) {
+        assertSuccessRequestObjectResponse(jwt.serialize(), algorithm, clientSecret, publicKey);
+    }
+
+    protected JWT obtainRequestObject(final String clientSecret, final JWSAlgorithm storedJwsAlgorithm) {
+        final String clientId = signingOptional ? defaultClientId : defaultClientIdSigningEnforced;
+        final String payload = AuthorizeFlowTest.getRequestObjectWithClaimsRequestPayload(clientId,
+                "https://example.org/cb");
+
+        return processJwsForRequestObject(storedJwsAlgorithm, payload, clientSecret, getSigningKey(storedJwsAlgorithm));
+    }
+
+    protected static PrivateKey getSigningKey(final JWSAlgorithm jwsAlgorithm) {
+        if (JWSAlgorithm.ES256.equals(jwsAlgorithm)) {
+            return loadESSigningCredential().getPrivateKey();
+        } else if (JWSAlgorithm.ES384.equals(jwsAlgorithm)) {
+            return loadES384SigningCredential().getPrivateKey();
+        } else if (JWSAlgorithm.ES512.equals(jwsAlgorithm)) {
+            return loadES512SigningCredential().getPrivateKey();
+        }
+        return loadRSSigningCredential().getPrivateKey();
+    }
+
+    protected static JWT processJwsForRequestObject(final JWSAlgorithm storedJwsAlgorithm, final String payload,
+            final String clientSecret, final PrivateKey signingKey) {
+        try {
+            if (storedJwsAlgorithm == null) {
+                return new PlainJWT(JWTClaimsSet.parse(payload));
+            } else if (JWSAlgorithm.Family.EC.contains(storedJwsAlgorithm)) {
+                return createPrivateKeyJWT(JWTClaimsSet.parse(payload), (ECPrivateKey) signingKey, storedJwsAlgorithm);
+            } else if (JWSAlgorithm.Family.RSA.contains(storedJwsAlgorithm)) {
+                return createPrivateKeyJWT(JWTClaimsSet.parse(payload), (RSAPrivateKey) signingKey, storedJwsAlgorithm);
+            } else if (JWSAlgorithm.Family.HMAC_SHA.contains(storedJwsAlgorithm)) {
+                if (clientSecret == null) {
+                    return null;
+                }
+                return createSecretJWT(JWTClaimsSet.parse(payload), clientSecret, storedJwsAlgorithm);
+            }
+        } catch (JOSEException | ParseException e) {
+            Assert.fail(e.getMessage(), e);
+        }
+        return null;
+    }
+
+
+}
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 2c8507ef..caf1077a 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
@@ -135,6 +135,7 @@
             <property name="profileConfigurations">
                  <list>
                      <bean parent="OIDC.SSO.MDDriven" p:encryptionOptional="true" p:useRequestObject="true" p:signRequestObject="false"/>
+                     <bean parent="OAUTH2.PAR.MDDriven" p:encryptionOptional="true" p:useRequestObject="true" p:signRequestObject="false"/>
                      <bean parent="OAUTH2.Token.MDDriven" p:encryptionOptional="false" />
                      <bean parent="OIDC.UserInfo.MDDriven" p:encryptionOptional="false" />
                  </list>
@@ -144,6 +145,7 @@
             <property name="profileConfigurations">
                  <list>
                      <bean parent="OIDC.SSO.MDDriven" p:encryptionOptional="false" p:useRequestObject="true" p:encryptRequestObject="true" p:signRequestObject="false"/>
+                     <bean parent="OAUTH2.PAR.MDDriven" p:encryptionOptional="false" p:useRequestObject="true" p:encryptRequestObject="true" p:signRequestObject="false"/>
                      <bean parent="OAUTH2.Token.MDDriven" p:encryptionOptional="false" />
                      <bean parent="OIDC.UserInfo.MDDriven" p:encryptionOptional="false" />
                  </list>
@@ -153,6 +155,7 @@
             <property name="profileConfigurations">
                  <list>
                      <bean parent="OIDC.SSO.MDDriven" p:encryptionOptional="true" p:useRequestObject="true" p:signRequestObject="true"/>
+                     <bean parent="OAUTH2.PAR.MDDriven" p:encryptionOptional="true" p:useRequestObject="true" p:signRequestObject="true"/>
                      <bean parent="OAUTH2.Token.MDDriven" p:encryptionOptional="false" />
                      <bean parent="OIDC.UserInfo.MDDriven" p:encryptionOptional="false" />
                  </list>

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


More information about the commits mailing list