keycloak-uncached

set request object mandatory for client, restrict delivery

3/18/2018 1:01:16 PM

Details

diff --git a/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java
index d7f9939..97adcf1 100644
--- a/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java
@@ -85,6 +85,8 @@ public class OIDCClientRepresentation {
     private String request_object_encryption_alg;
 
     private String request_object_encryption_enc;
+    
+    private String request_object_required;
 
     private Integer default_max_age;
 
@@ -336,6 +338,14 @@ public class OIDCClientRepresentation {
     public void setRequestObjectEncryptionEnc(String request_object_encryption_enc) {
         this.request_object_encryption_enc = request_object_encryption_enc;
     }
+    
+    public String getRequestObjectRequired() {
+        return request_object_required;
+    }
+
+    public void setRequestObjectRequired(String request_object_required) {
+        this.request_object_required = request_object_required;
+    }
 
     public Integer getDefaultMaxAge() {
         return default_max_age;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java
index 3562ed5..d645708 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java
@@ -23,6 +23,7 @@ import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.services.ErrorPageException;
 import org.keycloak.services.ServicesLogger;
@@ -31,6 +32,9 @@ import org.keycloak.services.messages.Messages;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import java.io.InputStream;
+import static org.keycloak.protocol.oidc.OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI;
+import static org.keycloak.protocol.oidc.OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST;
+import static org.keycloak.protocol.oidc.OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST_URI;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -50,6 +54,19 @@ public class AuthorizationEndpointRequestParserProcessor {
                 throw new RuntimeException("Illegal to use both 'request' and 'request_uri' parameters together");
             }
 
+            String requestObjectRequired = OIDCAdvancedConfigWrapper.fromClientModel(client).getRequestObjectRequired();
+
+            if (REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI.equals(requestObjectRequired)
+                    && requestParam == null && requestUriParam == null) {
+                throw new RuntimeException("Client is required to use 'request' or 'request_uri' parameter.");
+            } else if (REQUEST_OBJECT_REQUIRED_REQUEST.equals(requestObjectRequired)
+                    && requestParam == null) {
+                throw new RuntimeException("Client is required to use 'request' parameter.");
+            } else if (REQUEST_OBJECT_REQUIRED_REQUEST_URI.equals(requestObjectRequired)
+                    && requestUriParam == null) {
+                throw new RuntimeException("Client is required to use 'request_uri' parameter.");
+            }
+
             if (requestParam != null) {
                 new AuthzEndpointRequestObjectParser(session, requestParam, client).parseRequest(request);
             } else if (requestUriParam != null) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
index 6f4ff75..9fc49ba 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
@@ -31,6 +31,8 @@ public class OIDCAdvancedConfigWrapper {
     private static final String USER_INFO_RESPONSE_SIGNATURE_ALG = "user.info.response.signature.alg";
 
     private static final String REQUEST_OBJECT_SIGNATURE_ALG = "request.object.signature.alg";
+    
+    private static final String REQUEST_OBJECT_REQUIRED = "request.object.required";
 
     private static final String JWKS_URL = "jwks.url";
 
@@ -79,6 +81,14 @@ public class OIDCAdvancedConfigWrapper {
         String algStr = alg==null ? null : alg.toString();
         setAttribute(REQUEST_OBJECT_SIGNATURE_ALG, algStr);
     }
+    
+    public String getRequestObjectRequired() {
+        return getAttribute(REQUEST_OBJECT_REQUIRED);
+    }
+    
+    public void setRequestObjectRequired(String requestObjectRequired) {
+        setAttribute(REQUEST_OBJECT_REQUIRED, requestObjectRequired);
+    }
 
     public boolean isUseJwksUrl() {
         String useJwksUrl = getAttribute(USE_JWKS_URL);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index 148d840..71b5066 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -92,6 +92,11 @@ public class OIDCLoginProtocol implements LoginProtocol {
     public static final String CLIENT_SECRET_POST = "client_secret_post";
     public static final String CLIENT_SECRET_JWT = "client_secret_jwt";
     public static final String PRIVATE_KEY_JWT = "private_key_jwt";
+    
+    // Request object requirement options
+    public static final String REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI = "request or request_uri";
+    public static final String REQUEST_OBJECT_REQUIRED_REQUEST = "request only";
+    public static final String REQUEST_OBJECT_REQUIRED_REQUEST_URI = "request_uri only";
 
     // https://tools.ietf.org/html/rfc7636#section-4.3
     public static final String CODE_CHALLENGE_PARAM = "code_challenge";
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
index e140b52..43526c6 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
@@ -48,6 +48,7 @@ import java.io.IOException;
 import java.net.URI;
 import java.security.PublicKey;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -115,6 +116,17 @@ public class DescriptionConverter {
             configWrapper.setRequestObjectSignatureAlg(algorithm);
         }
 
+        if (clientOIDC.getRequestObjectRequired() != null) {
+            String requestObjectRequired = clientOIDC.getRequestObjectRequired();
+            if (Arrays.asList(
+                    OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI,
+                    OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST,
+                    OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST_URI)
+                    .contains(requestObjectRequired)) {
+                configWrapper.setRequestObjectRequired(requestObjectRequired);
+            }
+        }
+
         return client;
     }
 
@@ -185,6 +197,9 @@ public class DescriptionConverter {
         if (config.getRequestObjectSignatureAlg() != null) {
             response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString());
         }
+        if (config.getRequestObjectRequired() != null) {
+            response.setRequestObjectRequired(config.getRequestObjectRequired());
+        }
         if (config.isUseJwksUrl()) {
             response.setJwksUri(config.getJwksUrl());
         }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
index 57f71b2..9fbb6bc 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
@@ -200,6 +200,20 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
         Assert.assertEquals(config.getUserInfoSignedResponseAlg(), Algorithm.RS256);
         Assert.assertEquals(config.getRequestObjectSignatureAlg(), Algorithm.RS256);
     }
+    
+    @Test
+    public void testRequestObjectRequired() throws Exception {
+        OIDCClientRepresentation clientRep = createRep();
+        clientRep.setRequestObjectRequired(OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI);
+
+        OIDCClientRepresentation response = reg.oidc().create(clientRep);
+        Assert.assertEquals(OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI, response.getRequestObjectRequired());
+
+        // Test Keycloak representation
+        ClientRepresentation kcClient = getClient(response.getClientId());
+        OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
+        Assert.assertEquals(OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI, config.getRequestObjectRequired());
+    }
 
     @Test
     public void createClientImplicitFlow() throws ClientRegistrationException {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
index 78ec0e7..ae258d6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
@@ -71,6 +71,10 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import static org.keycloak.protocol.oidc.OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI;
+import static org.keycloak.protocol.oidc.OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST;
+import static org.keycloak.protocol.oidc.OIDCLoginProtocol.REQUEST_OBJECT_REQUIRED_REQUEST_URI;
+
 /**
  * Test for supporting advanced parameters of OIDC specs (max_age, prompt, ...)
  *
@@ -376,6 +380,281 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
     }
 
     // REQUEST & REQUEST_URI
+    
+    @Test
+    public void requestObjectNotRequiredNotProvided() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+        
+        // Send request without request object
+        // Assert that the request is accepted
+        OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
+        Assert.assertNotNull(response.getCode());
+        Assert.assertEquals("mystate2", response.getState());
+        assertTrue(appPage.isCurrent());
+    }
+    
+    @Test
+    public void requestObjectNotRequiredProvidedInRequestParam() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+        
+        // Set up a request object
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString());
+        
+        // Send request object in "request" param
+        oauth.request(oidcClientEndpointsResource.getOIDCRequest());
+        // Assert that the request is accepted
+        OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
+        Assert.assertNotNull(response1.getCode());
+        Assert.assertEquals("mystate2", response1.getState());
+        assertTrue(appPage.isCurrent());
+    }
+    
+    @Test
+    public void requestObjectNotRequiredProvidedInRequestUriParam() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+        
+        // Set up a request object
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString());
+        
+        // Send request object reference in "request_uri" param
+        oauth.requestUri(TestApplicationResourceUrls.clientRequestUri());
+        // Assert that the request is accepted
+        OAuthClient.AuthorizationEndpointResponse response2 = oauth.doLogin("test-user@localhost", "password");
+        Assert.assertNotNull(response2.getCode());
+        Assert.assertEquals("mystate2", response2.getState());
+        assertTrue(appPage.isCurrent());
+    }
+    
+    @Test
+    public void requestObjectRequiredNotProvided() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI);
+        clientResource.update(clientRep);
+        
+        // Send request without request object
+        // Assert that the request is not accepted
+        oauth.openLoginForm();
+        Assert.assertTrue(errorPage.isCurrent());
+        assertEquals("Invalid Request", errorPage.getError());
+        
+        // Revert requiring request object for client
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+    }
+    
+    @Test
+    public void requestObjectRequiredProvidedInRequestParam() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI);
+        clientResource.update(clientRep);
+        
+        // Set up a request object
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString());
+        
+        // Send request object in "request" param
+        oauth.request(oidcClientEndpointsResource.getOIDCRequest());
+        // Assert that the request is accepted
+        OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
+        Assert.assertNotNull(response1.getCode());
+        Assert.assertEquals("mystate2", response1.getState());
+        assertTrue(appPage.isCurrent());
+        
+        // Revert requiring request object for client
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+    }
+    
+    @Test
+    public void requestObjectRequiredProvidedInRequestUriParam() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI);
+        clientResource.update(clientRep);
+        
+        // Set up a request object
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString());
+        
+        // Send request object reference in "request_uri" param
+        oauth.requestUri(TestApplicationResourceUrls.clientRequestUri());
+        // Assert that the request is accepted
+        OAuthClient.AuthorizationEndpointResponse response2 = oauth.doLogin("test-user@localhost", "password");
+        Assert.assertNotNull(response2.getCode());
+        Assert.assertEquals("mystate2", response2.getState());
+        assertTrue(appPage.isCurrent());
+        
+        // Revert requiring request object for client
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+    }
+    
+    @Test
+    public void requestObjectRequiredAsRequestParamNotProvided() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(REQUEST_OBJECT_REQUIRED_REQUEST);
+        clientResource.update(clientRep);
+        
+        // Send request without request object
+        // Assert that the request is not accepted
+        oauth.openLoginForm();
+        Assert.assertTrue(errorPage.isCurrent());
+        assertEquals("Invalid Request", errorPage.getError());
+        
+        // Revert requiring request object for client
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+    }
+    
+    @Test
+    public void requestObjectRequiredAsRequestParamProvidedInRequestParam() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(REQUEST_OBJECT_REQUIRED_REQUEST);
+        clientResource.update(clientRep);
+        
+        // Set up a request object
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString());
+        
+        // Send request object in "request" param
+        oauth.request(oidcClientEndpointsResource.getOIDCRequest());
+        // Assert that the request is accepted
+        OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
+        Assert.assertNotNull(response1.getCode());
+        Assert.assertEquals("mystate2", response1.getState());
+        assertTrue(appPage.isCurrent());
+        
+        // Revert requiring request object for client
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+    }
+    
+    @Test
+    public void requestObjectRequiredAsRequestParamProvidedInRequestUriParam() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(REQUEST_OBJECT_REQUIRED_REQUEST);
+        clientResource.update(clientRep);
+        
+        // Set up a request object
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString());
+        
+        // Send request object reference in "request_uri" param
+        oauth.requestUri(TestApplicationResourceUrls.clientRequestUri());
+        // Assert that the request is accepted
+        oauth.openLoginForm();
+        Assert.assertTrue(errorPage.isCurrent());
+        assertEquals("Invalid Request", errorPage.getError());
+        
+        // Revert requiring request object for client
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+    }
+    
+    @Test
+    public void requestObjectRequiredAsRequestUriParamNotProvided() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(REQUEST_OBJECT_REQUIRED_REQUEST_URI);
+        clientResource.update(clientRep);
+        
+        // Send request without request object
+        // Assert that the request is not accepted
+        oauth.openLoginForm();
+        Assert.assertTrue(errorPage.isCurrent());
+        assertEquals("Invalid Request", errorPage.getError());
+        
+        // Revert requiring request object for client
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+    }
+    
+    @Test
+    public void requestObjectRequiredAsRequestUriParamProvidedInRequestParam() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(REQUEST_OBJECT_REQUIRED_REQUEST_URI);
+        clientResource.update(clientRep);
+        
+        // Set up a request object
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString());
+        
+        // Send request object in "request" param
+        oauth.request(oidcClientEndpointsResource.getOIDCRequest());
+        // Assert that the request is not accepted
+        oauth.openLoginForm();
+        Assert.assertTrue(errorPage.isCurrent());
+        assertEquals("Invalid Request", errorPage.getError());
+        
+        // Revert requiring request object for client
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+    }
+    
+    @Test
+    public void requestObjectRequiredAsRequestUriParamProvidedInRequestUriParam() throws Exception {
+        oauth.stateParamHardcoded("mystate2");
+        // Set request object not required for client
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(REQUEST_OBJECT_REQUIRED_REQUEST_URI);
+        clientResource.update(clientRep);
+        
+        // Set up a request object
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString());
+        
+        // Send request object reference in "request_uri" param
+        oauth.requestUri(TestApplicationResourceUrls.clientRequestUri());
+        // Assert that the request is accepted
+        OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
+        Assert.assertNotNull(response1.getCode());
+        Assert.assertEquals("mystate2", response1.getState());
+        assertTrue(appPage.isCurrent());
+        
+        // Revert requiring request object for client
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectRequired(null);
+        clientResource.update(clientRep);
+    }
 
     @Test
     public void requestParamUnsigned() throws Exception {
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index c384b13..12c5e05 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -309,6 +309,8 @@ user-info-signed-response-alg=User Info Signed Response Algorithm
 user-info-signed-response-alg.tooltip=JWA algorithm used for signed User Info Endpoint response. If set to 'unsigned', then User Info Response won't be signed and will be returned in application/json format.
 request-object-signature-alg=Request Object Signature Algorithm
 request-object-signature-alg.tooltip=JWA algorithm, which client needs to use when sending OIDC request object specified by 'request' or 'request_uri' parameters. If set to 'any', then Request object can be signed by any algorithm (including 'none' ).
+request-object-required=Request Object Required
+request-object-required-alg.tooltip=Specifies if the client needs to provide a request object with their authorization requests, and what method they can use for this. If set to "not required", providing a request object is optional. In all other cases providing a request object is mandatory. If set to "request", the request object must be provided by value. If set to "request_uri", the request object must be provided by reference. If set to "request or request_uri", either method can be used.
 fine-saml-endpoint-conf=Fine Grain SAML Endpoint Configuration
 fine-saml-endpoint-conf.tooltip=Expand this section to configure exact URLs for Assertion Consumer and Single Logout Service.
 assertion-consumer-post-binding-url=Assertion Consumer Service POST Binding URL
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 632ef2c..381a4c2 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -891,6 +891,13 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
         "none",
         "RS256"
     ];
+    
+    $scope.requestObjectRequiredOptions = [
+        "not required",
+        "request or request_uri",
+        "request only",
+        "request_uri only"
+    ];
 
     $scope.realm = realm;
     $scope.samlAuthnStatement = false;
@@ -1045,6 +1052,9 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
         var attrVal2 = $scope.client.attributes['request.object.signature.alg'];
         $scope.requestObjectSignatureAlg = attrVal2==null ? 'any' : attrVal2;
 
+        var attrVal3 = $scope.client.attributes['request.object.required'];
+        $scope.requestObjectRequired = attrVal3==null ? 'not required' : attrVal3;
+
         if ($scope.client.attributes["exclude.session.state.from.auth.response"]) {
             if ($scope.client.attributes["exclude.session.state.from.auth.response"] == "true") {
                 $scope.excludeSessionStateFromAuthResponse = true;
@@ -1143,6 +1153,14 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
             $scope.clientEdit.attributes['request.object.signature.alg'] = $scope.requestObjectSignatureAlg;
         }
     };
+    
+    $scope.changeRequestObjectRequired = function() {
+        if ($scope.requestObjectRequired === 'not required') {
+            $scope.clientEdit.attributes['request.object.required'] = null;
+        } else {
+            $scope.clientEdit.attributes['request.object.required'] = $scope.requestObjectRequired;
+        }
+    };
 
     $scope.$watch(function() {
         return $location.path();
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index 96e00e9..9352ff1 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -409,6 +409,19 @@
                 </div>
                 <kc-tooltip>{{:: 'request-object-signature-alg.tooltip' | translate}}</kc-tooltip>
             </div>
+            <div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
+                <label class="col-md-2 control-label" for="changeRequestObjectRequired">{{:: 'request-object-required' | translate}}</label>
+                <div class="col-sm-6">
+                    <div>
+                        <select class="form-control" id="requestObjectRequired"
+                                ng-change="changeRequestObjectRequired()"
+                                ng-model="requestObjectRequired"
+                                ng-options="sig for sig in requestObjectRequiredOptions">
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'request-object-required.tooltip' | translate}}</kc-tooltip>
+            </div>
         </fieldset>
 
         <fieldset data-ng-show="protocol == 'openid-connect'">