From a79d28f115aa1e492331609e2d57b9be202d86a6 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Tue, 13 Jul 2021 21:35:45 -0300 Subject: [PATCH] [KEYCLOAK-18729] - Support JAR when using PAR --- ...izationEndpointRequestParserProcessor.java | 2 +- .../AuthzEndpointRequestObjectParser.java | 19 +- .../request/AuthzEndpointParParser.java | 24 +- .../executor/SecureRequestObjectExecutor.java | 2 +- ...stingOIDCEndpointsApplicationResource.java | 6 + .../TestOIDCEndpointsApplicationResource.java | 8 + .../client/AbstractClientPoliciesTest.java | 7 +- .../testsuite/client/ClientPoliciesTest.java | 8 + .../oidc/OIDCAdvancedRequestParamsTest.java | 33 +-- .../org/keycloak/testsuite/par/ParTest.java | 255 ++++++++++++++++++ 10 files changed, 337 insertions(+), 27 deletions(-) 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 d695ee003d..585196e88e 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 @@ -81,7 +81,7 @@ public class AuthorizationEndpointRequestParserProcessor { // Define, if the request is `PAR` or usual `Request Object`. RequestUriType requestUriType = getRequestUriType(requestUriParam); if (requestUriType == RequestUriType.PAR) { - new AuthzEndpointParParser(session, requestUriParam).parseRequest(request); + new AuthzEndpointParParser(session, client, requestUriParam).parseRequest(request); } else { // Validate "requestUriParam" with allowed requestUris List requestUris = OIDCAdvancedConfigWrapper.fromClientModel(client).getRequestUris(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java index 8c12fbdf10..445e401432 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import java.util.HashSet; import java.util.Set; +import org.keycloak.OAuth2Constants; import org.keycloak.jose.JOSEHeader; import org.keycloak.jose.JOSE; import org.keycloak.jose.jws.Algorithm; @@ -33,7 +34,7 @@ import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; * * @author Marek Posolda */ -class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { +public class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { private static void validateAlgorithm(JOSE jwt, ClientModel clientModel) { if (jwt instanceof JWSInput) { @@ -62,6 +63,16 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { throw new RuntimeException("Failed to verify signature on 'request' object"); } + JsonNode clientId = this.requestParams.get(OAuth2Constants.CLIENT_ID); + + if (clientId == null) { + throw new RuntimeException("Request object must be set with the client_id"); + } + + if (!client.getClientId().equals(clientId.asText())) { + throw new RuntimeException("The client_id in the request object is not the same as the authorizing client"); + } + session.setAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT, requestParams); } @@ -89,4 +100,10 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { requestParams.fieldNames().forEachRemaining(keys::add); return keys; } + + @Override + protected T replaceIfNotNull(T previousVal, T newVal) { + // force parameters values from request object as per spec any parameter set directly should be ignored + return newVal; + } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java index ce275db549..10dceb1627 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java @@ -23,9 +23,13 @@ import java.util.Set; import java.util.UUID; import org.jboss.logging.Logger; +import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.PushedAuthzRequestStoreProvider; import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest; +import org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestObjectParser; import org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestParser; import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint; @@ -39,11 +43,14 @@ public class AuthzEndpointParParser extends AuthzEndpointRequestParser { private static final Logger logger = Logger.getLogger(AuthzEndpointParParser.class); + private final KeycloakSession session; + private final ClientModel client; private Map requestParams; - private String invalidRequestMessage = null; - public AuthzEndpointParParser(KeycloakSession session, String requestUri) { + public AuthzEndpointParParser(KeycloakSession session, ClientModel client, String requestUri) { + this.session = session; + this.client = client; PushedAuthzRequestStoreProvider parStore = session.getProvider(PushedAuthzRequestStoreProvider.class); UUID key; try { @@ -67,6 +74,19 @@ public class AuthzEndpointParParser extends AuthzEndpointRequestParser { } } + @Override + public void parseRequest(AuthorizationEndpointRequest request) { + String requestParam = requestParams.get(OIDCLoginProtocol.REQUEST_PARAM); + + if (requestParam != null) { + // parses the request object if PAR was registered using JAR + // parameters from requets object have precedence over those sent directly in the request + new AuthzEndpointRequestObjectParser(session, requestParam, client).parseRequest(request); + } else { + super.parseRequest(request); + } + } + @Override protected String getParameter(String paramName) { return requestParams.get(paramName); diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureRequestObjectExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureRequestObjectExecutor.java index d50028e319..7773fb557a 100644 --- a/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureRequestObjectExecutor.java +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureRequestObjectExecutor.java @@ -152,7 +152,7 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider } // check whether scope exists in both query parameter and request object - if (params.getFirst(OIDCLoginProtocol.SCOPE_PARAM) == null || requestObject.get(OIDCLoginProtocol.SCOPE_PARAM) == null) { + if (params.getFirst(OIDCLoginProtocol.SCOPE_PARAM) == null && requestObject.get(OIDCLoginProtocol.SCOPE_PARAM) == null) { logger.trace("scope object not exist."); throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Parameter 'scope' missing in the request parameters or in 'request' object"); } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java index 92bd82d70b..22d7bc968c 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java @@ -230,12 +230,18 @@ public class TestingOIDCEndpointsApplicationResource { @NoCache public void setOIDCRequest(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId, @QueryParam("redirectUri") String redirectUri, @QueryParam("maxAge") String maxAge, + @QueryParam("state") String state, @QueryParam("jwaAlgorithm") String jwaAlgorithm) { Map oidcRequest = new HashMap<>(); oidcRequest.put(OIDCLoginProtocol.CLIENT_ID_PARAM, clientId); oidcRequest.put(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE); oidcRequest.put(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri); + + if (state != null) { + oidcRequest.put(OIDCLoginProtocol.STATE_PARAM, state); + } + if (maxAge != null) { oidcRequest.put(OIDCLoginProtocol.MAX_AGE_PARAM, Integer.parseInt(maxAge)); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java index d0f934717e..5fa1a68c16 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java @@ -65,8 +65,16 @@ public interface TestOIDCEndpointsApplicationResource { @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) void setOIDCRequest(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId, @QueryParam("redirectUri") String redirectUri, @QueryParam("maxAge") String maxAge, + @QueryParam("state") String state, @QueryParam("jwaAlgorithm") String jwaAlgorithm); + @GET + @Path("/set-oidc-request") + @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) + void setOIDCRequest(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId, + @QueryParam("redirectUri") String redirectUri, @QueryParam("maxAge") String maxAge, + @QueryParam("jwaAlgorithm") String jwaAlgorithm); + @GET @Path("/register-oidc-request") @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java index 4a7272411c..23859731c7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java @@ -543,12 +543,13 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { requestObject.setResponseType("code"); requestObject.setRedirectUriParam(oauth.getRedirectUri()); requestObject.setScope("openid"); - String scope = KeycloakModelUtils.generateId(); - oauth.stateParamHardcoded(scope); - requestObject.setState(scope); + String state = KeycloakModelUtils.generateId(); + oauth.stateParamHardcoded(state); + requestObject.setState(state); requestObject.setMax_age(Integer.valueOf(600)); requestObject.setOtherClaims("custom_claim_ein", "rot"); requestObject.audience(Urls.realmIssuer(new URI(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth"), REALM_NAME), "https://example.com"); + requestObject.setNonce(KeycloakModelUtils.generateId()); return requestObject; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java index 966d54759d..e0729ffc87 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java @@ -1193,7 +1193,15 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest { registerRequestObject(requestObject, clientId, Algorithm.ES256, true); oauth.openLoginForm(); assertEquals(OAuthErrorException.INVALID_REQUEST, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); + assertEquals("Invalid parameter. Parameters in 'request' object not matching with request parameters", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); + + registerRequestObject(requestObject, clientId, Algorithm.ES256, true); + oauth.scope(null); + oauth.openid(false); + oauth.openLoginForm(); + assertEquals(OAuthErrorException.INVALID_REQUEST, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals("Parameter 'scope' missing in the request parameters or in 'request' object", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); + oauth.openid(true); // check whether "exp" claim exists requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId); 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 ab8ae79a30..e5ae3dd39e 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 @@ -547,7 +547,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest // Set up a request object TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", "mystate2", Algorithm.none.toString()); // Send request object in "request" param oauth.request(oidcClientEndpointsResource.getOIDCRequest()); @@ -569,7 +569,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest // Set up a request object TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", "mystate2", Algorithm.none.toString()); // Send request object reference in "request_uri" param oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); @@ -611,7 +611,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest // Set up a request object TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", "mystate2", Algorithm.none.toString()); // Send request object in "request" param oauth.request(oidcClientEndpointsResource.getOIDCRequest()); @@ -637,7 +637,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest // Set up a request object TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", "mystate2", Algorithm.none.toString()); // Send request object reference in "request_uri" param oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); @@ -683,7 +683,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest // Set up a request object TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", "mystate2", Algorithm.none.toString()); // Send request object in "request" param oauth.request(oidcClientEndpointsResource.getOIDCRequest()); @@ -779,7 +779,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest // Set up a request object TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", Algorithm.none.toString()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", oauth.getRedirectUri(), "10", "mystate2", Algorithm.none.toString()); // Send request object reference in "request_uri" param oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); @@ -812,7 +812,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest // Assert the value from request object has bigger priority then from the query parameter. oauth.redirectUri("http://invalid"); - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", "mystate2", Algorithm.none.toString()); requestStr = oidcClientEndpointsResource.getOIDCRequest(); oauth.request(requestStr); @@ -824,13 +824,11 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest @Test public void requestUriParamUnsigned() throws Exception { - oauth.stateParamHardcoded("mystate1"); - String validRedirectUri = oauth.getRedirectUri(); TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); // Send request object with invalid redirect uri. - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", "http://invalid", null, Algorithm.none.toString()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", "http://invalid", null, "mystate1", Algorithm.none.toString()); oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); oauth.openLoginForm(); @@ -839,7 +837,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest // Assert the value from request object has bigger priority then from the query parameter. oauth.redirectUri("http://invalid"); - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", "mystate1", Algorithm.none.toString()); OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); Assert.assertNotNull(response.getCode()); @@ -849,10 +847,9 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest @Test public void requestUriParamWithAllowedRequestUris() throws Exception { - oauth.stateParamHardcoded("mystate1"); String validRedirectUri = oauth.getRedirectUri(); TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", "mystate1", Algorithm.none.toString()); ClientManager.ClientManagerBuilder clientMgrBuilder = ClientManager.realm(adminClient.realm("test")).clientId("test-app"); oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); @@ -915,8 +912,6 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest @Test public void requestUriParamSigned() throws Exception { - oauth.stateParamHardcoded("mystate3"); - String validRedirectUri = oauth.getRedirectUri(); TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); @@ -937,7 +932,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest String clientPublicKeyPem = oidcClientEndpointsResource.generateKeys("RS256").get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY); // Verify signed request_uri will fail due to failed signature validation - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.RS256.toString()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", "mystate3", Algorithm.RS256.toString()); oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); assertEquals("Invalid Request", errorPage.getError()); @@ -968,8 +963,6 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest ClientResource clientResource = null; ClientRepresentation clientRep = null; try { - oauth.stateParamHardcoded("mystate3"); - String validRedirectUri = oauth.getRedirectUri(); TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); @@ -983,7 +976,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest if (Algorithm.none != actualAlgorithm) oidcClientEndpointsResource.generateKeys(actualAlgorithm.name()); // register request object - oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", actualAlgorithm.name()); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", "mystate3", actualAlgorithm.name()); // use and set jwks_url clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"); @@ -1214,6 +1207,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest oidcRequest.put(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE); oidcRequest.put(OIDCLoginProtocol.REDIRECT_URI_PARAM, oauth.getRedirectUri()); oidcRequest.put(OIDCLoginProtocol.CLAIMS_PARAM, claims); + oidcRequest.put(OIDCLoginProtocol.SCOPE_PARAM, "openid"); String request = new JWSBuilder().jsonContent(oidcRequest).none(); oauth = oauth.request(request); @@ -1261,6 +1255,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest oidcRequest.put(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE); oidcRequest.put(OIDCLoginProtocol.REDIRECT_URI_PARAM, oauth.getRedirectUri()); oidcRequest.put(OIDCLoginProtocol.CLAIMS_PARAM, claims); + oidcRequest.put(OIDCLoginProtocol.SCOPE_PARAM, "openid"); request = new JWSBuilder().jsonContent(oidcRequest).none(); oauth = oauth.request(request); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/par/ParTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/par/ParTest.java index 0bb2270838..5b2630d22c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/par/ParTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/par/ParTest.java @@ -19,6 +19,8 @@ package org.keycloak.testsuite.par; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername; @@ -39,14 +41,19 @@ import org.junit.Assert; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; +import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.common.Profile; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.Time; import org.keycloak.models.AdminRoles; import org.keycloak.models.Constants; import org.keycloak.models.ParConfig; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; import org.keycloak.representations.RefreshToken; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; @@ -55,9 +62,13 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.services.clientpolicy.ClientPolicyEvent; import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory; +import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.client.AbstractClientPoliciesTest; +import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; +import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource; +import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; import org.keycloak.testsuite.services.clientpolicy.executor.TestRaiseExeptionExecutorFactory; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.OAuthClient; @@ -66,6 +77,7 @@ import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder; import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfileBuilder; import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfilesBuilder; import org.keycloak.testsuite.util.OAuthClient.ParResponse; +import org.keycloak.util.JsonSerialization; import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.QUARKUS; import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE; @@ -203,6 +215,249 @@ public class ParTest extends AbstractClientPoliciesTest { } } + @Test + public void testSuccessfulUsingRequestParameter() throws Exception { + try { + // setup PAR realm settings + int requestUriLifespan = 45; + setParRealmSettings(requestUriLifespan); + + // create client dynamically + String clientId = createClientDynamically(generateSuffixedName(CLIENT_NAME), (OIDCClientRepresentation clientRep) -> { + clientRep.setRequirePushedAuthorizationRequests(Boolean.TRUE); + clientRep.setRedirectUris(new ArrayList<>(Arrays.asList(CLIENT_REDIRECT_URI))); + }); + + oauth.clientId(clientId); + + OIDCClientRepresentation oidcCRep = getClientDynamically(clientId); + String clientSecret = oidcCRep.getClientSecret(); + assertEquals(Boolean.TRUE, oidcCRep.getRequirePushedAuthorizationRequests()); + assertTrue(oidcCRep.getRedirectUris().contains(CLIENT_REDIRECT_URI)); + assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, oidcCRep.getTokenEndpointAuthMethod()); + + TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = new TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject(); + requestObject.id(KeycloakModelUtils.generateId()); + requestObject.iat(Long.valueOf(Time.currentTime())); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.setClientId(oauth.getClientId()); + requestObject.setResponseType("code"); + requestObject.setRedirectUriParam(CLIENT_REDIRECT_URI); + requestObject.setScope("openid"); + requestObject.setNonce(KeycloakModelUtils.generateId()); + + byte[] contentBytes = JsonSerialization.writeValueAsBytes(requestObject); + String encodedRequestObject = Base64Url.encode(contentBytes); + TestOIDCEndpointsApplicationResource client = testingClient.testApp().oidcClientEndpoints(); + + // use and set jwks_url + ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(oauth.getRealm()), oauth.getClientId()); + ClientRepresentation clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(TestApplicationResourceUrls.clientJwksUri()); + clientResource.update(clientRep); + client.generateKeys(org.keycloak.crypto.Algorithm.RS256); + client.registerOIDCRequest(encodedRequestObject, org.keycloak.crypto.Algorithm.RS256); + + // do not send any other parameter but the request request parameter + oauth.request(client.getOIDCRequest()); + oauth.responseType(null); + oauth.redirectUri(null); + oauth.scope(null); + ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, clientSecret); + assertEquals(201, pResp.getStatusCode()); + String requestUri = pResp.getRequestUri(); + assertEquals(requestUriLifespan, pResp.getExpiresIn()); + + // Authorization Request with request_uri of PAR + // remove parameters as query strings of uri + oauth.redirectUri(null); + oauth.scope(null); + oauth.responseType(null); + oauth.request(null); + oauth.requestUri(requestUri); + OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD); + + // Token Request + oauth.redirectUri(CLIENT_REDIRECT_URI); // get tokens, it needed. https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 + OAuthClient.AccessTokenResponse res = oauth.doAccessTokenRequest(loginResponse.getCode(), clientSecret); + assertEquals(200, res.getStatusCode()); + + oauth.verifyToken(res.getAccessToken()); + IDToken idToken = oauth.verifyIDToken(res.getIdToken()); + assertEquals(requestObject.getNonce(), idToken.getNonce()); + } finally { + restoreParRealmSettings(); + } + } + + @Test + public void testRequestParameterPrecedenceOverOtherParameters() throws Exception { + try { + // setup PAR realm settings + int requestUriLifespan = 45; + setParRealmSettings(requestUriLifespan); + + // create client dynamically + String clientId = createClientDynamically(generateSuffixedName(CLIENT_NAME), (OIDCClientRepresentation clientRep) -> { + clientRep.setRequirePushedAuthorizationRequests(Boolean.TRUE); + clientRep.setRedirectUris(new ArrayList<>(Arrays.asList(CLIENT_REDIRECT_URI))); + }); + + oauth.clientId(clientId); + + OIDCClientRepresentation oidcCRep = getClientDynamically(clientId); + String clientSecret = oidcCRep.getClientSecret(); + assertEquals(Boolean.TRUE, oidcCRep.getRequirePushedAuthorizationRequests()); + assertTrue(oidcCRep.getRedirectUris().contains(CLIENT_REDIRECT_URI)); + assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, oidcCRep.getTokenEndpointAuthMethod()); + + TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = new TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject(); + requestObject.id(KeycloakModelUtils.generateId()); + requestObject.iat(Long.valueOf(Time.currentTime())); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.setClientId(oauth.getClientId()); + requestObject.setResponseType("code"); + requestObject.setRedirectUriParam(CLIENT_REDIRECT_URI); + requestObject.setScope("openid"); + requestObject.setNonce(KeycloakModelUtils.generateId()); + requestObject.setState(oauth.stateParamRandom().getState()); + + + byte[] contentBytes = JsonSerialization.writeValueAsBytes(requestObject); + String encodedRequestObject = Base64Url.encode(contentBytes); + TestOIDCEndpointsApplicationResource client = testingClient.testApp().oidcClientEndpoints(); + + // use and set jwks_url + ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(oauth.getRealm()), oauth.getClientId()); + ClientRepresentation clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(TestApplicationResourceUrls.clientJwksUri()); + clientResource.update(clientRep); + client.generateKeys(org.keycloak.crypto.Algorithm.RS256); + client.registerOIDCRequest(encodedRequestObject, org.keycloak.crypto.Algorithm.RS256); + + // do not send any other parameter but the request request parameter + oauth.request(client.getOIDCRequest()); + oauth.responseType("code id_token"); + oauth.redirectUri("http://invalid"); + oauth.scope(null); + oauth.nonce("12345"); + ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, clientSecret); + assertEquals(201, pResp.getStatusCode()); + String requestUri = pResp.getRequestUri(); + assertEquals(requestUriLifespan, pResp.getExpiresIn()); + + oauth.scope("invalid"); + oauth.redirectUri("http://invalid"); + oauth.responseType("invalid"); + oauth.redirectUri(null); + oauth.nonce("12345"); + oauth.request(null); + oauth.requestUri(requestUri); + String wrongState = oauth.stateParamRandom().getState(); + oauth.stateParamHardcoded(wrongState); + OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD); + assertEquals(requestObject.getState(), loginResponse.getState()); + assertNotEquals(requestObject.getState(), wrongState); + + // Token Request + oauth.redirectUri(CLIENT_REDIRECT_URI); // get tokens, it needed. https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 + OAuthClient.AccessTokenResponse res = oauth.doAccessTokenRequest(loginResponse.getCode(), clientSecret); + assertEquals(200, res.getStatusCode()); + + oauth.verifyToken(res.getAccessToken()); + IDToken idToken = oauth.verifyIDToken(res.getIdToken()); + assertEquals(requestObject.getNonce(), idToken.getNonce()); + } finally { + restoreParRealmSettings(); + } + } + + @Test + public void testIgnoreParameterIfNotSetinRequestObject() throws Exception { + try { + // setup PAR realm settings + int requestUriLifespan = 45; + setParRealmSettings(requestUriLifespan); + + // create client dynamically + String clientId = createClientDynamically(generateSuffixedName(CLIENT_NAME), (OIDCClientRepresentation clientRep) -> { + clientRep.setRequirePushedAuthorizationRequests(Boolean.TRUE); + clientRep.setRedirectUris(new ArrayList<>(Arrays.asList(CLIENT_REDIRECT_URI))); + }); + + oauth.clientId(clientId); + + OIDCClientRepresentation oidcCRep = getClientDynamically(clientId); + String clientSecret = oidcCRep.getClientSecret(); + assertEquals(Boolean.TRUE, oidcCRep.getRequirePushedAuthorizationRequests()); + assertTrue(oidcCRep.getRedirectUris().contains(CLIENT_REDIRECT_URI)); + assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, oidcCRep.getTokenEndpointAuthMethod()); + + TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = new TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject(); + requestObject.id(KeycloakModelUtils.generateId()); + requestObject.iat(Long.valueOf(Time.currentTime())); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.setClientId(oauth.getClientId()); + requestObject.setResponseType("code"); + requestObject.setRedirectUriParam(CLIENT_REDIRECT_URI); + requestObject.setScope("openid"); + requestObject.setNonce(KeycloakModelUtils.generateId()); + + byte[] contentBytes = JsonSerialization.writeValueAsBytes(requestObject); + String encodedRequestObject = Base64Url.encode(contentBytes); + TestOIDCEndpointsApplicationResource client = testingClient.testApp().oidcClientEndpoints(); + + // use and set jwks_url + ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(oauth.getRealm()), oauth.getClientId()); + ClientRepresentation clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(TestApplicationResourceUrls.clientJwksUri()); + clientResource.update(clientRep); + client.generateKeys(org.keycloak.crypto.Algorithm.RS256); + client.registerOIDCRequest(encodedRequestObject, org.keycloak.crypto.Algorithm.RS256); + + // do not send any other parameter but the request request parameter + oauth.request(client.getOIDCRequest()); + oauth.responseType("code id_token"); + oauth.redirectUri("http://invalid"); + oauth.scope(null); + oauth.nonce("12345"); + ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, clientSecret); + assertEquals(201, pResp.getStatusCode()); + String requestUri = pResp.getRequestUri(); + assertEquals(requestUriLifespan, pResp.getExpiresIn()); + + oauth.scope("invalid"); + oauth.redirectUri("http://invalid"); + oauth.responseType("invalid"); + oauth.redirectUri(null); + oauth.nonce("12345"); + oauth.request(null); + oauth.requestUri(requestUri); + String wrongState = oauth.stateParamRandom().getState(); + oauth.stateParamHardcoded(wrongState); + OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD); + assertNull(loginResponse.getState()); + assertNotEquals(requestObject.getState(), wrongState); + + // Token Request + oauth.redirectUri(CLIENT_REDIRECT_URI); // get tokens, it needed. https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 + OAuthClient.AccessTokenResponse res = oauth.doAccessTokenRequest(loginResponse.getCode(), clientSecret); + assertEquals(200, res.getStatusCode()); + + oauth.verifyToken(res.getAccessToken()); + IDToken idToken = oauth.verifyIDToken(res.getIdToken()); + assertEquals(requestObject.getNonce(), idToken.getNonce()); + } finally { + restoreParRealmSettings(); + } + } + // success with the same client conducting multiple authz requests + PAR simultaneously @Test public void testSuccessfulMultipleParBySameClient() throws Exception {