[java-idp-oidc] 03/03: JOIDC-200 - Support for OAuth2 Pushed Authorization Requests (PAR)

Henri Mikkonen henri.mikkonen at iki.fi
Wed Apr 24 11:17:23 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=d81a673e4c38707189e59cd22de875ed582eba9b

commit d81a673e4c38707189e59cd22de875ed582eba9b
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed Apr 24 14:16:45 2024 +0300

    JOIDC-200 - Support for OAuth2 Pushed Authorization Requests (PAR)
    
    https://shibboleth.atlassian.net/browse/JOIDC-200
    
    Initial incomplete version, supports very basic use case but still WIP.
    
    OAUTH2.PAR is the profile configuration reference bean.
    - Default empty/null requestUriType refers to opaque request_uri serializer: state is encoded fully in the token
      - "SS" refers to one exploiting configurable storage service
    
    - New requestObjectValidated-flag in OIDCAuthenticationResponseContext to signal if request request object is already validated
      - We don't need to re-validate request object fetched via OP's own URI in authorize-endpoint
    - Refactored AbstractAuthorizationRequestLookupFunction to be able to fetch parameters from PAR requests too
    - Refactored AbstractOAuthAuthorizationRequestAction to be able to fetch authz request from PAR requests too
    - Updated SetRequestObjectToResponseContext to use request URI deserializers for the request_uris that are not registered in metadata
      - I.e. if URI is not registered for the RP, it's assumed to be issued by OP
      - If deserialization is successful, then the requestObjectValidated-flag is set to true
    - Updated ValidateRequestObject to exploit the requestObjectValidated-flag
      - If it's set, then request object doesn't need re-validation
    - Created factory beans similarly to refresh token serializers and deserializers
      - shibboleth.oidc.PushedAuthorizationRequestUriSerializerFactory
      - shibboleth.oidc.PushedAuthorizationRequestUriDeserializerFactory
    
    Missing request object enforcement and signature/encryption enforcement in PAR.
    Missing unit tests for the new features.
---
 .../context/OIDCAuthenticationResponseContext.java |  25 ++
 ...AbstractAuthorizationRequestLookupFunction.java |  11 +-
 .../decoding/impl/BaseOAuth2RequestDecoder.java    |   2 +-
 .../OAuth2PushedAuthorizationRequestDecoder.java   |  87 ++++++
 .../AbstractOAuthAuthorizationRequestAction.java   |  28 +-
 ...dPushedAuthorizationErrorResponseFromEvent.java |  37 +++
 ...mOutbounPushedAuthorizationResponseMessage.java | 194 ++++++++++++
 .../SetAuthorizationCodeToResponseContext.java     |   6 +-
 .../impl/SetRequestObjectToResponseContext.java    |  75 ++++-
 .../ValidatePushedAuthorizationClientIDMatch.java  |  87 ++++++
 .../oauth2/profile/impl/ValidateRequestObject.java |  19 +-
 .../oauth2/profile/impl/ValidateResponseMode.java  |  13 +-
 ...AuthorizationRequestTypeValidationStrategy.java |  25 +-
 ...orizationRequestUriDeserializationFunction.java | 169 +++++++++++
 ...thorizationRequestUriSerializationFunction.java | 170 +++++++++++
 ...orizationRequestUriDeserializationFunction.java | 149 +++++++++
 ...thorizationRequestUriSerializationFunction.java | 181 +++++++++++
 .../op/profile/spring/TokenExtensionFactory.java   |  12 +-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  12 +
 .../pushed-authorization-beans.xml                 | 338 +++++++++++++++++++++
 .../pushed-authorization-flow.xml                  |  60 ++++
 .../idp/flows/oidc/authorize/authorize-beans.xml   |  29 +-
 .../idp/service/relying-party/postconfig.xml       |  32 +-
 .../idp/plugin/oidc/op/conf/oidc.properties        |   4 +-
 .../oidc/op/static/openid-configuration.json       |   4 +-
 .../op/profile/flow/PushedAuthorizeFlowTest.java   | 242 +++++++++++++++
 .../shibboleth/idp/module/conf/relying-party.xml   |   3 +
 27 files changed, 1982 insertions(+), 32 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
index 96f91431..a8a93ec8 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
@@ -128,6 +128,9 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
     /** Session identifier. */
     @Nullable private String sessionId;
 
+    /** Whether request object has already been validated. */
+    private boolean requestObjectValidated = false;
+
     /** Constructor. */
     public OIDCAuthenticationResponseContext() {
         validatedAudience = new ArrayList<>();
@@ -549,4 +552,26 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
         sessionId = sid;
     }
 
+    /**
+     * Get whether request object has already been validated.
+     * 
+     * @return true if validated, false if not
+     * 
+     * @since 4.2.0
+     */
+    public boolean isRequestObjectValidated() {
+        return requestObjectValidated;
+    }
+
+    /**
+     * Set whether request object has already been validated.
+     * 
+     * @param flag true if validated, false if not
+     * 
+     * @since 4.2.0
+     */
+    public void setRequestObjectValidated(final boolean flag) {
+        requestObjectValidated = flag;
+    }
+    
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractAuthorizationRequestLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractAuthorizationRequestLookupFunction.java
index cf536a28..5d0577fb 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractAuthorizationRequestLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractAuthorizationRequestLookupFunction.java
@@ -22,6 +22,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+import com.nimbusds.oauth2.sdk.PushedAuthorizationRequest;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
 import net.shibboleth.shared.logic.Constraint;
@@ -73,7 +74,8 @@ public abstract class AbstractAuthorizationRequestLookupFunction<T>
             return null;
         }
         final Object message = input.ensureInboundMessageContext().getMessage();
-        if (message == null || !(messageClass.isInstance(message))) {
+        if (message == null || 
+                (!(messageClass.isInstance(message)) && !(message instanceof PushedAuthorizationRequest))) {
             return null;
         }
         if (input.getOutboundMessageContext() != null) {
@@ -83,7 +85,12 @@ public abstract class AbstractAuthorizationRequestLookupFunction<T>
                 requestObject = ctx.getRequestObject();
             }
         }
-        
+
+        if (message instanceof PushedAuthorizationRequest pushedAuthorizationRequest) {
+            final AuthorizationRequest authorizationRequest = pushedAuthorizationRequest.getAuthorizationRequest();
+            assert authorizationRequest != null;
+            return doLookup(authorizationRequest);
+        }
         return doLookup((AuthorizationRequest) message);
     }
 
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/BaseOAuth2RequestDecoder.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/BaseOAuth2RequestDecoder.java
index 9cf747da..c9f256f3 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/BaseOAuth2RequestDecoder.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/BaseOAuth2RequestDecoder.java
@@ -107,7 +107,7 @@ public abstract class BaseOAuth2RequestDecoder<T extends Request> extends Abstra
      * @param message the message from which to take the endpoint URI (with IP address), if the flag is false
      * @return the endpoint URI
      */
