[java-idp-oidc] 04/04: JOIDC-298 - Response mode query should not be allowed for implicit and hybrid flows
Codeberg
noreply at shibboleth.net
Thu Nov 5 16:58:39 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch maint-4.3
in repository java-idp-oidc.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-oidc/commit/c3d2258fb1a05d84db63dec475722597b484db06
commit c3d2258fb1a05d84db63dec475722597b484db06
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Nov 5 15:57:23 2026 +0200
JOIDC-298 - Response mode query should not be allowed for implicit and hybrid flows
https://shibboleth.atlassian.net/browse/JOIDC-298
- Refactored ValidateResponseMode to prevent 'query' mode for implicit and hybrid flows
- Response type is fetched via lookup strategy that is able to fetch it also via request object
- Cover in the flow tests for both authorize and PAR
---
.../oauth2/profile/impl/ValidateResponseMode.java | 48 ++++++--
.../pushed-authorization-beans.xml | 3 +-
.../idp/flows/oidc/authorize/authorize-beans.xml | 3 +-
.../oidc/op/profile/flow/AuthorizeFlowTest.java | 36 ++++++
.../op/profile/flow/PushedAuthorizeFlowTest.java | 127 +++++++++++++++++++++
5 files changed, 208 insertions(+), 9 deletions(-)
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 5f235e6e..66dae514 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
@@ -26,6 +26,7 @@ import org.slf4j.Logger;
import com.nimbusds.oauth2.sdk.AuthorizationRequest;
import com.nimbusds.oauth2.sdk.ResponseMode;
+import com.nimbusds.oauth2.sdk.ResponseType;
import net.shibboleth.oidc.profile.config.navigate.ResponseModesLookupFunction;
import net.shibboleth.oidc.profile.core.OidcEventIds;
@@ -48,6 +49,9 @@ public class ValidateResponseMode extends AbstractOAuthAuthorizationResponseActi
/** Lookup strategy for fetching the valid response modes. */
@Nonnull private Function<ProfileRequestContext, Set<String>> validResponseModesLookupStrategy;
+ /** Lookup strategy for fetching the requested response type. */
+ @NonnullAfterInit private Function<ProfileRequestContext, ResponseType> requestedResponseTypeLookupStrategy;
+
/**
* Constructor.
*/
@@ -77,6 +81,17 @@ public class ValidateResponseMode extends AbstractOAuthAuthorizationResponseActi
validResponseModesLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
}
+ /**
+ * Set the lookup strategy for fetching the requested response type.
+ *
+ * @param strategy What to set.
+ */
+ public void setRequestedResponseTypeLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, ResponseType> strategy) {
+ checkSetterPreconditions();
+ requestedResponseTypeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -85,17 +100,14 @@ public class ValidateResponseMode extends AbstractOAuthAuthorizationResponseActi
if (requestedResponseModeLookupStrategy == null) {
throw new ComponentInitializationException("Requested response mode lookup strategy cannot be null");
}
+ if (requestedResponseTypeLookupStrategy == null) {
+ throw new ComponentInitializationException("Requested response type lookup strategy cannot be null");
+ }
}
/** {@inheritDoc} */
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- final Set<String> validResponseModes = validResponseModesLookupStrategy.apply(profileRequestContext);
- if (validResponseModes == null || validResponseModes.isEmpty()) {
- log.debug("{} No restrictions for the response mode", getLogPrefix());
- return;
- }
-
final AuthorizationRequest authorizationRequest = getAuthorizationRequest();
if (authorizationRequest == null) {
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
@@ -103,10 +115,31 @@ public class ValidateResponseMode extends AbstractOAuthAuthorizationResponseActi
return;
}
+ //TODO: this could be stored in the context by the ValidateResponseType action
+ final ResponseType responseType = requestedResponseTypeLookupStrategy.apply(profileRequestContext);
+ if (responseType == null || responseType.isEmpty()) {
+ log.error("{} Could not resolve requested response type", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_RESPONSE_TYPE);
+ return;
+ }
final ResponseMode requestedMode = requestedResponseModeLookupStrategy.apply(profileRequestContext);
+ if (ResponseMode.QUERY.equals(requestedMode)) {
+ if (responseType.impliesImplicitFlow() || responseType.impliesHybridFlow()) {
+ log.warn("{} Response mode query is not allowed for response type {}", getLogPrefix(), responseType);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_RESPONSE_MODE);
+ return;
+ }
+ }
+
+ final Set<String> validResponseModes = validResponseModesLookupStrategy.apply(profileRequestContext);
+ if (validResponseModes == null || validResponseModes.isEmpty()) {
+ log.debug("{} No restrictions for the response mode", getLogPrefix());
+ return;
+ }
+
final ResponseMode responseMode;
if (requestedMode == null) {
- responseMode = authorizationRequest.impliedResponseMode();
+ responseMode = ResponseMode.resolve(null, responseType);
log.debug("{} No response mode set in the request, using the default: {}", getLogPrefix(), responseMode);
} else {
responseMode = requestedMode;
@@ -115,6 +148,7 @@ public class ValidateResponseMode extends AbstractOAuthAuthorizationResponseActi
if (!validResponseModes.stream().map(string -> new ResponseMode(string)).toList().contains(responseMode)) {
log.warn("{} Response mode {} is not allowed", getLogPrefix(), responseMode);
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_RESPONSE_MODE);
+ return;
}
}
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 91721d52..c98cda2f 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
@@ -294,7 +294,8 @@
</bean>
<bean id="ValidateResponseMode" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateResponseMode"
- scope="prototype">
+ scope="prototype"
+ p:requestedResponseTypeLookupStrategy-ref="RequestResponseTypeLookupFunction">
<property name="requestedResponseModeLookupStrategy">
<bean class="net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.DefaultRequestResponseModeLookupFunction"
p:useOnlyRequestObjectPredicate-ref="#{'%{idp.oauth2.par.useOnlyRequestObject:DefaultParUseOnlyRequestObjectCondition}'.trim()}"/>
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 2fa145e9..dc2d408d 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
@@ -443,7 +443,8 @@
<bean id="ValidateResponseMode" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateResponseMode"
scope="prototype"
- p:requestedResponseModeLookupStrategy-ref="DefaultRequestResponseModeLookupFunction"/>
+ p:requestedResponseModeLookupStrategy-ref="DefaultRequestResponseModeLookupFunction"
+ p:requestedResponseTypeLookupStrategy-ref="RequestResponseTypeLookupFunction" />
<bean id="DefaultRequestCodeChallengeLookupFunction"
class="net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.DefaultRequestCodeChallengeLookupFunction"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
index c00b0ba0..ff2d8e97 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
@@ -814,6 +814,24 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
assertErrorDescriptionContains(result, "InvalidResponseMode");
}
+ @Test
+ public void testWithImplicitFlow_queryResponseModeNotAllowed() throws IOException, SessionException {
+ request.setMethod("GET");
+ setRequestParameters(List.of(new Pair<>("client_id", clientIdQueryResponseMode),
+ new Pair<>("response_type", "id_token"),
+ new Pair<>("scope", "openid profile"),
+ new Pair<>("response_mode", "query"),
+ new Pair<>("redirect_uri", redirectUri),
+ new Pair<>("nonce", "idhas3h23hi13h1o2i32")));
+ storeMetadata(storageService, clientIdQueryResponseMode, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_request");
+ assertErrorDescriptionContains(result, "InvalidResponseMode");
+ }
+
@Test
public void testWithImplicitFlow_formPostResponseModeNotAllowed() throws IOException, SessionException {
request.setMethod("GET");
@@ -1336,6 +1354,24 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
Assert.assertNull(successResponse.getIssuer());
}
+ @Test
+ public void testWithHybridTokenFlow_queryResponseModeNotAllowed() throws IOException, SessionException {
+ request.setMethod("GET");
+ setRequestParameters(List.of(new Pair<>("client_id", clientId),
+ new Pair<>("response_type", "code token"),
+ new Pair<>("scope", "openid profile"),
+ new Pair<>("redirect_uri", redirectUri),
+ new Pair<>("response_mode", "query"),
+ new Pair<>("nonce", "idhas3h23hi13h1o2i32")));
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_request");
+ assertErrorDescriptionContains(result, "InvalidResponseMode");
+ }
+
@Test
public void testWithHybridTokenFlowRequestObjectEnforcedNoRO() throws IOException, SessionException {
request.setMethod("GET");
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 8034efaa..84bca9e2 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
@@ -199,6 +199,133 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
}
}
+ @Test
+ public void testQueryResponseModeWithCodeSuccess() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException {
+ for (final String clientId : clientIds) {
+ storeMetadata(storageService, clientId, clientSecret, scope, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ final Map<String, String> requestParams = createRequestParameters(clientId);
+ requestParams.put("response_type", "code");
+ requestParams.put("response_mode", "query");
+ 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 testQueryResponseModeWithCodeSuccessRO() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException, JOSEException {
+ for (final String clientId : clientIds) {
+ storeMetadata(storageService, clientId, clientSecret, scope, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ final String nonce = idGenerator.generateIdentifier();
+ final JWTClaimsSet ro = new JWTClaimsSet.Builder()
+ .claim("iss", clientId)
+ .claim("client_id", clientId)
+ .claim("aud", issuer)
+ .claim("response_type", "code")
+ .claim("response_mode", "query")
+ .claim("redirect_uri", "https://example.org/cb")
+ .claim("scope", "openid profile")
+ .claim("nonce", nonce)
+ .build();
+ final SignedJWT requestObject = createSecretJWT(ro, clientSecret);
+ setHttpFormRequest("POST", createRequestParameters(clientId, null, null,
+ requestObject.serialize()));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertSuccessResponse(result, clientId);
+ final PushedAuthorizationSuccessResponse response =
+ parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+ verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString(), null, nonce);
+ }
+ }
+
+ @Test
+ public void testQueryResponseModePreventedForHybrid() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException {
+ for (final String clientId : clientIds) {
+ storeMetadata(storageService, clientId, clientSecret, scope, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ final Map<String, String> requestParams = createRequestParameters(clientId);
+ requestParams.put("response_type", "code token");
+ requestParams.put("response_mode", "query");
+ setHttpFormRequest("POST", requestParams);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ assertErrorDescriptionContains(result, "InvalidResponseMode");
+ }
+ }
+
+ @Test
+ public void testQueryResponseModePreventedForImplicit() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException {
+ for (final String clientId : clientIds) {
+ storeMetadata(storageService, clientId, clientSecret, scope, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ final Map<String, String> requestParams = createRequestParameters(clientId);
+ requestParams.put("response_type", "id_token");
+ requestParams.put("response_mode", "query");
+ requestParams.put("nonce", idGenerator.generateIdentifier());
+ setHttpFormRequest("POST", requestParams);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ assertErrorDescriptionContains(result, "InvalidResponseMode");
+ }
+ }
+
+ @Test
+ public void testQueryResponseModePreventedForHybridRO() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException, JOSEException {
+ for (final String clientId : clientIds) {
+ storeMetadata(storageService, clientId, clientSecret, scope, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ final JWTClaimsSet ro = new JWTClaimsSet.Builder()
+ .claim("iss", clientId)
+ .claim("client_id", clientId)
+ .claim("aud", issuer)
+ .claim("response_type", "code token")
+ .claim("response_mode", "query")
+ .claim("redirect_uri", "https://example.org/cb")
+ .claim("scope", "openid profile")
+ .claim("nonce", idGenerator.generateIdentifier())
+ .build();
+ final SignedJWT requestObject = createSecretJWT(ro, clientSecret);
+ setHttpFormRequest("POST", createRequestParameters(clientId, null, null,
+ requestObject.serialize()));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ }
+ }
+
+ @Test
+ public void testQueryResponseModePreventedForImplicitRO() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException, JOSEException {
+ for (final String clientId : clientIds) {
+ storeMetadata(storageService, clientId, clientSecret, scope, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ final JWTClaimsSet ro = new JWTClaimsSet.Builder()
+ .claim("iss", clientId)
+ .claim("client_id", clientId)
+ .claim("aud", issuer)
+ .claim("response_type", "id_token")
+ .claim("response_mode", "query")
+ .claim("redirect_uri", "https://example.org/cb")
+ .claim("scope", "openid profile")
+ .claim("nonce", idGenerator.generateIdentifier())
+ .build();
+ final SignedJWT requestObject = createSecretJWT(ro, clientSecret);
+ setHttpFormRequest("POST", createRequestParameters(clientId, null, null,
+ requestObject.serialize()));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ }
+ }
+
@Test
public void testSuccessWithOpenIdParams() throws IOException, NoSuchAlgorithmException, URISyntaxException,
DataSealerException, ComponentInitializationException {
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list