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