-    @Nullable protected String getEndpointURI(final T message) {
+    @Nullable protected String getEndpointURI(final Request message) {
         if (removeIpAddressFromEndpointUri) {
             final HttpServletRequest httpServletRequest = getHttpServletRequest();
             return httpServletRequest != null ? httpServletRequest.getRequestURI() : null;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/OAuth2PushedAuthorizationRequestDecoder.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/OAuth2PushedAuthorizationRequestDecoder.java
new file mode 100644
index 00000000..1a35a5e3
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/OAuth2PushedAuthorizationRequestDecoder.java
@@ -0,0 +1,87 @@
+/*
+ * 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.oauth2.decoding.impl;
+
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.slf4j.Logger;
+
+import com.google.common.base.MoreObjects;
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+import com.nimbusds.oauth2.sdk.PushedAuthorizationRequest;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.http.JakartaServletUtils;
+
+import net.shibboleth.idp.plugin.oidc.op.decoding.impl.RequestUtil;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Message decoder decoding OAuth2 {@link PushedAuthorizationRequest}s.
+ */
+public class OAuth2PushedAuthorizationRequestDecoder extends BaseOAuth2RequestDecoder<PushedAuthorizationRequest> {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(OAuth2PushedAuthorizationRequestDecoder.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected PushedAuthorizationRequest parseMessage() throws MessageDecodingException {
+        try {
+            final HTTPRequest httpReq = JakartaServletUtils.createHTTPRequest(getHttpServletRequest());
+            getProtocolMessageLogger().trace("Inbound request {}", RequestUtil.toString(httpReq));
+            if (httpReq != null) {
+                switchIntoCustomResource(httpReq);
+                return PushedAuthorizationRequest.parse(httpReq);
+            }
+            throw new MessageDecodingException("Could not create HTTPRequest object from the incoming request");
+        } catch (final com.nimbusds.oauth2.sdk.ParseException | IOException e) {
+            log.error("Unable to decode inbound request: {}", e.getMessage());
+            throw new MessageDecodingException(e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected String getMessageToLog(@Nullable final PushedAuthorizationRequest message) {
+        if (message == null) {
+            return null;
+        }
+        final AuthorizationRequest request = message.getAuthorizationRequest();
+        assert request != null;
+        return MoreObjects.toStringHelper(this).omitNullValues()
+                .add("clientId", request.getClientID())
+                .add("codeChallenge", request.getCodeChallenge())
+                .add("codeChallengeMethod", request.getCodeChallengeMethod())
+                .add("customParameters", request.getCustomParameters())
+                .add("endpointURI", getEndpointURI(request))
+                .add("prompt", request.getPrompt())
+                .add("redirectionURI", request.getRedirectionURI())
+                .add("requestObject", request.getRequestObject() == null ?
+                        null : request.getRequestObject().serialize())
+                .add("requestURI", request.getRequestURI())
+                .add("resources", request.getResources())
+                .add("responseMode", request.getResponseMode())
+                .add("responseType", request.getResponseType())
+                .add("scope", request.getScope())
+                .add("state", request.getState())
+                .toString();
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/AbstractOAuthAuthorizationRequestAction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/AbstractOAuthAuthorizationRequestAction.java
index 2791dbfb..fcd16c79 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/AbstractOAuthAuthorizationRequestAction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/AbstractOAuthAuthorizationRequestAction.java
@@ -14,10 +14,14 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 
 import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+import com.nimbusds.oauth2.sdk.PushedAuthorizationRequest;
 
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCRequestAction;
 
@@ -27,13 +31,33 @@ import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCRequestAction;
  */
 public abstract class AbstractOAuthAuthorizationRequestAction extends AbstractOIDCRequestAction<AuthorizationRequest> {
 
+    /** The authorization request to operate on. */
+    @Nullable private AuthorizationRequest authorizationRequest;
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        final Object request = getRequest();
+        if (request instanceof AuthorizationRequest authzRequest) {
+            authorizationRequest = authzRequest;
+        } else if (request instanceof PushedAuthorizationRequest pushedAuthorizationRequest) {
+            authorizationRequest = pushedAuthorizationRequest.getAuthorizationRequest();
+        }
+        return true;
+    }
+
     /**
      * Returns OAuth authorization request.
      * 
      * @return request
      */
-    public AuthorizationRequest getAuthorizationRequest() {
-        return getRequest();
+    @Nullable public AuthorizationRequest getAuthorizationRequest() {
+        return authorizationRequest;
     }
 
 }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildPushedAuthorizationErrorResponseFromEvent.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildPushedAuthorizationErrorResponseFromEvent.java
new file mode 100644
index 00000000..ce0ae85c
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildPushedAuthorizationErrorResponseFromEvent.java
@@ -0,0 +1,37 @@
+/*
+ * 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.oauth2.profile.impl;
+
+import org.opensaml.profile.context.EventContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import com.nimbusds.oauth2.sdk.ErrorObject;
+import com.nimbusds.oauth2.sdk.PushedAuthorizationErrorResponse;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractBuildErrorResponseFromEvent;
+
+/**
+ * This action reads an event from the configured {@link EventContext} lookup strategy, constructs an OAuth2 Token
+ * PAR error response message and attaches it as the outbound message.
+ */
+public class BuildPushedAuthorizationErrorResponseFromEvent
+        extends AbstractBuildErrorResponseFromEvent<PushedAuthorizationErrorResponse> {
+
+    @Override
+    protected PushedAuthorizationErrorResponse buildErrorResponse(final ErrorObject error,
+            final ProfileRequestContext profileRequestContext) {
+        return new PushedAuthorizationErrorResponse(error);
+    }
+
+}
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
new file mode 100644
index 00000000..eda82c9a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutbounPushedAuthorizationResponseMessage.java
@@ -0,0 +1,194 @@
+/*
+ * 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.oauth2.profile.impl;
+
+import java.net.URI;
+import java.text.ParseException;
+import java.time.Duration;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.oauth2.sdk.PushedAuthorizationRequest;
+import com.nimbusds.oauth2.sdk.PushedAuthorizationSuccessResponse;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.profile.config.navigate.PushedAuthorizationRequestUriClaimsSetManipulationStrategyLookupFunction;
+import net.shibboleth.oidc.profile.config.navigate.PushedAuthorizationRequestUriLifetimeLookupFunction;
+import net.shibboleth.oidc.profile.config.navigate.PushedAuthorizationRequestUriTypeLookupFunction;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Action that forms outbound token introspection success message. Formed message is set to
+ * {@link ProfileRequestContext#getOutboundMessageContext()}.
+ */
+public class FormOutbounPushedAuthorizationResponseMessage extends AbstractOAuthAuthorizationResponseAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(FormOutbounPushedAuthorizationResponseMessage.class);
+
+    /** Strategy used to obtain the request URI type to issue. */
+    @Nonnull private Function<ProfileRequestContext,String> requestUriTypeLookupStrategy;    
+    
+    /** Strategy used to obtain the request URI lifetime. */
+    @Nonnull private Function<ProfileRequestContext,Duration> requestUriLifetimeLookupStrategy;
+
+    /** Lookup function to supply strategy bi-function for manipulating request URI claims set. */ 
+    @Nonnull
+    private Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+        requestUriClaimsSetManipulationStrategyLookupStrategy;
+
+    /** The strategies used for serializing request URI claims set, key referring to the refresh token type. */
+    @Nonnull private Map<String, BiFunction<ProfileRequestContext,Map<String,Object>,URI>>
+        requestUriClaimsSetSerializationStrategies;
+
+    /** The request URI type to use. */
+    @Nullable private String requestUriType;
+
+    /** The request URI lifetime to use. */
+    @Nullable private Duration requestUriLifetime;
+
+    /** The strategy used for manipulating the request URI claims set. */
+    @Nullable private BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> manipulationStrategy;
+
+    /** The request message to operate on. */
+    @Nullable private PushedAuthorizationRequest requestMessage;
+
+    /**
+     * Constructor.
+     */
+    public FormOutbounPushedAuthorizationResponseMessage() {
+        requestUriTypeLookupStrategy = new PushedAuthorizationRequestUriTypeLookupFunction();
+        requestUriLifetimeLookupStrategy = new PushedAuthorizationRequestUriLifetimeLookupFunction();
+        requestUriClaimsSetManipulationStrategyLookupStrategy =
+                new PushedAuthorizationRequestUriClaimsSetManipulationStrategyLookupFunction();
+        requestUriClaimsSetSerializationStrategies = CollectionSupport.emptyMap();
+    }
+
+    public void setRequestUriClaimsSetSerializationStrategies(
+            @Nonnull final Map<String, BiFunction<ProfileRequestContext,Map<String,Object>,URI>> strategies) {
+       super.checkSetterPreconditions();
+       requestUriClaimsSetSerializationStrategies = Constraint.isNotNull(strategies,
+               "Request URI serialization strategies cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        final Object request = getRequest();
+        if (!(request instanceof PushedAuthorizationRequest)) {
+            log.error("{} The request message type is unexpected {}", getLogPrefix(), request);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        requestMessage = (PushedAuthorizationRequest) request;
+
+        requestUriType = StringSupport.trimOrNull(requestUriTypeLookupStrategy.apply(profileRequestContext));
+
+        requestUriLifetime = requestUriLifetimeLookupStrategy.apply(profileRequestContext);
+        if (requestUriLifetime == null) {
+            log.warn("{} No lifetime supplied for request URI", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+
+        manipulationStrategy = requestUriClaimsSetManipulationStrategyLookupStrategy.apply(profileRequestContext);
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final Map<String, Object> claimsSet = buildClaimsSet(profileRequestContext);
+        
+        final BiFunction<ProfileRequestContext,Map<String,Object>,URI> serializationStrategy =
+                requestUriClaimsSetSerializationStrategies.get(requestUriType == null ? "" : requestUriType);
+        if (serializationStrategy == null) {
+            log.error("{} Could not find a seralization strategy for request URI type {}", getLogPrefix(),
+                    requestUriType);
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return;
+        }
+        final URI serializedUri = serializationStrategy.apply(profileRequestContext, claimsSet);
+        if (serializedUri == null) {
+            log.error("{} Could not serialize the claims set with request URI type {}", getLogPrefix(),
+                    requestUriType);
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return;
+        }
+
+        assert requestUriLifetime != null;
+        long lifetime = requestUriLifetime.getSeconds();
+
+        final PushedAuthorizationSuccessResponse response =
+                new PushedAuthorizationSuccessResponse(serializedUri, lifetime);
+        profileRequestContext.ensureOutboundMessageContext().setMessage(response);
+
+    }
+
+    @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 JWT requestObject = oidcContext.getRequestObject();
+        if (requestObject != null) {
+            try {
+                claimsSet.putAll(requestObject.getJWTClaimsSet().getClaims());
+                log.debug("{} Request object claims set successfully merged", getLogPrefix());
+            } catch (ParseException e) {
+                log.error("{} Could not parse request object claims set", getLogPrefix());
+            }
+        }
+
+        if (manipulationStrategy != null) {
+            log.debug("{} Manipulation strategy has been set, applying it to the claims set {}", getLogPrefix(),
+                    claimsSet);
+            assert manipulationStrategy != null;
+            final Map<String, Object> result = manipulationStrategy.apply(profileRequestContext,
+                    claimsSet);
+            if (result == null) {
+                log.debug("{} Manipulation strategy returned null, leaving claims set untouched.",
+                        getLogPrefix());
+            } else {
+                log.debug("{} Manipulation strategy changed the contents of the claims set", getLogPrefix());
+                return result;
+            }
+        } else {
+            log.debug("{} No manipulation strategy configured", getLogPrefix());
+        }
+        assert claimsSet != null;
+        return claimsSet;
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java
index 5262bec6..bad17758 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java
@@ -346,7 +346,11 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOAuthAuthoriz
         final OIDCAuthenticationResponseContext responseCtx = getOidcResponseContext();
         assert responseCtx != null;
         final AuthorizationRequest authorizationRequest = getAuthorizationRequest();
-        assert authorizationRequest != null;
+        if (authorizationRequest == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            log.error("{} Could not resolve AuthorizationRequest message from request", getLogPrefix());
+            return;
+        }
         
         final OIDCAuthenticationResponseConsentContext consentCtx =
                 consentContextLookupStrategy.apply(profileRequestContext);
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 0c51c6a7..5ff7d2b5 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
@@ -18,7 +18,10 @@ import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.function.BiFunction;
 import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
@@ -33,6 +36,7 @@ import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.opensaml.messaging.encoder.AbstractMessageEncoder;
 import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.security.httpclient.HttpClientSecurityParameters;
 import org.opensaml.security.httpclient.HttpClientSecuritySupport;
@@ -40,7 +44,10 @@ import org.slf4j.Logger;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.JWTParser;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
@@ -50,6 +57,7 @@ import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -79,6 +87,17 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
     /** Object mapper used for pretty-printing JWT contents. */
     @NonnullAfterInit private ObjectMapper objectMapper;
 
+    /** List of deserializers for OP-issued request_uri values. */
+    @Nonnull private List<BiFunction<ProfileRequestContext,URI,Map<String,Object>>>
+        pushedAuthorizationRequestUriDeserializers;
+
+    /**
+     * Constructor.
+     */
+    public SetRequestObjectToResponseContext() {
+        pushedAuthorizationRequestUriDeserializers = CollectionSupport.emptyList();
+    }
+
     /**
      * Set the {@link HttpClient} to use.
      * 
@@ -121,6 +140,20 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
         objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
     }
 
+    /**
+     * Set the list of deserializers for OP-issued request_uri values.
+     * 
+     * @param deserializers What to set.
+     * 
+     * @since 4.2.0
+     */
+    public void setPushedAuthorizationRequestUriDeserializers(
+            @Nonnull final List<BiFunction<ProfileRequestContext,URI,Map<String,Object>>> deserializers) {
+        checkSetterPreconditions();
+        pushedAuthorizationRequestUriDeserializers = Constraint.isNotNull(deserializers,
+                "List of request_uri deserializers cannot be null");
+    }
+
     /**
      * Build the {@link HttpClientContext} instance to be used by the HttpClient.
      * 
@@ -159,10 +192,17 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
             return false;
         }
 
-        if (!getAuthorizationRequest().specifiesRequestObject()) {
+        final AuthorizationRequest authorizationRequest = getAuthorizationRequest();
+        if (authorizationRequest == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            log.error("{} Could not resolve AuthorizationRequest message from request", getLogPrefix());
+            return false;
+        }
+
+        if (!authorizationRequest.specifiesRequestObject()) {
             if (requestObjectEnforcedPredicate.test(profileRequestContext)) {
                 log.warn("{} No request_uri or request by value, even though it's enforced for {}", getLogPrefix(),
-                        getAuthorizationRequest().getClientID().getValue());
+                        authorizationRequest.getClientID().getValue());
                 ActionSupport.buildEvent(profileRequestContext, OidcEventIds.MISSING_MANDATORY_REQUEST_OBJECT);
             } else {
                 log.debug("{} No request_uri or request by value, nothing to do", getLogPrefix());
@@ -170,8 +210,8 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
             return false;
         }
         
-        if (getAuthorizationRequest().getRequestObject() != null
-                && getAuthorizationRequest().getRequestURI() != null) {
+        if (authorizationRequest.getRequestObject() != null
+                && authorizationRequest.getRequestURI() != null) {
             log.error("{} request_uri and request object cannot be both set", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.REQUEST_OBJECT_AND_URI);
             return false;
@@ -185,7 +225,9 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
         assert oidcResponseContext != null;
-        final JWT requestObject = getAuthorizationRequest().getRequestObject();
+        final AuthorizationRequest authorizationRequest = getAuthorizationRequest();
+        assert authorizationRequest != null;
+        final JWT requestObject = authorizationRequest.getRequestObject();
         if (requestObject != null) {
             oidcResponseContext.setRequestObject(requestObject);
             log.debug("{} Request object {} by value stored to oidc response context", getLogPrefix(),
@@ -204,20 +246,37 @@ public class SetRequestObjectToResponseContext extends AbstractOAuthAuthorizatio
                 if (metadata != null) {
                     final Set<URI> allowedURIs = metadata.getRequestObjectURIs();
                     if (allowedURIs != null) {
-                        authorized = allowedURIs.contains(getAuthorizationRequest().getRequestURI());
+                        authorized = allowedURIs.contains(authorizationRequest.getRequestURI());
                     }
                 }
             }
         }
         
         if (!authorized) {
+            for (final BiFunction<ProfileRequestContext,URI,Map<String,Object>> deserializer :
+                pushedAuthorizationRequestUriDeserializers) {
+                final Map<String,Object> claimsSet = deserializer.apply(profileRequestContext,
+                        authorizationRequest.getRequestURI());
+                if (claimsSet == null) {
+                    continue;
+                }
+                try {
+                    final JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(claimsSet);
+                    oidcResponseContext.setRequestObject(new PlainJWT(jwtClaimsSet));
+                    log.debug("{} Request object by PAR reference stored to oidc response context", getLogPrefix());
+                    oidcResponseContext.setRequestObjectValidated(true);
+                    return;
+                } catch (final ParseException e) {
+                    log.error("{} Could not build JWT from the request_uri claims set", getLogPrefix(), e);
+                }
+            }
             log.error("{} Unregistered request URI blocked: {}", getLogPrefix(),
-                    getAuthorizationRequest().getRequestURI());
+                    authorizationRequest.getRequestURI());
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REQUEST_URI);
             return;
         }
         
-        final HttpGet httpRequest = new HttpGet(getAuthorizationRequest().getRequestURI());
+        final HttpGet httpRequest = new HttpGet(authorizationRequest.getRequestURI());
         final HttpClientContext httpContext = buildHttpContext(httpRequest);
         try (final ClassicHttpResponse response = httpClient.executeOpen(null, httpRequest, httpContext)) {
             final String scheme = httpRequest.getUri().getScheme();
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidatePushedAuthorizationClientIDMatch.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidatePushedAuthorizationClientIDMatch.java
new file mode 100644
index 00000000..8cabcd0a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidatePushedAuthorizationClientIDMatch.java
@@ -0,0 +1,87 @@
+/*
+ * 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.oauth2.profile.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestClientIDLookupFunction;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Validates the client ID in the incoming authorization request matches with the one used in endpoint authentication.
+ */
+public class ValidatePushedAuthorizationClientIDMatch  extends AbstractOAuthAuthorizationRequestAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidatePushedAuthorizationClientIDMatch.class);
+
+    /** Strategy used to obtain the client id value used in endpoint authentication. */
+    @Nonnull private Function<MessageContext, ClientID> clientIDLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public ValidatePushedAuthorizationClientIDMatch() {
+        clientIDLookupStrategy = new TokenRequestClientIDLookupFunction();
+    }
+
+    /**
+     * Set the strategy used to locate the client id value used in endpoint authentication.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setClientIDLookupStrategy(@Nonnull final Function<MessageContext, ClientID> strategy) {
+        checkSetterPreconditions();
+        clientIDLookupStrategy =
+                Constraint.isNotNull(strategy, "ClientIDLookupStrategy lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final ClientID authenticatedClientId =
+                clientIDLookupStrategy.apply(profileRequestContext.getInboundMessageContext());
+        if (authenticatedClientId == null) {
+            log.warn("{} No client ID that was used in client authentication could be resolved", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return;
+        }
+        final AuthorizationRequest authorizationRequest = getAuthorizationRequest();
+        if (authorizationRequest == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            log.error("{} Could not resolve AuthorizationRequest message from request", getLogPrefix());
+            return;
+        }
+        final ClientID requestClientId = authorizationRequest.getClientID();
+        if (!authenticatedClientId.equals(requestClientId)) {
+            log.warn("{} The client ID used in authentication {} did not match with one in request {}",
+                    authenticatedClientId, requestClientId, getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObject.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObject.java
index cd6a4c49..b1ecb011 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObject.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObject.java
@@ -20,6 +20,7 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
@@ -28,6 +29,7 @@ import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.PlainJWT;
 import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
 import com.nimbusds.oauth2.sdk.ResponseType;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
@@ -111,6 +113,10 @@ public class ValidateRequestObject extends AbstractOAuthAuthorizationResponseAct
             log.debug("{} No request object, nothing to do", getLogPrefix());
             return false;
         }
+        if (oidcResponseContext.isRequestObjectValidated()) {
+            log.debug("{} Request object is already validated, nothing to do", getLogPrefix());
+            return false;
+        }
         return true;
     }
     
@@ -148,15 +154,21 @@ public class ValidateRequestObject extends AbstractOAuthAuthorizationResponseAct
         try {
             assert requestObject != null;
             claimsSet = requestObject.getJWTClaimsSet();
+            final AuthorizationRequest authorizationRequest = getAuthorizationRequest();
+            if (authorizationRequest == null) {
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+                log.error("{} Could not resolve AuthorizationRequest message from request", getLogPrefix());
+                return;
+            }
             if (claimsSet.getClaims().containsKey("client_id")
-                    && !getAuthorizationRequest().getClientID()
+                    && !authorizationRequest.getClientID()
                             .equals(new ClientID((String) claimsSet.getClaim("client_id")))) {
                 log.error("{} client_id in request object not matching client_id request parameter", getLogPrefix());
                 ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REQUEST_OBJECT);
                 return;
             }
             if (claimsSet.getClaims().containsKey("response_type")
-                    && !getAuthorizationRequest().getResponseType().equals(new ResponseType(
+                    && !authorizationRequest.getResponseType().equals(new ResponseType(
                             ((String) claimsSet.getClaim("response_type")).split(" ")))) {
                 log.error("{} response_type in request object not matching response_type request parameter",
                         getLogPrefix());
@@ -182,6 +194,9 @@ public class ValidateRequestObject extends AbstractOAuthAuthorizationResponseAct
             return;
         }
 
+        final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
+        assert oidcResponseContext != null;
+        oidcResponseContext.setRequestObjectValidated(true);
     }    
     
     // Checkstyle: CyclomaticComplexity ON
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseMode.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseMode.java
index da0e06eb..4c800a63 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseMode.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseMode.java
@@ -20,9 +20,11 @@ import java.util.function.Function;
 import javax.annotation.Nonnull;
 
 import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
 import com.nimbusds.oauth2.sdk.ResponseMode;
 
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestResponseModeLookupFunction;
@@ -83,11 +85,18 @@ public class ValidateResponseMode extends AbstractOAuthAuthorizationResponseActi
             log.debug("{} No restrictions for the response mode", getLogPrefix());
             return;
         }
-        
+
+        final AuthorizationRequest authorizationRequest = getAuthorizationRequest();
+        if (authorizationRequest == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            log.error("{} Could not resolve AuthorizationRequest message from request", getLogPrefix());
+            return;
+        }
+
         final ResponseMode requestedMode = requestedResponseModeLookupStrategy.apply(profileRequestContext);
         final ResponseMode responseMode;
         if (requestedMode == null) {
-            responseMode = getAuthorizationRequest().impliedResponseMode();
+            responseMode = authorizationRequest.impliedResponseMode();
             log.debug("{} No response mode set in the request, using the default: {}", getLogPrefix(), responseMode);
         } else {
             responseMode = requestedMode;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultAuthorizationRequestTypeValidationStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultAuthorizationRequestTypeValidationStrategy.java
index 5adae36e..3cca50ad 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultAuthorizationRequestTypeValidationStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultAuthorizationRequestTypeValidationStrategy.java
@@ -23,6 +23,7 @@ import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 
 import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+import com.nimbusds.oauth2.sdk.PushedAuthorizationRequest;
 import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
 
 import net.shibboleth.shared.logic.Constraint;
@@ -69,16 +70,24 @@ public class DefaultAuthorizationRequestTypeValidationStrategy implements Predic
             return false;
         }
         final Object message = messageContext.getMessage();
-        if (message instanceof AuthorizationRequest) {
-            if (requireAuthenticationRequest.test(input)) {
-                if (message instanceof AuthenticationRequest) {
-                    return true;
-                }
-                return false;
-            }
-            return true;
+        if (message instanceof AuthorizationRequest authorizationRequest) {
+            return applyPredicate(input, authorizationRequest);
+        } else if (message instanceof PushedAuthorizationRequest pushedAuthorizationRequest) {
+            final AuthorizationRequest authorizationRequest = pushedAuthorizationRequest.getAuthorizationRequest();
+            assert authorizationRequest != null;
+            return applyPredicate(input, authorizationRequest);
         }
         return false;
     }
 
+    protected boolean applyPredicate(@Nullable final ProfileRequestContext input,
+            @Nonnull final AuthorizationRequest request) {
+        if (requireAuthenticationRequest.test(input)) {
+            if (request instanceof AuthenticationRequest) {
+                return true;
+            }
+            return false;
+        }
+        return true;
+    }
 }
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
new file mode 100644
index 00000000..f8a24629
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPushedAuthorizationRequestUriDeserializationFunction.java
@@ -0,0 +1,169 @@
+/*
+ * 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 java.net.URI;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.ReplayCache;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+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;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.security.DataSealer;
+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
+    implements BiFunction<ProfileRequestContext,URI,Map<String,Object>> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log =
+            LoggerFactory.getLogger(DefaultPushedAuthorizationRequestUriDeserializationFunction.class);
+
+    /** Data sealer for decrypting the contents of the claims set. */
+    @NonnullAfterInit private DataSealer dataSealer;
+
+    /** Object mapper used for deserializing the claims set. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /** Message replay cache instance to use. */
+    @NonnullAfterInit private ReplayCache replayCache;
+
+    /** The lifetime for the sealed object. */
+    @Nonnull private Duration lifetime;
+
+    /**
+     * Constructor.
+     */
+    public DefaultPushedAuthorizationRequestUriDeserializationFunction() {
+        final Duration fiveMins = Duration.ofMinutes(5);
+        assert fiveMins != null;
+        lifetime = fiveMins;
+    }
+    
+    /**
+     * Set the data sealer instance to use.
+     * 
+     * @param sealer What to set.
+     */
+    public void setDataSealer(@Nonnull final DataSealer sealer) {
+        checkSetterPreconditions();
+        dataSealer = Constraint.isNotNull(sealer, "Data sealer cannot be null");
+    }
+
+    /**
+     * Set the object mapper used for deserializing the claims set.
+     * 
+     * @param mapper What to set.
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /**
+     * Set the replay cache instance to use.
+     * 
+     * @param cache The replayCache to set.
+     */
+    public void setReplayCache(@Nonnull final ReplayCache cache) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        replayCache = Constraint.isNotNull(cache, "ReplayCache 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 {
+        super.doInitialize();
+
+        if (dataSealer == null) {
+            throw new ComponentInitializationException("Data sealer cannot be null");
+        }
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+        if (replayCache == null) {
+            throw new ComponentInitializationException("Replay cache cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    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:", "");
+            assert sealedValue != null;
+            try {
+                final String unsealedValue = dataSealer.unwrap(sealedValue);
+                final Map<String,Object> result = objectMapper.readValue(unsealedValue,
+                        new TypeReference<LinkedHashMap<String, Object>>() {});
+                final String jti = StringSupport.trimOrNull((String) result.get("jti"));
+                if (jti == null) {
+                    log.error("No token identifier (jti) found from the claims set");
+                    return null;
+                }
+                final String cacheContext = getClass().getName();
+                assert cacheContext != null;
+                final Instant expiration = Instant.now().plus(lifetime);
+                assert expiration != null;
+                if (!replayCache.check(cacheContext, jti, expiration)) {
+                    log.warn("Replay detected for request_uri {}", uri.toString());
+                    return null;
+                }
+                return result;
+            } catch (final DataSealerException e) {
+                log.debug("Could not decrypt the contents of the request_uri {}", uri.toString(), e);
+                return null;
+            } catch (final JsonProcessingException e) {
+                log.error("Could not deserialize the unsealed contents of the request_uri {}", uri.toString(), e);
+                return null;
+            }
+        }
+        log.error("Request URI content set is null");
+        return null;
+    }
+
+}
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
new file mode 100644
index 00000000..68d4eb62
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPushedAuthorizationRequestUriSerializationFunction.java
@@ -0,0 +1,170 @@
+/*
+ * 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 java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+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;
+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;
+
+/**
+ * Default serialization function for the request URI claims set within OAuth2 PAR.
+ */
+public class DefaultPushedAuthorizationRequestUriSerializationFunction extends AbstractInitializableComponent
+    implements BiFunction<ProfileRequestContext,Map<String,Object>,URI> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log =
+            LoggerFactory.getLogger(DefaultPushedAuthorizationRequestUriSerializationFunction.class);
+
+    /** Data sealer for encrypting the claims set. */
+    @NonnullAfterInit private DataSealer dataSealer;
+
+    /** Object mapper used for serializing the claims set. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
+    @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+
+    /** The lifetime for the sealed object. */
+    @Nonnull private Duration lifetime;
+
+    /**
+     * Constructor.
+     */
+    public DefaultPushedAuthorizationRequestUriSerializationFunction() {
+        final Duration fiveMins = Duration.ofMinutes(5);
+        assert fiveMins != null;
+        lifetime = fiveMins;
+        idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
+    }
+    
+    /**
+     * Set the data sealer instance to use.
+     * 
+     * @param sealer What to set.
+     */
+    public void setDataSealer(@Nonnull final DataSealer sealer) {
+        checkSetterPreconditions();
+        dataSealer = Constraint.isNotNull(sealer, "Data sealer cannot be null");
+    }
+
+    /**
+     * Set the object mapper used for serializing the claims set.
+     * 
+     * @param mapper What to set.
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper 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");
+    }
+
+    /**
+     * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIdentifierGeneratorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+
+        idGeneratorLookupStrategy =
+                Constraint.isNotNull(strategy, "Identifier generation strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (dataSealer == null) {
+            throw new ComponentInitializationException("The data sealer cannot be null");
+        }
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public URI apply(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nullable final Map<String, Object> claimsSet) {
+        if (claimsSet != null && !claimsSet.isEmpty()) {
+            final Instant expiration = Instant.now().plus(lifetime);
+            final IdentifierGenerationStrategy idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+            if (idGenerator == null) {
+                log.error("Could not resolve ID generator");
+                return null;
+            }
+            final Map<String, Object> input = new HashMap<String, Object>(claimsSet);
+            input.put("jti", idGenerator.generateIdentifier(false)); //TODO flag?
+            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;
+                return new URI(result);
+            } catch (final JsonProcessingException e) {
+                log.error("Could not transform the given claims set into JSON", e);
+                return null;
+            } catch (final DataSealerException e) {
+                log.error("Could not encrypt the serialized claims set", e);
+                return null;
+            } catch (final URISyntaxException e) {
+                log.error("Could not build an URI of the sealed claims set", e);
+                return null;
+            }
+        }
+        log.error("Request URI claims set is null/empty");
+        return null;
+    }
+
+}
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
new file mode 100644
index 00000000..b1b93431
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/StorageServicePushedAuthorizationRequestUriDeserializationFunction.java
@@ -0,0 +1,149 @@
+/*
+ * 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 java.io.IOException;
+import java.net.URI;
+import java.time.Duration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+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;
+
+/**
+ * Default deserialization function for decoding the request URI into claims set within OAuth2 PAR.
+ */
+public class StorageServicePushedAuthorizationRequestUriDeserializationFunction extends AbstractInitializableComponent
+    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);
+
+    /** Object mapper used for deserializing the claims set. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /** 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.
+     * 
+     * @param mapper What to set.
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /**
+     * Set the {@link StorageService} back-end to use.
+     * 
+     * @param storage the back-end to use
+     */
+    public void setStorageService(@Nonnull final StorageService storage) {
+        checkSetterPreconditions();
+        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 {
+        super.doInitialize();
+
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+        if (storageService == null) {
+            throw new ComponentInitializationException("Storage service cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    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:", "");
+            assert jti != null;
+            try {
+                final StorageRecord<?> storageRecord = storageService.read(CONTEXT_NAME, jti);
+                if (storageRecord == null) {
+                    log.debug("Could not find any records with jti {}", jti);
+                    return null;
+                }
+                if (storageService.delete(CONTEXT_NAME, jti)) {
+                    log.debug("Storage record {} successfully deleted", jti);
+                }
+                final Map<String,Object> result = objectMapper.readValue(storageRecord.getValue(),
+                        new TypeReference<LinkedHashMap<String, Object>>() {});
+                return result;
+            } catch (final JsonProcessingException e) {
+                log.error("Could not deserialize the contents of the request_uri {}", uri.toString(), e);
+                return null;
+            } catch (IOException e) {
+                log.error("Exception catched from the storage service", e);
+                return null;
+            }
+        }
+        log.error("Request URI content set is null");
+        return null;
+    }
+
+}
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
new file mode 100644
index 00000000..03f9c112
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/StorageServicePushedAuthorizationRequestUriSerializationFunction.java
@@ -0,0 +1,181 @@
+/*
+ * 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 java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+
+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;
+
+/**
+ * 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
+    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);
+
+    /** Object mapper used for serializing the claims set. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /** Storage service used for storing the claims set on server-side. */
+    @NonnullAfterInit private StorageService storageService;
+
+    /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
+    @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+
+    /** The lifetime for the sealed object. */
+    @Nonnull private Duration lifetime;
+
+    /**
+     * Constructor.
+     */
+    public StorageServicePushedAuthorizationRequestUriSerializationFunction() {
+        final Duration fiveMins = Duration.ofMinutes(5);
+        assert fiveMins != null;
+        lifetime = fiveMins;
+        idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
+    }
+    
+    /**
+     * Set the object mapper used for serializing the claims set.
+     * 
+     * @param mapper What to set.
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /**
+     * Set the {@link StorageService} back-end to use.
+     * 
+     * @param storage the back-end to use
+     */
+    public void setStorageService(@Nonnull final StorageService storage) {
+        checkSetterPreconditions();
+        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");
+    }
+
+    /**
+     * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIdentifierGeneratorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+
+        idGeneratorLookupStrategy =
+                Constraint.isNotNull(strategy, "Identifier generation strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+        if (storageService == null) {
+            throw new ComponentInitializationException("Storage service cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public URI apply(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nullable final Map<String, Object> claimsSet) {
+        if (claimsSet != null && !claimsSet.isEmpty()) {
+            final Instant expiration = Instant.now().plus(lifetime);
+            final IdentifierGenerationStrategy idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+            if (idGenerator == null) {
+                log.error("Could not resolve ID generator");
+                return null;
+            }
+            final Map<String, Object> input = new HashMap<String, Object>(claimsSet);
+            final String jti = idGenerator.generateIdentifier(false); //TODO flag?
+            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;
+                    return new URI(result);
+                }
+                log.error("Existing record with id {} already found in the storage service", jti);
+                return null;
+            } catch (final JsonProcessingException e) {
+                log.error("Could not transform the given claims set into JSON", e);
+                return null;
+            } catch (final URISyntaxException e) {
+                log.error("Could not build an URI of the claims set", e);
+                return null;
+            } catch (final IOException e) {
+                log.error("Could not store the claims set into storage service", e);
+                return null;
+            }
+        }
+        log.error("Request URI claims set is null/empty");
+        return null;
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/spring/TokenExtensionFactory.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/spring/TokenExtensionFactory.java
index 547bf81d..050290ac 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/spring/TokenExtensionFactory.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/spring/TokenExtensionFactory.java
@@ -14,7 +14,9 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.spring;
 
+import java.net.URI;
 import java.util.List;
+import java.util.Map;
 import java.util.function.BiFunction;
 
 import javax.annotation.Nonnull;
@@ -43,7 +45,11 @@ public class TokenExtensionFactory implements ApplicationContextAware {
         /** Refresh token serializer. */
         REFRESH_TOKEN_SERIALIZER,
         /** Refresh token deserializer. */
-        REFRESH_TOKEN_DESERIALIZER
+        REFRESH_TOKEN_DESERIALIZER,
+        /** Refresh token serializer. */
+        PAR_REQUEST_URI_SERIALIZER,
+        /** Refresh token deserializer. */
+        PAR_REQUEST_URI_DESERIALIZER
     }
 
     /** Class logger. */
@@ -78,6 +84,10 @@ public class TokenExtensionFactory implements ApplicationContextAware {
                 new ParameterizedTypeReference<BiFunction<ProfileRequestContext,RefreshTokenClaimsSet,String>>() {};
             case REFRESH_TOKEN_DESERIALIZER ->
                 new ParameterizedTypeReference<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>>() {};
+            case PAR_REQUEST_URI_SERIALIZER ->
+                new ParameterizedTypeReference<BiFunction<ProfileRequestContext,Map<String,Object>,URI>>() {};
+            case PAR_REQUEST_URI_DESERIALIZER ->
+                new ParameterizedTypeReference<BiFunction<ProfileRequestContext,URI,Map<String,Object>>>() {};
         };
         extensionType = ResolvableType.forType(typeReference);
         allowNonPrototype = nonPrototype;
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 0873f2bc..81e67379 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -798,6 +798,18 @@
         c:allowNonPrototype="false"
         abstract="true"/>
 
+    <bean id="shibboleth.oidc.PushedAuthorizationRequestUriSerializerFactory"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.spring.TokenExtensionFactory"
+        c:tokenExtensionType="PAR_REQUEST_URI_SERIALIZER"
+        c:allowNonPrototype="false"
+        abstract="true"/>
+
+    <bean id="shibboleth.oidc.PushedAuthorizationRequestUriDeserializerFactory"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.spring.TokenExtensionFactory"
+        c:tokenExtensionType="PAR_REQUEST_URI_DESERIALIZER"
+        c:allowNonPrototype="false"
+        abstract="true"/>
+
     <!-- TODO: OPCSP-prefixed beans temporarily defined here and used in views to calculate CSP hashes and nonces.
          Switch into shibboleth.CSP -prefixed ones once we depend on 5.1+ -->
     
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
new file mode 100644
index 00000000..71c86905
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
@@ -0,0 +1,338 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <bean id="shibboleth.oidc.profileId" class="java.lang.String"
+        c:_0="#{T(net.shibboleth.oidc.profile.oauth2.config.OAuth2PushedAuthorizationRequestConfiguration).PROFILE_ID}" />
+
+    <bean id="shibboleth.oidc.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.oauthpar:OAuth2.PAR}" />
+
+    <util:constant id="shibboleth.metrics.ProfileCounter"
+        static-field="net.shibboleth.oidc.profile.oauth2.config.impl.DefaultOAuth2PushedAuthorizationRequestConfiguration.PROFILE_COUNTER" />
+
+    <bean id="DecodeMessage" class="org.opensaml.profile.action.impl.DecodeMessage" scope="prototype">
+        <constructor-arg>
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oauth2.decoding.impl.OAuth2PushedAuthorizationRequestDecoder"
+                scope="prototype" p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+                p:removeIpAddressFromEndpointUri="%{idp.oidc.logging.removeIpAddressFromProtocolMessage:false}"/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="shibboleth.ClientIDLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestClientIDLookupFunction"
+        scope="prototype" />
+
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeOutboundResponseMessageContext"
+        scope="prototype" />
+
+    <bean id="ValidatePushedAuthorizationClientIDMatch"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidatePushedAuthorizationClientIDMatch"
+        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+        scope="prototype"/>
+
+    <bean id="ValidateClientIDAgainstPolicy"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateClientIDAgainstPolicy"
+        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+        scope="prototype"
+        p:unregisteredClientPolicyEnforcer="#{getObject('shibboleth.oidc.UnregisteredClientPolicyEnforcer') ?: getObject('shibboleth.oidc.DefaultUnregisteredClientPolicyEnforcer')}"/>
+
+    <bean id="ValidateAuthorizationRequestType"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateAuthorizationRequestType"
+        scope="prototype"
+        p:authorizationRequestTypeValidationStrategy-ref="#{'%{idp.oauth2.authorizationRequestTypeValidationStrategy:DefaultAuthorizationRequestTypeValidationStrategy}'.trim()}" />
+
+    <bean id="DefaultAuthorizationRequestTypeValidationStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultAuthorizationRequestTypeValidationStrategy">
+        <property name="requireAuthenticationRequestPredicate">
+            <bean class="net.shibboleth.oidc.profile.config.logic.RequireAuthenticationRequestPredicate" />
+        </property>
+    </bean>
+
+    <bean id="PopulateRequestObjectDecryptionParameters"
+        class="net.shibboleth.oidc.profile.impl.PopulateJWTDecryptionParameters" scope="prototype"
+        p:configurationLookupStrategy-ref="DecryptionConfigurationLookup"
+        p:decryptionParametersResolver-ref="JWTDecryptionParametersResolver" />
+
+    <bean id="DecryptionConfigurationLookup" lazy-init="true"
+        class="net.shibboleth.oidc.profile.config.navigate.JWTDecryptionConfigurationLookupFunction" />
+
+    <bean id="JWTDecryptionParametersResolver"
+        class="net.shibboleth.oidc.security.jose.impl.DefaultDecryptionParametersResolver" />
+
+    <bean id="PopulateRequestObjectSignatureValidationParameters"
+            class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureValidationParameters"
+            scope="prototype"
+            c:strategy-ref="shibboleth.MessageContextLookup.Inbound">
+        <property name="configurationLookupStrategy">
+            <bean class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureValidationConfigurationLookupFunction" />
+        </property>
+        <property name="signatureValidationParametersResolver">
+            <bean class="net.shibboleth.oidc.security.jose.impl.BasicSignatureValidationParametersResolver" />
+        </property>
+    </bean>
+
+    <bean id="SetRequestObjectToResponseContext"
+        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:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}" />
+
+    <bean id="RequestObjectEncryptedCondition" parent="shibboleth.Conditions.Expression"
+        c:expression="#input.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getRequestObject() instanceof T(com.nimbusds.jwt.EncryptedJWT)" />
+
+    <bean id="CheckClientJWTDecryptionConfiguration"
+        class="net.shibboleth.oidc.security.impl.CheckClientJWTDecryptionConfiguration" scope="prototype">
+        <property name="jwtTokenLookupStrategy">
+            <bean
+                class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                c:_0="#{ T(org.opensaml.profile.context.ProfileRequestContext) }"
+                c:outputType="#{T(com.nimbusds.jwt.JWT)}"
+                c:expression="#input.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getRequestObject()" />
+        </property>
+        <property name="clientInformationLookupStrategy">
+            <bean
+                class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                c:_0="#{ T(org.opensaml.profile.context.ProfileRequestContext) }"
+                c:expression="#input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext)).getClientInformation()" />
+        </property>
+        <property name="encryptionOptionalPredicate">
+            <bean parent="shibboleth.Conditions.NOT">
+                <constructor-arg>
+                    <bean parent="shibboleth.Conditions.AND">
+                        <constructor-arg>
+                            <bean parent="shibboleth.Conditions.Expression"
+                                c:expression="#input.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getRequestObject() != null" />
+                        </constructor-arg>
+                        <constructor-arg>
+                            <bean class="net.shibboleth.oidc.profile.config.logic.EncryptRequestObjectPredicate"/>
+                        </constructor-arg>
+                    </bean>
+                </constructor-arg>
+            </bean>
+        </property>
+        <property name="keyTransportEncryptionAlgorithmLookupStrategy">
+            <bean
+                class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                c:keyName="request_object_encryption_alg"/>
+        </property>
+        <property name="dataEncryptionAlgorithmLookupStrategy">
+            <bean
+                class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                c:keyName="request_object_encryption_enc"/>
+        </property>
+        <property name="errorEventId"
+            value="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_REQUEST_OBJECT}"/>
+    </bean>
+
+    <bean id="DecryptRequestObject" class="net.shibboleth.oidc.security.impl.DecryptJWE" scope="prototype">
+        <property name="jwtTokenLookupStrategy">
+            <bean class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                c:_0="#{ T(org.opensaml.profile.context.ProfileRequestContext) }"
+                c:outputType="#{T(com.nimbusds.jwt.EncryptedJWT)}"
+                c:expression="#input.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getRequestObject()" />
+        </property>
+        <property name="jwtUpdateStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.RequestObjectUpdateStrategy" />
+        </property>
+        <property name="errorEventId"
+            value="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_REQUEST_OBJECT}"/>
+        <property name="activationCondition">
+            <ref bean="RequestObjectEncryptedCondition" />
+        </property>
+    </bean>
+
+    <bean id="RequestObjectSignedCondition" parent="shibboleth.Conditions.Expression"
+        c:expression="#input.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getRequestObject() instanceof T(com.nimbusds.jwt.SignedJWT)" />
+
+    <bean id="UseRequestObjectPredicate" class="net.shibboleth.oidc.profile.config.logic.UseRequestObjectPredicate" />
+
+    <bean id="SignRequestObjectPredicate" class="net.shibboleth.oidc.profile.config.logic.SignRequestObjectPredicate" />
+
+    <bean id="ValidateRequestObjectSignature" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+        scope="prototype" c:executionDirection="INBOUND">
+        <constructor-arg>
+            <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+                <property name="handlers">
+                    <list>
+                        <bean class="net.shibboleth.oidc.security.impl.CheckClientJWTSignatureAlgorithmHandler"
+                            scope="prototype" p:defaultAlgorithmValue="">
+                            <property name="jwtTokenLookupStrategy">
+                                <bean
+                                    class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                                    c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
+                                    c:outputType="#{T(com.nimbusds.jwt.SignedJWT)}"
+                                    c:expression="#input.getParent().ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getRequestObject()" />
+                            </property>
+                            <property name="clientInformationLookupStrategy">
+                                <bean
+                                    class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                                    c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
+                                    c:expression="#input.ensureSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext)).getClientInformation()" />
+                            </property>
+                            <property name="signatureAlgorithmLookupStrategy">
+                                <bean
+                                    class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                                    c:keyName="request_object_signing_alg" />
+                            </property>
+                        </bean>
+                        <bean class="net.shibboleth.oidc.security.impl.JWTMessageSignatureSecurityHandler"
+                            scope="prototype">
+                            <property name="jwtTokenLookupStrategy">
+                                <bean
+                                    class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                                    c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
+                                    c:outputType="#{T(com.nimbusds.jwt.SignedJWT)}"
+                                    c:expression="#input.getParent().ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getRequestObject()" />
+                            </property>
+                            <property name="clientInformationLookupStrategy">
+                                <bean
+                                    class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                                    c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
+                                    c:expression="#input.ensureSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext)).getClientInformation()" />
+                            </property>
+                        </bean>
+                    </list>
+                </property>
+            </bean>
+        </constructor-arg>
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.OR">
+                <constructor-arg>
+                    <ref bean="RequestObjectSignedCondition" />
+                </constructor-arg>
+                <constructor-arg>
+                    <bean parent="shibboleth.Conditions.AND">
+                        <constructor-arg>
+                            <ref bean="UseRequestObjectPredicate" />
+                        </constructor-arg>
+                        <constructor-arg>
+                            <ref bean="SignRequestObjectPredicate" />
+                        </constructor-arg>
+                    </bean>
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="ValidateRequestObject" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateRequestObject"
+        scope="prototype"
+        p:plainClaimsValidator="#{getObject('shibboleth.oidc.PlainRequestObjectClaimsValidation') ?: getObject('shibboleth.oidc.DefaultPlainRequestObjectClaimsValidation')}"
+        p:signedClaimsValidator="#{getObject('shibboleth.oidc.SignedRequestObjectClaimsValidation') ?: getObject('shibboleth.oidc.DefaultSignedRequestObjectClaimsValidation')}">
+    </bean>
+
+    <bean id="shibboleth.oidc.DefaultPlainRequestObjectClaimsValidation"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        p:claimValidators-ref="PlainClaimsValidators" />
+
+    <bean id="shibboleth.oidc.DefaultSignedRequestObjectClaimsValidation"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        p:claimValidators-ref="SignedClaimsValidators" />
+
+    <bean id="ExpiryClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+    <bean id="NotBeforeClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.NotBeforeClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+    <bean id="IssuerClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+        p:claimName="iss">
+        <property name="valueToMatchLookupStrategy">
+            <bean parent="shibboleth.BiFunctions.Expression"
+                c:expression="#custom.apply(#input1.getInboundMessageContext()) == null ? null : #custom.apply(#input1.getInboundMessageContext()).toString()"
+                p:customObject-ref="shibboleth.ClientIDLookupStrategy" />
+        </property>
+    </bean>
+
+    <bean id="AudienceClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator">
+         <property name="audienceLookupStrategy">
+            <bean parent="shibboleth.BiFunctions.Expression"
+                c:expression="#custom.apply(#input1)"
+                p:customObject-ref="shibboleth.ResponderIdLookup.Simple" />
+        </property>
+    </bean>
+
+    <util:list id="PlainClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="ExpiryClaimsValidator" />
+        <ref bean="NotBeforeClaimsValidator" />
+    </util:list>
+
+    <util:list id="SignedClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="ExpiryClaimsValidator" />
+        <ref bean="NotBeforeClaimsValidator" />
+        <ref bean="IssuerClaimsValidator" />
+        <ref bean="AudienceClaimsValidator" />
+    </util:list>
+
+    <bean id="ValidateRedirectURI" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateRedirectURI"
+        scope="prototype"
+        p:requireRequestedValue="true"
+        p:unregisteredClientPolicyEnforcer="#{getObject('shibboleth.oidc.UnregisteredClientPolicyEnforcer') ?: getObject('shibboleth.oidc.DefaultUnregisteredClientPolicyEnforcer')}"/>
+
+    <bean id="ValidateResponseType" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateResponseType"
+        scope="prototype"
+        p:unregisteredClientPolicyEnforcer="#{getObject('shibboleth.oidc.UnregisteredClientPolicyEnforcer') ?: getObject('shibboleth.oidc.DefaultUnregisteredClientPolicyEnforcer')}"/>
+
+    <bean id="ValidateResponseMode" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateResponseMode"
+        scope="prototype" />
+
+    <bean id="ValidateCodeChallenge" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateCodeChallenge"
+        scope="prototype" />
+
+    <bean id="FormOutboundMessage"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.FormOutbounPushedAuthorizationResponseMessage"
+        scope="prototype"
+        p:requestUriClaimsSetSerializationStrategies-ref="#{'%{idp.oauth2.par.serializationStrategies:shibboleth.oidc.DefaultPushedAuthorizationRequestUriSerializationStrategies}'.trim()}" />
+
+    <util:map id="shibboleth.oidc.DefaultPushedAuthorizationRequestUriSerializationStrategies">
+        <entry key="">
+            <bean factory-bean="DefaultStatelessRequestUriSerializerFactory" factory-method="getBean" />
+        </entry>
+        <entry key="SS">
+            <bean factory-bean="DefaultStorageServiceRequestUriSerializerFactory" factory-method="getBean" />
+        </entry>
+    </util:map>
+
+    <bean id="DefaultStatelessRequestUriSerializerFactory"
+        parent="shibboleth.oidc.PushedAuthorizationRequestUriSerializerFactory"
+        c:id="DefaultStatelessRequestUriSerializerFunction"/>
+
+    <bean id="DefaultStorageServiceRequestUriSerializerFactory"
+        parent="shibboleth.oidc.PushedAuthorizationRequestUriSerializerFactory"
+        c:id="DefaultStorageServiceRequestUriSerializerFunction"/>
+
+    <bean id="DefaultStatelessRequestUriSerializerFunction"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultPushedAuthorizationRequestUriSerializationFunction"
+        scope="prototype"
+        p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+        p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
+        p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy"/>
+
+    <bean id="DefaultStorageServiceRequestUriSerializerFunction"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.logic.StorageServicePushedAuthorizationRequestUriSerializationFunction"
+        scope="prototype"
+        p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
+        p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy"
+        p:storageService-ref="#{'%{idp.oauth2.par.StorageService:shibboleth.StorageService}'.trim()}" />
+
+    <bean id="BuildErrorResponseFromEvent"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildPushedAuthorizationErrorResponseFromEvent"
+        scope="prototype"
+        p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier"
+        p:mappedErrors="#{getObject('shibboleth.oauth2.par.MappedErrors') ?: getObject('shibboleth.oidc.DefaultApiMappedErrors')}">
+        <property name="eventContextLookupStrategy">
+            <bean class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
+        </property>
+    </bean>
+
+</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-flow.xml
new file mode 100644
index 00000000..907cf8be
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-flow.xml
@@ -0,0 +1,60 @@
+<flow xmlns="http://www.springframework.org/schema/webflow"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="oidc/abstract-api, oidc/metadata-lookup">
+
+    <action-state id="InitializeMandatoryContexts">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="PopulateMetricContext" />
+        <evaluate expression="FlowStartPopulateAuditContext" />
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="DecodeMessage" />
+    </action-state>
+
+    <action-state id="DecodeMessage">
+        <evaluate expression="DecodeMessage" />
+        <evaluate expression="PostDecodePopulateAuditContext" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- DoMetadataLookup is expected to proceed to SelectConfiguration -->
+        <transition on="proceed" to="DoMetadataLookup" />
+    </action-state>
+
+    <action-state id="SelectConfiguration">
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+        <evaluate expression="SelectProfileConfiguration" />
+        <evaluate expression="PostLookupPopulateAuditContext" />
+        <evaluate expression="PopulateInboundInterceptContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckInboundInterceptContext" />
+    </action-state>
+    
+    <!-- Authentication subflow happens here. -->
+
+    <action-state id="ResumeAfterAuthentication">
+        <evaluate expression="ValidatePushedAuthorizationClientIDMatch" />
+        <evaluate expression="ValidateClientIDAgainstPolicy" />
+        <evaluate expression="ValidateAuthorizationRequestType" />
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="SetRequestObjectToResponseContext" />
+        <evaluate expression="PopulateRequestObjectDecryptionParameters" />
+        <evaluate expression="PopulateRequestObjectSignatureValidationParameters" />
+        <evaluate expression="CheckClientJWTDecryptionConfiguration" />
+        <evaluate expression="DecryptRequestObject" />
+        <evaluate expression="ValidateRequestObjectSignature" />
+        <evaluate expression="ValidateRequestObject" />
+        <evaluate expression="ValidateRedirectURI" />
+        <evaluate expression="ValidateResponseType" />
+        <evaluate expression="ValidateResponseMode" />
+        <evaluate expression="ValidateCodeChallenge" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="PopulateOutboundInterceptContext" />
+    </action-state>
+    
+    <bean-import resource="pushed-authorization-beans.xml" />
+
+</flow>
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 613076fe..5796da38 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
@@ -122,7 +122,34 @@
         p:httpClient="#{getObject('shibboleth.oidc.NonBrowser.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
         p:httpClientSecurityParameters="#{getObject('shibboleth.oidc.NonBrowser.HttpClientSecurityParameters')}"
         p:requestObjectEnforcedPredicate-ref="UseRequestObjectPredicate"
-        p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}" />
+        p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}"
+        p:pushedAuthorizationRequestUriDeserializers-ref="#{'%{idp.oauth2.par.deserializationStrategies:shibboleth.oidc.DefaultPushedAuthorizationRequestUriDeserializers}'.trim()}" />
+
+    <util:list id="shibboleth.oidc.DefaultPushedAuthorizationRequestUriDeserializers">
+        <bean factory-bean="DefaultStatelessPushedAuthorizationRequestUriDeserializerFactory" factory-method="getBean" />
+        <bean factory-bean="DefaultStorageServicePushedAuthorizationRequestUriDeserializerFactory" factory-method="getBean" />
+    </util:list>
+
+    <bean id="DefaultStatelessPushedAuthorizationRequestUriDeserializerFactory"
+        parent="shibboleth.oidc.PushedAuthorizationRequestUriDeserializerFactory"
+        c:id="DefaultStatelessPushedAuthorizationRequestUriDeserializationFunction"/>
+
+    <bean id="DefaultStatelessPushedAuthorizationRequestUriDeserializationFunction" 
+        class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultPushedAuthorizationRequestUriDeserializationFunction"
+        scope="prototype"
+        p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+        p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
+        p:replayCache-ref="shibboleth.ReplayCache" />
+
+    <bean id="DefaultStorageServicePushedAuthorizationRequestUriDeserializerFactory"
+        parent="shibboleth.oidc.PushedAuthorizationRequestUriDeserializerFactory"
+        c:id="DefaultStorageServicePushedAuthorizationRequestUriDeserializationFunction"/>
+
+    <bean id="DefaultStorageServicePushedAuthorizationRequestUriDeserializationFunction" 
+        class="net.shibboleth.idp.plugin.oidc.op.profile.logic.StorageServicePushedAuthorizationRequestUriDeserializationFunction"
+        scope="prototype"
+        p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
+        p:storageService-ref="#{'%{idp.oauth2.par.StorageService:shibboleth.StorageService}'.trim()}" />
 
     <bean id="RequestObjectEncryptedCondition" parent="shibboleth.Conditions.Expression"
         c:expression="#input.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getRequestObject() instanceof T(com.nimbusds.jwt.EncryptedJWT)" />
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 b6a3317e..f05dffcc 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,6 +83,13 @@
           p:requireIdTokenHint="%{idp.oidc.logout.requireIdTokenHint:true}"
           p:encryptionOptional="%{idp.oidc.logout.encryptionOptional:true}"/>
 
+    <bean id="OAUTH2.PAR" parent="AbstractOIDCProfile" 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:claimsValidator-ref="DefaultJWTClaimsValidator"
+        p:unregisteredClientPolicy="#{getObject('shibboleth.oidc.DefaultUnregisteredClientPolicy')}" />
+
     <bean id="DefaultLogoutHintMatchingPredicate"
           class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultLogoutHintMatchingPredicate"/>
 
@@ -633,6 +640,29 @@
         </property>
     </bean>
 
+    <bean id="OAUTH2.PAR.MDDriven" parent="AbstractMDDrivenOAuthClientAuthenticatableProfile" lazy-init="true"
+            class="net.shibboleth.oidc.profile.oauth2.config.impl.DefaultOAuth2PushedAuthorizationRequestConfiguration">
+        <property name="issuerLookupStrategy">
+            <bean parent="shibboleth.MDDrivenStringProperty" p:propertyName="issuer"
+                  p:defaultValue-ref="shibboleth.oidc.issuer"/>
+        </property>
+        <property name="tokenEndpointAuthMethodsLookupStrategy">
+            <bean parent="shibboleth.MDDrivenSetProperty" p:propertyName="tokenEndpointAuthMethods">
+                <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}" />
+                    </bean>
+                </property>
+            </bean>
+        </property>
+        <property name="unregisteredClientPolicyLookupStrategy">
+            <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="unregisteredClientPolicy"
+                p:propertyType="#{T(java.util.function.Function)}"
+                p:defaultValue-ref="shibboleth.oidc.DefaultUnregisteredClientPolicy" />
+        </property>
+    </bean>
+
     <!-- Default client-auth JWT validation wiring. -->
 
     <bean id="AdaptedRelyingPartyIdLookup" class="net.shibboleth.shared.logic.BiFunctionSupport"
@@ -671,7 +701,7 @@
 
     <bean id="DefaultAuthenticationAudienceClaimsValidator"
         class="net.shibboleth.oidc.security.jwt.claims.impl.AuthenticationAudienceClaimsValidator"
-        p:endpointTargets="%{idp.oauth2.jwtAuth.audienceValidator.endpointTargets:/profile/oauth2/introspection,/profile/oauth2/revocation}"
+        p:endpointTargets="%{idp.oauth2.jwtAuth.audienceValidator.endpointTargets:/profile/oauth2/introspection,/profile/oauth2/revocation,/profile/oauth2/pushed-authorization}"
         p:endpointReplacement="/profile/oidc/token">
         <property name="audienceLookupStrategy">
             <bean parent="shibboleth.BiFunctions.Expression"
diff --git a/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties b/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
index 4657b3ce..7a4e15b1 100644
--- a/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
+++ b/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
@@ -200,8 +200,8 @@ idp.oidc.subject.salt = this_too_should_be_ch4ng3d
 
 # Bean used to validate audience claim in the JWT authentication.
 #idp.oauth2.jwtAuth.audienceValidator = DefaultAuthenticationAudienceClaimsValidator
-# The default pattern also accepts token endpoint URL as the audience in introspection and revocation endpoints.
-#idp.oauth2.jwtAuth.audienceValidator.endpointTargets = /profile/oauth2/introspection,/profile/oauth2/revocation
+# The default pattern also accepts token endpoint URL as the audience in introspection, revocation and PAR endpoints.
+#idp.oauth2.jwtAuth.audienceValidator.endpointTargets = /profile/oauth2/introspection,/profile/oauth2/revocation,/profile/oauth2/pushed-authorization
 
 # Bean to determine whether refresh token is issuance is activated
 #idp.oauth2.refreshToken.activation = DefaultRefreshTokenActivationCondition
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/static/openid-configuration.json b/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/static/openid-configuration.json
index 2c242a23..7276dbe2 100644
--- a/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/static/openid-configuration.json
+++ b/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/static/openid-configuration.json
@@ -8,6 +8,7 @@
    "revocation_endpoint":"https://{{ service_name }}/idp/profile/oauth2/revocation",
    "jwks_uri":"https://{{ service_name }}/idp/profile/oidc/keyset",
    "end_session_endpoint":"https://{{ service_name }}/idp/profile/oidc/end-session",
+   "pushed_authorization_request_endpoint":"https://{{ service_name }}/idp/profile/oauth2/pushed-authorization",
    "response_types_supported":[
       "code",
       "id_token",
@@ -140,5 +141,6 @@
    "backchannel_logout_supported":true,
    "backchannel_logout_session_supported":true,
    "frontchannel_logout_supported":true,
-   "frontchannel_logout_session_supported":true
+   "frontchannel_logout_session_supported":true,
+   "require_pushed_authorization_requests":false
 }
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
new file mode 100644
index 00000000..9585535a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
@@ -0,0 +1,242 @@
+/*
+ * 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.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.opensaml.storage.RevocationCache;
+import org.opensaml.storage.StorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+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.Test;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWT;
+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 net.shibboleth.oidc.security.credential.JWKCredential;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.security.DataSealerException;
+
+// Checkstyle: ThrowsCount OFF
+
+/**
+ * Unit tests for the OAuth2 pushed authorization request flow.
+ */
+public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlowTest {
+
+    public static final String FLOW_ID = "oauth2/pushed-authorization";
+
+    private Scope scope = Scope.parse("openid profile email");
+    
+    @Autowired
+    @Qualifier("testbed.DefaultRSSigningCredential")
+    private JWKCredential signingKey = null;
+    
+    @Autowired
+    @Qualifier("shibboleth.StorageService")
+    private StorageService storageService;
+
+    @Autowired
+    @Qualifier("shibboleth.oidc.RevocationCache")
+    private RevocationCache revocationCache;
+
+    public PushedAuthorizeFlowTest() {
+        super(FLOW_ID);
+    }
+
+    @AfterMethod
+    public void tearDown() throws IOException {
+        removeMetadata(storageService, clientId);
+        removeMetadata(storageService, clientIdNotMDDriven);
+    }
+
+    @Test
+    public void testUnmatchedClient() throws NoSuchAlgorithmException, URISyntaxException, DataSealerException,
+            ComponentInitializationException {
+        setBasicAuth("policyAcceptedClient1", clientSecret);
+
+        final Map<String, String> requestParams = createRequestParameters("notFoundClient");
+
+        setHttpFormRequest("POST", requestParams);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+    }
+    
+    @Test
+    public void testFailedAuthentication() throws IOException, NoSuchAlgorithmException, URISyntaxException,
+            DataSealerException, ComponentInitializationException {
+        storeMetadata(storageService, clientId, clientSecret, scope);
+        setBasicAuth(clientId, clientSecret + "X");
+        final Map<String, String> requestParams = createRequestParameters(clientId);
+
+        setHttpFormRequest("POST", requestParams);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_CLIENT_CODE);
+    }
+
+    @Test
+    public void testInvalidMessage() throws IOException, NoSuchAlgorithmException, URISyntaxException,
+            DataSealerException, ComponentInitializationException {
+        storeMetadata(storageService, clientId, clientSecret, scope);
+        setBasicAuth(clientId, clientSecret);
+        final Map<String, String> requestParams = new HashMap<>();
+        requestParams.put("client_id", clientId); //incomplete set for authorization request
+
+        setHttpFormRequest("POST", requestParams);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+    }
+    
+    @Test
+    public void testFailureUnverified_nonCompliantPolicy() throws IOException, NoSuchAlgorithmException,
+            URISyntaxException, DataSealerException, ComponentInitializationException {
+        setBasicAuth(clientId, clientSecret);
+        setHttpFormRequest("POST", createRequestParameters(clientId));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.ACCESS_DENIED_CODE);
+    }
+
+    @Test
+    public void testSuccessUnverified_compliantPolicy() throws IOException, NoSuchAlgorithmException,
+            URISyntaxException, DataSealerException, ComponentInitializationException {
+        final String clientId = "policyAcceptedClient1";
+        setBasicAuth(clientId, clientSecret);
+        setHttpFormRequest("POST", createRequestParameters(clientId));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertSuccessResponse(result, clientId);
+        final PushedAuthorizationSuccessResponse response =
+                parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+        verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString());
+    }
+
+    @Test
+    public void testSuccess() throws IOException, NoSuchAlgorithmException, URISyntaxException, DataSealerException,
+            ComponentInitializationException {
+        for (final String clientId : clientIds) {
+            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);
+            assertSuccessResponse(result, clientId);
+            final PushedAuthorizationSuccessResponse response =
+                    parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+            verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString());
+        }
+    }
+
+    @Test
+    public void testSuccessWithPostAuth() throws IOException, NoSuchAlgorithmException, URISyntaxException,
+            DataSealerException, ComponentInitializationException {
+        for (final String clientId : clientIds) {
+            storeMetadata(storageService, clientId, clientSecret, scope, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_POST, "https://example.org/cb");
+            final Map<String, String> requestParams = createRequestParameters(clientId);
+            requestParams.put("client_secret", clientSecret);
+            setHttpFormRequest("POST", requestParams);
+            final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+            assertSuccessResponse(result, clientId);
+            final PushedAuthorizationSuccessResponse response =
+                    parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+            verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString());
+        }
+    }
+
+    @Test
+    public void testSuccessWithSamlMetadata() throws NoSuchAlgorithmException, URISyntaxException, DataSealerException,
+            ComponentInitializationException {
+        setBasicAuth(clientIdSaml, clientSecretSaml);
+        setHttpFormRequest("POST", createRequestParameters(clientIdSaml));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertSuccessResponse(result, clientIdSaml);
+        final PushedAuthorizationSuccessResponse response =
+                parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+        verifyAuthorizeEndpoint(clientIdSaml, response.getRequestURI().toString());
+    }
+
+    protected Pair<String, String> getErrorDetaisForJWTValidation() {
+        return new Pair<>("invalid_client", "Client authentication failed");
+    }
+
+    protected FlowExecutionResult launchWithJwtAuthentication(final JWT jwt, final JWSAlgorithm algorithm,
+            final ClientAuthenticationMethod method, final PublicKey publicKey) throws Exception {
+        // use 'iss' claim from JWT as clientId if set, 'sub' otherwise
+        final String iss =  jwt.getJWTClaimsSet().getStringClaim("iss");
+        final String clientId = iss == null ? jwt.getJWTClaimsSet().getStringClaim("sub") : iss;
+        if (publicKey == null) {
+            storeMetadata(storageService, clientId, clientSecret, scope, algorithm, method, "https://example.org/cb");
+        } else {
+            storeMetadata(storageService, clientId, null, scope, algorithm, method, null, publicKey, "https://example.org/cb");
+        }
+        final Map<String, String> requestParameters = createRequestParameters(clientId);
+        populateClientAssertionParams(requestParameters, jwt);
+        setHttpFormRequest("POST", requestParameters);
+        return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+    }
+
+    protected void verifyAuthorizeEndpoint(final String clientId, final String requestUri) {
+        setBasicAuth("jdoe", "changeit");
+        request.setMethod("GET");
+        request.removeAllParameters();
+        final String redirectUri = "https://example.org/cb";
+
+        AuthorizeFlowTest.setRequestParameters(request, List.of(new Pair<>("client_id", clientId),
+                new Pair<>("response_type", "code"),
+                new Pair<>("redirect_uri", redirectUri),
+                new Pair<>("request_uri", requestUri)));
+
+        initializeThreadLocals();
+
+        final FlowExecutionResult result = flowExecutor.launchExecution("oidc/authorize", null, externalContext);
+        Assert.assertEquals(result.getOutcome().getId(), END_STATE_ID);
+
+        final FlowExecutionResult replayResult = flowExecutor.launchExecution("oidc/authorize", null, externalContext);
+        Assert.assertEquals(replayResult.getOutcome().getId(), "ErrorView");
+
+        request.removeAllParameters();
+        request.removeHeader("Authorization");
+
+    }
+    protected Map<String,String> createRequestParameters(final String id) {
+        final Map<String,String> result = new HashMap<>();
+        result.put("client_id", id);
+        result.put("response_type", "code");
+        result.put("scope", "openid profile");
+        result.put("redirect_uri", "https://example.org/cb");
+        return result;
+    }
+
+    protected void assertSuccessResponse(final FlowExecutionResult result, final String id) {
+        final PushedAuthorizationSuccessResponse resp =
+                parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+        Assert.assertNotNull(resp);
+        Assert.assertNotNull(resp.getRequestURI());
+        Assert.assertNotNull(resp.getLifetime());
+    }
+    
+}
\ No newline at end of file
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 b3a7bc34..2c8507ef 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
@@ -45,6 +45,7 @@
                 <bean parent="OIDC.UserInfo" />
                 <bean parent="OAUTH2.Introspection" />
                 <bean parent="OAUTH2.Revocation" />
+                <bean parent="OAUTH2.PAR" />
             </list>
         </property>
     </bean>
@@ -69,6 +70,7 @@
                 <ref bean="OAUTH2.Token.MDDriven" />
                 <ref bean="OAUTH2.Introspection.MDDriven" />
                 <ref bean="OAUTH2.Revocation.MDDriven" />
+                <bean parent="OAUTH2.PAR.MDDriven" />
             </list>
         </property>
     </bean>
@@ -103,6 +105,7 @@
                      <ref bean="OIDC.UserInfo" />
                      <ref bean="OAUTH2.Introspection" />
                      <ref bean="OAUTH2.Revocation" />
+                     <bean parent="OAUTH2.PAR" />
                  </list>
             </property>
         </bean>

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


More information about the commits mailing list