[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