From 892d5fd1b7ab6510d1eb94e72a3dfcab9334853d Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 31 Aug 2016 18:41:00 +0200 Subject: [PATCH 1/2] TestingExportImport in separate resource --- .../rest/TestingResourceProvider.java | 92 +----------- .../resource/TestingExportImportResource.java | 142 ++++++++++++++++++ .../TestingExportImportResource.java | 93 ++++++++++++ .../client/resources/TestingResource.java | 63 +------- .../exportimport/ExportImportTest.java | 97 +++++------- 5 files changed, 280 insertions(+), 207 deletions(-) create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index b658d35c3a..78b7e70432 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -76,6 +76,7 @@ import org.keycloak.models.UserProvider; import org.keycloak.representations.idm.AuthDetailsRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.rest.resource.TestingExportImportResource; import static org.keycloak.exportimport.ExportImportConfig.*; @@ -564,22 +565,6 @@ public class TestingResourceProvider implements RealmResourceProvider { return result; } - @GET - @Path("/run-import") - @Produces(MediaType.APPLICATION_JSON) - public Response runImport() { - new ExportImportManager(session).runImport(); - return Response.ok().build(); - } - - @GET - @Path("/run-export") - @Produces(MediaType.APPLICATION_JSON) - public Response runExport() { - new ExportImportManager(session).runExport(); - return Response.ok().build(); - } - @GET @Path("/valid-credentials") @Produces(MediaType.APPLICATION_JSON) @@ -652,78 +637,9 @@ public class TestingResourceProvider implements RealmResourceProvider { return realmProvider.getRealmByName(realmName); } - @GET - @Path("/get-users-per-file") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public Integer getUsersPerFile() { - String usersPerFile = System.getProperty(USERS_PER_FILE, String.valueOf(DEFAULT_USERS_PER_FILE)); - return Integer.parseInt(usersPerFile.trim()); - } - - @PUT - @Path("/set-users-per-file") - @Consumes(MediaType.APPLICATION_JSON) - public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile) { - System.setProperty(USERS_PER_FILE, String.valueOf(usersPerFile)); - } - - @GET - @Path("/get-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String getDir() { - return System.getProperty(DIR); - } - - @PUT - @Path("/set-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String setDir(@QueryParam("dir") String dir) { - return System.setProperty(DIR, dir); - } - - @PUT - @Path("/export-import-provider") - @Consumes(MediaType.APPLICATION_JSON) - public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider) { - System.setProperty(PROVIDER, exportImportProvider); - } - - @PUT - @Path("/export-import-file") - @Consumes(MediaType.APPLICATION_JSON) - public void setFile(@QueryParam("file") String file) { - System.setProperty(FILE, file); - } - - @PUT - @Path("/export-import-action") - @Consumes(MediaType.APPLICATION_JSON) - public void setAction(@QueryParam("exportImportAction") String exportImportAction) { - System.setProperty(ACTION, exportImportAction); - } - - @PUT - @Path("/set-realm-name") - @Consumes(MediaType.APPLICATION_JSON) - public void setRealmName(@QueryParam("realmName") String realmName) { - if (realmName != null && !realmName.isEmpty()) { - System.setProperty(REALM_NAME, realmName); - } else { - System.getProperties().remove(REALM_NAME); - } - } - - @GET - @Path("/get-test-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String getExportImportTestDirectory() { - System.setProperty("project.build.directory", "target"); - String absolutePath = new File(System.getProperty("project.build.directory", "target")).getAbsolutePath(); - return absolutePath; + @Path("/export-import") + public TestingExportImportResource getExportImportResource() { + return new TestingExportImportResource(session); } } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java new file mode 100644 index 0000000000..4f9151c753 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java @@ -0,0 +1,142 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.testsuite.rest.resource; + +import java.io.File; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.keycloak.exportimport.ExportImportManager; +import org.keycloak.models.KeycloakSession; + +import static org.keycloak.exportimport.ExportImportConfig.ACTION; +import static org.keycloak.exportimport.ExportImportConfig.DEFAULT_USERS_PER_FILE; +import static org.keycloak.exportimport.ExportImportConfig.DIR; +import static org.keycloak.exportimport.ExportImportConfig.FILE; +import static org.keycloak.exportimport.ExportImportConfig.PROVIDER; +import static org.keycloak.exportimport.ExportImportConfig.REALM_NAME; +import static org.keycloak.exportimport.ExportImportConfig.USERS_PER_FILE; + +/** + * @author Marek Posolda + */ +public class TestingExportImportResource { + + private final KeycloakSession session; + + public TestingExportImportResource(KeycloakSession session) { + this.session = session; + } + + @GET + @Path("/run-import") + @Produces(MediaType.APPLICATION_JSON) + public Response runImport() { + new ExportImportManager(session).runImport(); + return Response.ok().build(); + } + + @GET + @Path("/run-export") + @Produces(MediaType.APPLICATION_JSON) + public Response runExport() { + new ExportImportManager(session).runExport(); + return Response.ok().build(); + } + + @GET + @Path("/get-users-per-file") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Integer getUsersPerFile() { + String usersPerFile = System.getProperty(USERS_PER_FILE, String.valueOf(DEFAULT_USERS_PER_FILE)); + return Integer.parseInt(usersPerFile.trim()); + } + + @PUT + @Path("/set-users-per-file") + @Consumes(MediaType.APPLICATION_JSON) + public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile) { + System.setProperty(USERS_PER_FILE, String.valueOf(usersPerFile)); + } + + @GET + @Path("/get-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String getDir() { + return System.getProperty(DIR); + } + + @PUT + @Path("/set-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String setDir(@QueryParam("dir") String dir) { + return System.setProperty(DIR, dir); + } + + @PUT + @Path("/export-import-provider") + @Consumes(MediaType.APPLICATION_JSON) + public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider) { + System.setProperty(PROVIDER, exportImportProvider); + } + + @PUT + @Path("/export-import-file") + @Consumes(MediaType.APPLICATION_JSON) + public void setFile(@QueryParam("file") String file) { + System.setProperty(FILE, file); + } + + @PUT + @Path("/export-import-action") + @Consumes(MediaType.APPLICATION_JSON) + public void setAction(@QueryParam("exportImportAction") String exportImportAction) { + System.setProperty(ACTION, exportImportAction); + } + + @PUT + @Path("/set-realm-name") + @Consumes(MediaType.APPLICATION_JSON) + public void setRealmName(@QueryParam("realmName") String realmName) { + if (realmName != null && !realmName.isEmpty()) { + System.setProperty(REALM_NAME, realmName); + } else { + System.getProperties().remove(REALM_NAME); + } + } + + @GET + @Path("/get-test-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String getExportImportTestDirectory() { + System.setProperty("project.build.directory", "target"); + String absolutePath = new File(System.getProperty("project.build.directory", "target")).getAbsolutePath(); + return absolutePath; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java new file mode 100644 index 0000000000..27fa3604c4 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java @@ -0,0 +1,93 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.testsuite.client.resources; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * @author Marek Posolda + */ +public interface TestingExportImportResource { + + @GET + @Path("/run-import") + @Produces(MediaType.APPLICATION_JSON) + public Response runImport(); + + @GET + @Path("/run-export") + @Produces(MediaType.APPLICATION_JSON) + public Response runExport(); + + @GET + @Path("/get-users-per-file") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Integer getUsersPerFile(); + + @PUT + @Path("/set-users-per-file") + @Consumes(MediaType.APPLICATION_JSON) + public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile); + + @GET + @Path("/get-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String getDir(); + + @PUT + @Path("/set-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String setDir(@QueryParam("dir") String dir); + + @PUT + @Path("/export-import-provider") + @Consumes(MediaType.APPLICATION_JSON) + public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider); + + @PUT + @Path("/export-import-file") + @Consumes(MediaType.APPLICATION_JSON) + public void setFile(@QueryParam("file") String file); + + @PUT + @Path("/export-import-action") + @Consumes(MediaType.APPLICATION_JSON) + public void setAction(@QueryParam("exportImportAction") String exportImportAction); + + @PUT + @Path("/set-realm-name") + @Consumes(MediaType.APPLICATION_JSON) + public void setRealmName(@QueryParam("realmName") String realmName); + + @GET + @Path("/get-test-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String getExportImportTestDirectory(); + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java index fcf5d8317a..0dbcd58f55 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java @@ -17,8 +17,8 @@ package org.keycloak.testsuite.client.resources; -import java.util.Date; import java.util.List; + import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.EventRepresentation; @@ -37,7 +37,6 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.Map; import org.jboss.resteasy.annotations.cache.NoCache; -import org.keycloak.exportimport.ExportImportManager; /** * @author Marko Strukelj @@ -205,16 +204,6 @@ public interface TestingResource { @Path("/update-pass-through-auth-state") @Produces(MediaType.APPLICATION_JSON) AuthenticatorState updateAuthenticator(AuthenticatorState state); - - @GET - @Path("/run-import") - @Produces(MediaType.APPLICATION_JSON) - public Response runImport(); - - @GET - @Path("/run-export") - @Produces(MediaType.APPLICATION_JSON) - public Response runExport(); @GET @Path("/valid-credentials") @@ -250,53 +239,7 @@ public interface TestingResource { @Produces(MediaType.APPLICATION_JSON) public UserRepresentation getUserByServiceAccountClient(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId); + @Path("export-import") + TestingExportImportResource exportImport(); - @GET - @Path("/get-users-per-file") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public Integer getUsersPerFile(); - - @PUT - @Path("/set-users-per-file") - @Consumes(MediaType.APPLICATION_JSON) - public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile); - - @GET - @Path("/get-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String getDir(); - - @PUT - @Path("/set-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String setDir(@QueryParam("dir") String dir); - - @PUT - @Path("/export-import-provider") - @Consumes(MediaType.APPLICATION_JSON) - public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider); - - @PUT - @Path("/export-import-file") - @Consumes(MediaType.APPLICATION_JSON) - public void setFile(@QueryParam("file") String file); - - @PUT - @Path("/export-import-action") - @Consumes(MediaType.APPLICATION_JSON) - public void setAction(@QueryParam("exportImportAction") String exportImportAction); - - @PUT - @Path("/set-realm-name") - @Consumes(MediaType.APPLICATION_JSON) - public void setRealmName(@QueryParam("realmName") String realmName); - - @GET - @Path("/get-test-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String getExportImportTestDirectory(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java index 6abaf97456..43c6fa9bb9 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java @@ -75,11 +75,11 @@ public class ExportImportTest extends AbstractExportImportTest { @Test public void testDirFullExportImport() throws Throwable { - testingClient.testing().setProvider(DirExportProviderFactory.PROVIDER_ID); - String targetDirPath = testingClient.testing().getExportImportTestDirectory()+ File.separator + "dirExport"; + testingClient.testing().exportImport().setProvider(DirExportProviderFactory.PROVIDER_ID); + String targetDirPath = testingClient.testing().exportImport().getExportImportTestDirectory()+ File.separator + "dirExport"; DirExportProvider.recursiveDeleteDir(new File(targetDirPath)); - testingClient.testing().setDir(targetDirPath); - testingClient.testing().setUsersPerFile(ExportImportConfig.DEFAULT_USERS_PER_FILE); + testingClient.testing().exportImport().setDir(targetDirPath); + testingClient.testing().exportImport().setUsersPerFile(ExportImportConfig.DEFAULT_USERS_PER_FILE); testFullExportImport(); @@ -89,11 +89,13 @@ public class ExportImportTest extends AbstractExportImportTest { @Test public void testDirRealmExportImport() throws Throwable { - testingClient.testing().setProvider(DirExportProviderFactory.PROVIDER_ID); - String targetDirPath = testingClient.testing().getExportImportTestDirectory() + File.separator + "dirRealmExport"; + testingClient.testing() + .exportImport() + .setProvider(DirExportProviderFactory.PROVIDER_ID); + String targetDirPath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "dirRealmExport"; DirExportProvider.recursiveDeleteDir(new File(targetDirPath)); - testingClient.testing().setDir(targetDirPath); - testingClient.testing().setUsersPerFile(3); + testingClient.testing().exportImport().setDir(targetDirPath); + testingClient.testing().exportImport().setUsersPerFile(3); testRealmExportImport(); @@ -104,18 +106,18 @@ public class ExportImportTest extends AbstractExportImportTest { @Test public void testSingleFileFullExportImport() throws Throwable { - testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); - String targetFilePath = testingClient.testing().getExportImportTestDirectory() + File.separator + "singleFile-full.json"; - testingClient.testing().setFile(targetFilePath); + testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); + String targetFilePath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "singleFile-full.json"; + testingClient.testing().exportImport().setFile(targetFilePath); testFullExportImport(); } @Test public void testSingleFileRealmExportImport() throws Throwable { - testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); - String targetFilePath = testingClient.testing().getExportImportTestDirectory() + File.separator + "singleFile-realm.json"; - testingClient.testing().setFile(targetFilePath); + testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); + String targetFilePath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "singleFile-realm.json"; + testingClient.testing().exportImport().setFile(targetFilePath); testRealmExportImport(); } @@ -126,14 +128,14 @@ public class ExportImportTest extends AbstractExportImportTest { removeRealm("test-realm"); // Set the realm, which doesn't have builtin clients/roles inside JSON - testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); + testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); URL url = ExportImportTest.class.getResource("/model/testrealm.json"); String targetFilePath = new File(url.getFile()).getAbsolutePath(); - testingClient.testing().setFile(targetFilePath); + testingClient.testing().exportImport().setFile(targetFilePath); - testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT); - testingClient.testing().runImport(); + testingClient.testing().exportImport().runImport(); RealmResource testRealmRealm = adminClient.realm("test-realm"); @@ -158,14 +160,14 @@ public class ExportImportTest extends AbstractExportImportTest { realm.components().add(component); - testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); + testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); - String targetFilePath = testingClient.testing().getExportImportTestDirectory() + File.separator + "singleFile-realm.json"; - testingClient.testing().setFile(targetFilePath); - testingClient.testing().setAction(ExportImportConfig.ACTION_EXPORT); - testingClient.testing().setRealmName("component-realm"); + String targetFilePath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "singleFile-realm.json"; + testingClient.testing().exportImport().setFile(targetFilePath); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT); + testingClient.testing().exportImport().setRealmName("component-realm"); - testingClient.testing().runExport(); + testingClient.testing().exportImport().runExport(); // Delete some realm (and some data in admin realm) adminClient.realm("component-realm").remove(); @@ -173,9 +175,9 @@ public class ExportImportTest extends AbstractExportImportTest { Assert.assertEquals(3, adminClient.realms().findAll().size()); // Configure import - testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT); - testingClient.testing().runImport(); + testingClient.testing().exportImport().runImport(); realmRep = realm.toRepresentation(); @@ -203,10 +205,10 @@ public class ExportImportTest extends AbstractExportImportTest { } private void testFullExportImport() throws LifecycleException { - testingClient.testing().setAction(ExportImportConfig.ACTION_EXPORT); - testingClient.testing().setRealmName(""); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT); + testingClient.testing().exportImport().setRealmName(""); - testingClient.testing().runExport(); + testingClient.testing().exportImport().runExport(); removeRealm("test"); removeRealm("test-realm"); @@ -218,9 +220,9 @@ public class ExportImportTest extends AbstractExportImportTest { assertNotAuthenticated("test", "user3", "password"); // Configure import - testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT); - testingClient.testing().runImport(); + testingClient.testing().exportImport().runImport(); // Ensure data are imported back Assert.assertEquals(3, adminClient.realms().findAll().size()); @@ -232,10 +234,10 @@ public class ExportImportTest extends AbstractExportImportTest { } private void testRealmExportImport() throws LifecycleException { - testingClient.testing().setAction(ExportImportConfig.ACTION_EXPORT); - testingClient.testing().setRealmName("test"); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT); + testingClient.testing().exportImport().setRealmName("test"); - testingClient.testing().runExport(); + testingClient.testing().exportImport().runExport(); // Delete some realm (and some data in admin realm) adminClient.realm("test").remove(); @@ -248,9 +250,9 @@ public class ExportImportTest extends AbstractExportImportTest { assertNotAuthenticated("test", "user3", "password"); // Configure import - testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT); - testingClient.testing().runImport(); + testingClient.testing().exportImport().runImport(); // Ensure data are imported back, but just for "test" realm Assert.assertEquals(3, adminClient.realms().findAll().size()); @@ -273,27 +275,4 @@ public class ExportImportTest extends AbstractExportImportTest { Assert.assertEquals(expectedResult, testingClient.testing().validCredentials(realmName, username, password)); } - private static String getExportImportTestDirectory() { - String dirPath = null; - String relativeDirExportImportPath = "testsuite" + File.separator + - "integration-arquillian" + File.separator + - "tests" + File.separator + - "base" + File.separator + - "target" + File.separator + - "export-import"; - - if (System.getProperties().containsKey("maven.home")) { - dirPath = System.getProperty("user.dir").replaceFirst("testsuite.integration.*", Matcher.quoteReplacement(relativeDirExportImportPath)); - } else { - for (String c : System.getProperty("java.class.path").split(File.pathSeparator)) { - if (c.contains(File.separator + "testsuite" + File.separator + "integration-arquillian" + File.separator)) { - dirPath = c.replaceFirst("testsuite.integration-arquillian.*", Matcher.quoteReplacement(relativeDirExportImportPath)); - } - } - } - - String absolutePath = new File(dirPath).getAbsolutePath(); - return absolutePath; - } - } From a24a43c4beb3bcbc0988f715bb722ddff3a45493 Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 1 Sep 2016 11:21:57 +0200 Subject: [PATCH 2/2] KEYCLOAK-3349 Support for 'request' and 'request_uri' parameters --- .../AbstractIdentityProviderFactory.java | 2 +- .../provider/IdentityProviderFactory.java | 4 +- .../client/JWTClientAuthenticator.java | 26 +-- .../KeycloakOIDCIdentityProviderFactory.java | 5 +- .../oidc/OIDCIdentityProviderFactory.java | 13 +- .../saml/SAMLIdentityProviderFactory.java | 3 +- .../oidc/OIDCAdvancedConfigWrapper.java | 12 ++ .../protocol/oidc/OIDCLoginProtocol.java | 1 + .../protocol/oidc/OIDCWellKnownProvider.java | 7 +- .../oidc/endpoints/AuthorizationEndpoint.java | 156 ++++-------------- .../request/AuthorizationEndpointRequest.java | 88 ++++++++++ ...izationEndpointRequestParserProcessor.java | 73 ++++++++ .../AuthzEndpointQueryStringParser.java | 52 ++++++ .../AuthzEndpointRequestObjectParser.java | 88 ++++++++++ .../request/AuthzEndpointRequestParser.java | 122 ++++++++++++++ .../OIDCConfigurationRepresentation.java | 11 ++ .../protocol/oidc/utils/JWKSUtils.java | 10 +- .../org/keycloak/services/ServicesLogger.java | 4 + .../oidc/DescriptionConverter.java | 31 ++-- .../admin/IdentityProvidersResource.java | 4 +- .../services/util/CertificateInfoHelper.java | 37 +++++ .../rest/TestApplicationResourceProvider.java | 11 +- ...estApplicationResourceProviderFactory.java | 27 ++- .../rest/TestingResourceProvider.java | 15 +- ...stingOIDCEndpointsApplicationResource.java | 137 +++++++++++++++ .../resources/TestApplicationResource.java | 4 + .../TestApplicationResourceUrls.java | 48 ++++++ .../TestOIDCEndpointsApplicationResource.java | 58 +++++++ .../keycloak/testsuite/util/OAuthClient.java | 23 +++ .../client/OIDCClientRegistrationTest.java | 29 +++- .../oidc/OIDCAdvancedRequestParamsTest.java | 116 +++++++++++-- .../oidc/OIDCWellKnownProviderTest.java | 5 +- .../messages/admin-messages_en.properties | 2 + .../admin/resources/js/controllers/clients.js | 35 ++-- .../resources/partials/client-detail.html | 13 ++ 35 files changed, 1053 insertions(+), 219 deletions(-) create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java diff --git a/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java b/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java index e8be6d6192..953797b51c 100755 --- a/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java @@ -50,7 +50,7 @@ public abstract class AbstractIdentityProviderFactory parseConfig(InputStream inputStream) { + public Map parseConfig(KeycloakSession session, InputStream inputStream) { return new HashMap(); } } diff --git a/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java b/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java index fba8e6431f..93ef720fad 100755 --- a/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java @@ -17,6 +17,7 @@ package org.keycloak.broker.provider; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.provider.ProviderFactory; import java.io.InputStream; @@ -47,8 +48,9 @@ public interface IdentityProviderFactory extends Pro *

Creates an {@link IdentityProvider} based on the configuration from * inputStream.

* + * @param session * @param inputStream The input stream from where configuration will be loaded from.. * @return */ - Map parseConfig(InputStream inputStream); + Map parseConfig(KeycloakSession session, InputStream inputStream); } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java index 789d38111c..86dd4e8c74 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java @@ -40,6 +40,7 @@ import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; @@ -166,30 +167,13 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator { } protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context) { - CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, ATTR_PREFIX); - - String encodedCertificate = certInfo.getCertificate(); - String encodedPublicKey = certInfo.getPublicKey(); - - if (encodedCertificate == null && encodedPublicKey == null) { - Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' doesn't have certificate or publicKey configured"); + try { + return CertificateInfoHelper.getSignatureValidationKey(client, ATTR_PREFIX); + } catch (ModelException me) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", me.getMessage()); context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse); return null; } - - if (encodedCertificate != null && encodedPublicKey != null) { - Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' has both publicKey and certificate configured"); - context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse); - return null; - } - - // TODO: Caching of publicKeys / certificates, so it doesn't need to be always computed from pem. For performance reasons... - if (encodedCertificate != null) { - X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate); - return clientCert.getPublicKey(); - } else { - return KeycloakModelUtils.getPublicKey(encodedPublicKey); - } } @Override diff --git a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java index d3e8eba5aa..56f24a2c58 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java @@ -18,6 +18,7 @@ package org.keycloak.broker.oidc; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; import java.io.InputStream; import java.util.Map; @@ -45,8 +46,8 @@ public class KeycloakOIDCIdentityProviderFactory extends AbstractIdentityProvide } @Override - public Map parseConfig(InputStream inputStream) { - return OIDCIdentityProviderFactory.parseOIDCConfig(inputStream); + public Map parseConfig(KeycloakSession session, InputStream inputStream) { + return OIDCIdentityProviderFactory.parseOIDCConfig(session, inputStream); } diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java index 82c2cdd966..a0e5017504 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java @@ -19,6 +19,7 @@ package org.keycloak.broker.oidc; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.jose.jwk.JWK; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; @@ -56,11 +57,11 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory } @Override - public Map parseConfig(InputStream inputStream) { - return parseOIDCConfig(inputStream); + public Map parseConfig(KeycloakSession session, InputStream inputStream) { + return parseOIDCConfig(session, inputStream); } - protected static Map parseOIDCConfig(InputStream inputStream) { + protected static Map parseOIDCConfig(KeycloakSession session, InputStream inputStream) { OIDCConfigurationRepresentation rep; try { rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class); @@ -74,14 +75,14 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory config.setTokenUrl(rep.getTokenEndpoint()); config.setUserInfoUrl(rep.getUserinfoEndpoint()); if (rep.getJwksUri() != null) { - sendJwksRequest(rep, config); + sendJwksRequest(session, rep, config); } return config.getConfig(); } - protected static void sendJwksRequest(OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) { + protected static void sendJwksRequest(KeycloakSession session, OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) { try { - JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(rep.getJwksUri()); + JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(session, rep.getJwksUri()); PublicKey key = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG); if (key == null) { logger.supportedJwkNotFound(JWK.Use.SIG.asString()); diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java index dac6750719..261799524e 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java @@ -24,6 +24,7 @@ import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType; import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType; import org.keycloak.dom.saml.v2.metadata.KeyTypes; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.util.DocumentUtil; @@ -54,7 +55,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory } @Override - public Map parseConfig(InputStream inputStream) { + public Map parseConfig(KeycloakSession session, InputStream inputStream) { try { Object parsedObject = new SAMLParser().parse(inputStream); EntityDescriptorType entityType; 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 37fe2043d6..3233690493 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java @@ -30,6 +30,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 final ClientModel clientModel; private final ClientRepresentation clientRep; @@ -62,6 +64,16 @@ public class OIDCAdvancedConfigWrapper { return getUserInfoSignedResponseAlg() != null; } + public Algorithm getRequestObjectSignatureAlg() { + String alg = getAttribute(REQUEST_OBJECT_SIGNATURE_ALG); + return alg==null ? null : Enum.valueOf(Algorithm.class, alg); + } + + public void setRequestObjectSignatureAlg(Algorithm alg) { + String algStr = alg==null ? null : alg.toString(); + setAttribute(REQUEST_OBJECT_SIGNATURE_ALG, algStr); + } + private String getAttribute(String attrKey) { if (clientModel != null) { 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 3c4c3aa650..c263405213 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -66,6 +66,7 @@ public class OIDCLoginProtocol implements LoginProtocol { public static final String REQUEST_PARAM = "request"; public static final String REQUEST_URI_PARAM = "request_uri"; public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM; + public static final String CLAIMS_PARAM = "claims"; public static final String LOGOUT_REDIRECT_URI = "OIDC_LOGOUT_REDIRECT_URI"; public static final String ISSUER = "iss"; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index dfca14434f..800630c2c2 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -50,6 +50,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider { public static final List DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString()); + public static final List DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.none.toString(), Algorithm.RS256.toString()); + public static final List DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS); public static final List DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE, OIDCResponseType.NONE, OIDCResponseType.ID_TOKEN, OIDCResponseType.TOKEN, "id_token token", "code id_token", "code token", "code id_token token"); @@ -93,6 +95,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED); config.setUserInfoSigningAlgValuesSupported(DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED); + config.setRequestObjectSigningAlgValuesSupported(DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED); config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED); config.setSubjectTypesSupported(DEFAULT_SUBJECT_TYPES_SUPPORTED); config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED); @@ -107,8 +110,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setScopesSupported(SCOPES_SUPPORTED); - config.setRequestParameterSupported(false); - config.setRequestUriParameterSupported(false); + config.setRequestParameterSupported(true); + config.setRequestUriParameterSupported(true); return config; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index 931b04cfba..1b4db2951a 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -17,11 +17,6 @@ package org.keycloak.protocol.oidc.endpoints; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - import javax.ws.rs.GET; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; @@ -42,6 +37,8 @@ import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.RealmModel; import org.keycloak.protocol.AuthorizationEndpointBase; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest; +import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor; import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder; import org.keycloak.protocol.oidc.utils.OIDCResponseMode; import org.keycloak.protocol.oidc.utils.OIDCResponseType; @@ -67,43 +64,10 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { /** * Prefix used to store additional HTTP GET params from original client request into {@link ClientSessionModel} note to be available later in Authenticators, RequiredActions etc. Prefix is used to * prevent collisions with internally used notes. - * + * * @see ClientSessionModel#getNote(String) - * @see #KNOWN_REQ_PARAMS - * @see #additionalReqParams */ public static final String CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX = "client_request_param_"; - /** - * Max number of additional req params copied into client session note to prevent DoS attacks - * - * @see #additionalReqParams - */ - public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5; - /** - * Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored - * - * @see #additionalReqParams - */ - public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200; - - /** Set of known protocol GET params not to be stored into {@link #additionalReqParams} */ - private static final Set KNOWN_REQ_PARAMS = new HashSet<>(); - static { - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_MODE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REDIRECT_URI_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.STATE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.SCOPE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.LOGIN_HINT_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.PROMPT_PARAM); - KNOWN_REQ_PARAMS.add(AdapterConstants.KC_IDP_HINT); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.NONCE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.MAX_AGE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM); - } private enum Action { REGISTER, CODE, FORGOT_CREDENTIALS @@ -116,19 +80,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { private OIDCResponseType parsedResponseType; private OIDCResponseMode parsedResponseMode; - private String clientId; + private AuthorizationEndpointRequest request; private String redirectUri; - private String redirectUriParam; - private String responseType; - private String responseMode; - private String state; - private String scope; - private String loginHint; - private String prompt; - private String nonce; - private String maxAge; - private String idpHint; - protected Map additionalReqParams = new HashMap<>(); public AuthorizationEndpoint(RealmModel realm, EventBuilder event) { super(realm, event); @@ -139,34 +92,25 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { public Response build() { MultivaluedMap params = uriInfo.getQueryParameters(); - clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM); - responseType = params.getFirst(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); - responseMode = params.getFirst(OIDCLoginProtocol.RESPONSE_MODE_PARAM); - redirectUriParam = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM); - state = params.getFirst(OIDCLoginProtocol.STATE_PARAM); - scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM); - loginHint = params.getFirst(OIDCLoginProtocol.LOGIN_HINT_PARAM); - prompt = params.getFirst(OIDCLoginProtocol.PROMPT_PARAM); - idpHint = params.getFirst(AdapterConstants.KC_IDP_HINT); - nonce = params.getFirst(OIDCLoginProtocol.NONCE_PARAM); - maxAge = params.getFirst(OIDCLoginProtocol.MAX_AGE_PARAM); - - extractAdditionalReqParams(params); + String clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM); checkSsl(); checkRealm(); - checkClient(); + checkClient(clientId); + + request = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params); + checkRedirectUri(); Response errorResponse = checkResponseType(); if (errorResponse != null) { return errorResponse; } - if (!TokenUtil.isOIDCRequest(scope)) { + if (!TokenUtil.isOIDCRequest(request.getScope())) { logger.oidcScopeMissing(); } - errorResponse = checkOIDCParams(params); + errorResponse = checkOIDCParams(); if (errorResponse != null) { return errorResponse; } @@ -186,27 +130,6 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { throw new RuntimeException("Unknown action " + action); } - protected void extractAdditionalReqParams(MultivaluedMap params) { - for (String paramName : params.keySet()) { - if (!KNOWN_REQ_PARAMS.contains(paramName)) { - String value = params.getFirst(paramName); - if (value != null && value.trim().isEmpty()) { - value = null; - } - if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) { - if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) { - logger.debug("Maximal number of additional OIDC params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!"); - break; - } - additionalReqParams.put(paramName, value); - } else { - logger.debug("OIDC Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE); - } - } - - } - } - public AuthorizationEndpoint register() { event.event(EventType.REGISTER); action = Action.REGISTER; @@ -243,7 +166,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { } } - private void checkClient() { + private void checkClient(String clientId) { if (clientId == null) { event.error(Errors.INVALID_REQUEST); throw new ErrorPageException(session, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM); @@ -271,6 +194,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { } private Response checkResponseType() { + String responseType = request.getResponseType(); + if (responseType == null) { logger.missingParameter(OAuth2Constants.RESPONSE_TYPE); event.error(Errors.INVALID_REQUEST); @@ -292,7 +217,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { OIDCResponseMode parsedResponseMode = null; try { - parsedResponseMode = OIDCResponseMode.parse(responseMode, parsedResponseType); + parsedResponseMode = OIDCResponseMode.parse(request.getResponseMode(), parsedResponseType); } catch (IllegalArgumentException iae) { logger.invalidParameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM); event.error(Errors.INVALID_REQUEST); @@ -325,20 +250,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { return null; } - private Response checkOIDCParams(MultivaluedMap params) { - if (params.getFirst(OIDCLoginProtocol.REQUEST_PARAM) != null) { - logger.unsupportedParameter(OIDCLoginProtocol.REQUEST_PARAM); - event.error(Errors.INVALID_REQUEST); - return redirectErrorToClient(parsedResponseMode, OAuthErrorException.REQUEST_NOT_SUPPORTED, null); - } - - if (params.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM) != null) { - logger.unsupportedParameter(OIDCLoginProtocol.REQUEST_URI_PARAM); - event.error(Errors.INVALID_REQUEST); - return redirectErrorToClient(parsedResponseMode, OAuthErrorException.REQUEST_URI_NOT_SUPPORTED, null); - } - - if (parsedResponseType.isImplicitOrHybridFlow() && nonce == null) { + private Response checkOIDCParams() { + if (parsedResponseType.isImplicitOrHybridFlow() && request.getNonce() == null) { logger.missingParameter(OIDCLoginProtocol.NONCE_PARAM); event.error(Errors.INVALID_REQUEST); return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: nonce"); @@ -355,14 +268,16 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { errorResponseBuilder.addParam(OAuth2Constants.ERROR_DESCRIPTION, errorDescription); } - if (state != null) { - errorResponseBuilder.addParam(OAuth2Constants.STATE, state); + if (request.getState() != null) { + errorResponseBuilder.addParam(OAuth2Constants.STATE, request.getState()); } return errorResponseBuilder.build(); } private void checkRedirectUri() { + String redirectUriParam = request.getRedirectUriParam(); + event.detail(Details.REDIRECT_URI, redirectUriParam); redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client); @@ -377,27 +292,28 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); clientSession.setRedirectUri(redirectUri); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); - clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, responseType); - clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUriParam); + clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, request.getResponseType()); + clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam()); clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); - if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); - if (nonce != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, nonce); - if (maxAge != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge); - if (scope != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope); - if (loginHint != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint); - if (prompt != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, prompt); - if (idpHint != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, idpHint); - if (responseMode != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, responseMode); + if (request.getState() != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, request.getState()); + if (request.getNonce() != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce()); + if (request.getMaxAge() != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge())); + if (request.getScope() != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope()); + if (request.getLoginHint() != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint()); + if (request.getPrompt() != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt()); + if (request.getIdpHint() != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint()); + if (request.getResponseMode() != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode()); - if (additionalReqParams != null) { - for (String paramName : additionalReqParams.keySet()) { - clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, additionalReqParams.get(paramName)); + if (request.getAdditionalReqParams() != null) { + for (String paramName : request.getAdditionalReqParams().keySet()) { + clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName)); } } } private Response buildAuthorizationCodeAuthorizationResponse() { + String idpHint = request.getIdpHint(); if (idpHint != null && !"".equals(idpHint)) { IdentityProviderModel identityProviderModel = realm.getIdentityProviderByAlias(idpHint); @@ -413,7 +329,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { this.event.event(EventType.LOGIN); clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE); - return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(prompt, OIDCLoginProtocol.PROMPT_VALUE_NONE), false); + return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(request.getPrompt(), OIDCLoginProtocol.PROMPT_VALUE_NONE), false); } private Response buildRegister() { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java new file mode 100644 index 0000000000..998a58ca45 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.protocol.oidc.endpoints.request; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Marek Posolda + */ +public class AuthorizationEndpointRequest { + + String clientId; + String redirectUriParam; + String responseType; + String responseMode; + String state; + String scope; + String loginHint; + String prompt; + String nonce; + Integer maxAge; + String idpHint; + Map additionalReqParams = new HashMap<>(); + + public String getClientId() { + return clientId; + } + + public String getRedirectUriParam() { + return redirectUriParam; + } + + public String getResponseType() { + return responseType; + } + + public String getResponseMode() { + return responseMode; + } + + public String getState() { + return state; + } + + public String getScope() { + return scope; + } + + public String getLoginHint() { + return loginHint; + } + + public String getPrompt() { + return prompt; + } + + public String getNonce() { + return nonce; + } + + public Integer getMaxAge() { + return maxAge; + } + + public String getIdpHint() { + return idpHint; + } + + public Map getAdditionalReqParams() { + return additionalReqParams; + } +} 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 new file mode 100644 index 0000000000..8db219c08f --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.protocol.oidc.endpoints.request; + +import java.io.InputStream; +import java.util.Map; + +import javax.ws.rs.core.MultivaluedMap; + +import org.keycloak.common.util.StreamUtil; +import org.keycloak.connections.httpclient.HttpClientProvider; +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.OIDCLoginProtocol; +import org.keycloak.services.ErrorPageException; +import org.keycloak.services.ServicesLogger; +import org.keycloak.services.messages.Messages; + +/** + * @author Marek Posolda + */ +public class AuthorizationEndpointRequestParserProcessor { + + private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER; + + public static AuthorizationEndpointRequest parseRequest(EventBuilder event, KeycloakSession session, ClientModel client, MultivaluedMap requestParams) { + try { + AuthorizationEndpointRequest request = new AuthorizationEndpointRequest(); + + new AuthzEndpointQueryStringParser(requestParams).parseRequest(request); + + String requestParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_PARAM); + String requestUriParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM); + + if (requestParam != null && requestUriParam != null) { + throw new RuntimeException("Illegal to use both 'request' and 'request_uri' parameters together"); + } + + if (requestParam != null) { + new AuthzEndpointRequestObjectParser(requestParam, client).parseRequest(request); + } else if (requestUriParam != null) { + InputStream is = session.getProvider(HttpClientProvider.class).get(requestUriParam); + String retrievedRequest = StreamUtil.readString(is); + + new AuthzEndpointRequestObjectParser(retrievedRequest, client).parseRequest(request); + } + + return request; + + } catch (Exception e) { + logger.invalidRequest(e); + event.error(Errors.INVALID_REQUEST); + throw new ErrorPageException(session, Messages.INVALID_REQUEST); + } + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java new file mode 100644 index 0000000000..8384fdcdcb --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java @@ -0,0 +1,52 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.protocol.oidc.endpoints.request; + +import java.util.Set; + +import javax.ws.rs.core.MultivaluedMap; + +/** + * Parse the parameters from request queryString + * + * @author Marek Posolda + */ +class AuthzEndpointQueryStringParser extends AuthzEndpointRequestParser { + + private final MultivaluedMap requestParams; + + public AuthzEndpointQueryStringParser(MultivaluedMap requestParams) { + this.requestParams = requestParams; + } + + @Override + protected String getParameter(String paramName) { + return requestParams.getFirst(paramName); + } + + @Override + protected Integer getIntParameter(String paramName) { + String paramVal = requestParams.getFirst(paramName); + return paramVal==null ? null : Integer.parseInt(paramVal); + } + + @Override + protected Set keySet() { + return requestParams.keySet(); + } +} 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 new file mode 100644 index 0000000000..658a2c002e --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java @@ -0,0 +1,88 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.protocol.oidc.endpoints.request; + +import java.security.PublicKey; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSHeader; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.ClientModel; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; +import org.keycloak.services.util.CertificateInfoHelper; +import org.keycloak.util.JsonSerialization; + +/** + * Parse the parameters from OIDC "request" object + * + * @author Marek Posolda + */ +class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { + + private final Map requestParams; + + public AuthzEndpointRequestObjectParser(String requestObject, ClientModel client) throws Exception { + JWSInput input = new JWSInput(requestObject); + JWSHeader header = input.getHeader(); + + Algorithm requestedSignatureAlgorithm = OIDCAdvancedConfigWrapper.fromClientModel(client).getRequestObjectSignatureAlg(); + + if (requestedSignatureAlgorithm != null && requestedSignatureAlgorithm != header.getAlgorithm()) { + throw new RuntimeException("Request object signed with different algorithm than client requested algorithm"); + } + + if (header.getAlgorithm() == Algorithm.none) { + this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class); + } else if (header.getAlgorithm() == Algorithm.RS256) { + PublicKey clientPublicKey = CertificateInfoHelper.getSignatureValidationKey(client, JWTClientAuthenticator.ATTR_PREFIX); + boolean verified = RSAProvider.verify(input, clientPublicKey); + if (!verified) { + throw new RuntimeException("Failed to verify signature on 'request' object"); + } + + this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class); + } else { + throw new RuntimeException("Unsupported JWA algorithm used for signed request"); + } + } + + @Override + protected String getParameter(String paramName) { + Object val = this.requestParams.get(paramName); + return val==null ? null : val.toString(); + } + + @Override + protected Integer getIntParameter(String paramName) { + Object val = this.requestParams.get(paramName); + return val==null ? null : Integer.parseInt(getParameter(paramName)); + } + + @Override + protected Set keySet() { + return requestParams.keySet(); + } + + static class TypedHashMap extends HashMap { + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java new file mode 100644 index 0000000000..e322d4b75e --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java @@ -0,0 +1,122 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.protocol.oidc.endpoints.request; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.keycloak.constants.AdapterConstants; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.services.ServicesLogger; + +/** + * @author Marek Posolda + */ +abstract class AuthzEndpointRequestParser { + + private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER; + + /** + * Max number of additional req params copied into client session note to prevent DoS attacks + * + */ + public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5; + + /** + * Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored + * + */ + public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200; + + /** Set of known protocol GET params not to be stored into additionalReqParams} */ + private static final Set KNOWN_REQ_PARAMS = new HashSet<>(); + static { + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_MODE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REDIRECT_URI_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.STATE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.SCOPE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.LOGIN_HINT_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.PROMPT_PARAM); + KNOWN_REQ_PARAMS.add(AdapterConstants.KC_IDP_HINT); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.NONCE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.MAX_AGE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM); + } + + + public void parseRequest(AuthorizationEndpointRequest request) { + String clientId = getParameter(OIDCLoginProtocol.CLIENT_ID_PARAM); + + if (request.clientId != null && !request.clientId.equals(clientId)) { + throw new IllegalArgumentException("The client_id parameter doesn't match the one from OIDC 'request' or 'request_uri'"); + } + + request.clientId = clientId; + request.responseType = replaceIfNotNull(request.responseType, getParameter(OIDCLoginProtocol.RESPONSE_TYPE_PARAM)); + request.responseMode = replaceIfNotNull(request.responseMode, getParameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM)); + request.redirectUriParam = replaceIfNotNull(request.redirectUriParam, getParameter(OIDCLoginProtocol.REDIRECT_URI_PARAM)); + request.state = replaceIfNotNull(request.state, getParameter(OIDCLoginProtocol.STATE_PARAM)); + request.scope = replaceIfNotNull(request.scope, getParameter(OIDCLoginProtocol.SCOPE_PARAM)); + request.loginHint = replaceIfNotNull(request.loginHint, getParameter(OIDCLoginProtocol.LOGIN_HINT_PARAM)); + request.prompt = replaceIfNotNull(request.prompt, getParameter(OIDCLoginProtocol.PROMPT_PARAM)); + request.idpHint = replaceIfNotNull(request.idpHint, getParameter(AdapterConstants.KC_IDP_HINT)); + request.nonce = replaceIfNotNull(request.nonce, getParameter(OIDCLoginProtocol.NONCE_PARAM)); + request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM)); + + extractAdditionalReqParams(request.additionalReqParams); + } + + + protected void extractAdditionalReqParams(Map additionalReqParams) { + for (String paramName : keySet()) { + if (!KNOWN_REQ_PARAMS.contains(paramName)) { + String value = getParameter(paramName); + if (value != null && value.trim().isEmpty()) { + value = null; + } + if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) { + if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) { + logger.debug("Maximal number of additional OIDC params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!"); + break; + } + additionalReqParams.put(paramName, value); + } else { + logger.debug("OIDC Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE); + } + } + + } + } + + protected T replaceIfNotNull(T previousVal, T newVal) { + return newVal==null ? previousVal : newVal; + } + + + protected abstract String getParameter(String paramName); + + protected abstract Integer getIntParameter(String paramName); + + protected abstract Set keySet(); + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java index ee5241baa0..181e0d28df 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -67,6 +67,9 @@ public class OIDCConfigurationRepresentation { @JsonProperty("userinfo_signing_alg_values_supported") private List userInfoSigningAlgValuesSupported; + @JsonProperty("request_object_signing_alg_values_supported") + private List requestObjectSigningAlgValuesSupported; + @JsonProperty("response_modes_supported") private List responseModesSupported; @@ -195,6 +198,14 @@ public class OIDCConfigurationRepresentation { this.userInfoSigningAlgValuesSupported = userInfoSigningAlgValuesSupported; } + public List getRequestObjectSigningAlgValuesSupported() { + return requestObjectSigningAlgValuesSupported; + } + + public void setRequestObjectSigningAlgValuesSupported(List requestObjectSigningAlgValuesSupported) { + this.requestObjectSigningAlgValuesSupported = requestObjectSigningAlgValuesSupported; + } + public List getResponseModesSupported() { return responseModesSupported; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java index c856a81cef..d8f7fe72de 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java @@ -18,12 +18,15 @@ package org.keycloak.protocol.oidc.utils; import java.io.IOException; +import java.io.InputStream; import java.security.PublicKey; -import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.common.util.StreamUtil; +import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWKParser; +import org.keycloak.models.KeycloakSession; import org.keycloak.util.JsonSerialization; /** @@ -31,8 +34,9 @@ import org.keycloak.util.JsonSerialization; */ public class JWKSUtils { - public static JSONWebKeySet sendJwksRequest(String jwksURI) throws IOException { - String keySetString = SimpleHttp.doGet(jwksURI).asString(); + public static JSONWebKeySet sendJwksRequest(KeycloakSession session, String jwksURI) throws IOException { + InputStream is = session.getProvider(HttpClientProvider.class).get(jwksURI); + String keySetString = StreamUtil.readString(is); return JsonSerialization.readValue(keySetString, JSONWebKeySet.class); } diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java index de7eef743f..af6c4c13e5 100644 --- a/services/src/main/java/org/keycloak/services/ServicesLogger.java +++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java @@ -430,4 +430,8 @@ public interface ServicesLogger extends BasicLogger { @Message(id=96, value="Not found JWK of supported keyType under jwks_uri for usage: %s") void supportedJwkNotFound(String usage); + @LogMessage(level = WARN) + @Message(id=97, value="Invalid request") + void invalidRequest(@Cause Throwable t); + } 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 a95cc090d2..e1939ef0c8 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 @@ -89,14 +89,12 @@ public class DescriptionConverter { } client.setClientAuthenticatorType(clientAuthFactory.getId()); - // Externalize to ClientAuthenticator itself? - if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT)) { - - PublicKey publicKey = retrievePublicKey(clientOIDC); - if (publicKey == null) { - throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString()); - } + PublicKey publicKey = retrievePublicKey(session, clientOIDC); + if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT) && publicKey == null) { + throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString()); + } + if (publicKey != null) { String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey); CertificateRepresentation rep = new CertificateRepresentation(); @@ -104,20 +102,24 @@ public class DescriptionConverter { CertificateInfoHelper.updateClientRepresentationCertificateInfo(client, rep, JWTClientAuthenticator.ATTR_PREFIX); } + OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client); if (clientOIDC.getUserinfoSignedResponseAlg() != null) { - String userInfoSignedResponseAlg = clientOIDC.getUserinfoSignedResponseAlg(); - Algorithm algorithm = Enum.valueOf(Algorithm.class, userInfoSignedResponseAlg); + Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getUserinfoSignedResponseAlg()); + configWrapper.setUserInfoSignedResponseAlg(algorithm); + } - OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setUserInfoSignedResponseAlg(algorithm); + if (clientOIDC.getRequestObjectSigningAlg() != null) { + Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getRequestObjectSigningAlg()); + configWrapper.setRequestObjectSignatureAlg(algorithm); } return client; } - private static PublicKey retrievePublicKey(OIDCClientRepresentation clientOIDC) { + private static PublicKey retrievePublicKey(KeycloakSession session, OIDCClientRepresentation clientOIDC) { if (clientOIDC.getJwksUri() == null && clientOIDC.getJwks() == null) { - throw new ClientRegistrationException("Requested client authentication method '%s' but jwks_uri nor jwks were available in config"); + return null; } if (clientOIDC.getJwksUri() != null && clientOIDC.getJwks() != null) { @@ -129,7 +131,7 @@ public class DescriptionConverter { keySet = clientOIDC.getJwks(); } else { try { - keySet = JWKSUtils.sendJwksRequest(clientOIDC.getJwksUri()); + keySet = JWKSUtils.sendJwksRequest(session, clientOIDC.getJwksUri()); } catch (IOException ioe) { throw new ClientRegistrationException("Failed to send JWKS request to specified jwks_uri", ioe); } @@ -166,6 +168,9 @@ public class DescriptionConverter { if (config.isUserInfoSignatureRequired()) { response.setUserinfoSignedResponseAlg(config.getUserInfoSignedResponseAlg().toString()); } + if (config.getRequestObjectSignatureAlg() != null) { + response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString()); + } return response; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java index 958a8494e0..8e7c9acb69 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java @@ -116,7 +116,7 @@ public class IdentityProvidersResource { InputPart file = formDataMap.get("file").get(0); InputStream inputStream = file.getBody(InputStream.class, null); IdentityProviderFactory providerFactory = getProviderFactorytById(providerId); - Map config = providerFactory.parseConfig(inputStream); + Map config = providerFactory.parseConfig(session, inputStream); return config; } @@ -143,7 +143,7 @@ public class IdentityProvidersResource { try { IdentityProviderFactory providerFactory = getProviderFactorytById(providerId); Map config; - config = providerFactory.parseConfig(inputStream); + config = providerFactory.parseConfig(session, inputStream); return config; } finally { try { diff --git a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java index b309927f81..359d28d84b 100644 --- a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java +++ b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java @@ -17,9 +17,18 @@ package org.keycloak.services.util; +import java.security.PublicKey; +import java.security.cert.X509Certificate; import java.util.HashMap; +import javax.ws.rs.core.Response; + +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.ClientAuthenticationFlowContext; +import org.keycloak.authentication.authenticators.client.ClientAuthUtil; import org.keycloak.models.ClientModel; +import org.keycloak.models.ModelException; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.ClientRepresentation; @@ -34,6 +43,8 @@ public class CertificateInfoHelper { public static final String PUBLIC_KEY = "public.key"; + // CLIENT MODEL METHODS + public static CertificateRepresentation getCertificateFromClient(ClientModel client, String attributePrefix) { String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; @@ -75,6 +86,32 @@ public class CertificateInfoHelper { } + public static PublicKey getSignatureValidationKey(ClientModel client, String attributePrefix) throws ModelException { + CertificateRepresentation certInfo = getCertificateFromClient(client, attributePrefix); + + String encodedCertificate = certInfo.getCertificate(); + String encodedPublicKey = certInfo.getPublicKey(); + + if (encodedCertificate == null && encodedPublicKey == null) { + throw new ModelException("Client doesn't have certificate or publicKey configured"); + } + + if (encodedCertificate != null && encodedPublicKey != null) { + throw new ModelException("Client has both publicKey and certificate configured"); + } + + // TODO: Caching of publicKeys / certificates, so it doesn't need to be always computed from pem. For performance reasons... + if (encodedCertificate != null) { + X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate); + return clientCert.getPublicKey(); + } else { + return KeycloakModelUtils.getPublicKey(encodedPublicKey); + } + } + + + // CLIENT REPRESENTATION METHODS + public static void updateClientRepresentationCertificateInfo(ClientRepresentation client, CertificateRepresentation rep, String attributePrefix) { String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java index bc63c99f70..5f392a0bc4 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java @@ -27,6 +27,8 @@ import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.TestAvailabilityAction; import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resources.RealmsResource; +import org.keycloak.testsuite.rest.resource.TestingExportImportResource; +import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -53,14 +55,16 @@ public class TestApplicationResourceProvider implements RealmResourceProvider { private final BlockingQueue adminLogoutActions; private final BlockingQueue adminPushNotBeforeActions; private final BlockingQueue adminTestAvailabilityAction; + private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData; public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue adminLogoutActions, BlockingQueue adminPushNotBeforeActions, - BlockingQueue adminTestAvailabilityAction) { + BlockingQueue adminTestAvailabilityAction, TestApplicationResourceProviderFactory.OIDCClientData oidcClientData) { this.session = session; this.adminLogoutActions = adminLogoutActions; this.adminPushNotBeforeActions = adminPushNotBeforeActions; this.adminTestAvailabilityAction = adminTestAvailabilityAction; + this.oidcClientData = oidcClientData; } @POST @@ -164,6 +168,11 @@ public class TestApplicationResourceProvider implements RealmResourceProvider { return sb.toString(); } + @Path("/oidc-client-endpoints") + public TestingOIDCEndpointsApplicationResource getTestingOIDCClientEndpoints() { + return new TestingOIDCEndpointsApplicationResource(oidcClientData); + } + @Override public Object getResource() { return this; diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java index 98ca2baa74..6bd7dc262d 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java @@ -28,6 +28,7 @@ import org.keycloak.representations.adapters.action.TestAvailabilityAction; import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resource.RealmResourceProviderFactory; +import java.security.KeyPair; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; @@ -40,9 +41,11 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv private BlockingQueue pushNotBeforeActions = new LinkedBlockingDeque<>(); private BlockingQueue testAvailabilityActions = new LinkedBlockingDeque<>(); + private final OIDCClientData oidcClientData = new OIDCClientData(); + @Override public RealmResourceProvider create(KeycloakSession session) { - return new TestApplicationResourceProvider(session, adminLogoutActions, pushNotBeforeActions, testAvailabilityActions); + return new TestApplicationResourceProvider(session, adminLogoutActions, pushNotBeforeActions, testAvailabilityActions, oidcClientData); } @Override @@ -62,4 +65,26 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv return "app"; } + + public static class OIDCClientData { + + private KeyPair signingKeyPair; + private String oidcRequest; + + public KeyPair getSigningKeyPair() { + return signingKeyPair; + } + + public void setSigningKeyPair(KeyPair signingKeyPair) { + this.signingKeyPair = signingKeyPair; + } + + public String getOidcRequest() { + return oidcRequest; + } + + public void setOidcRequest(String oidcRequest) { + this.oidcRequest = oidcRequest; + } + } } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index 78b7e70432..2085cfac14 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -23,15 +23,20 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import org.infinispan.Cache; +import org.keycloak.OAuth2Constants; import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.events.Event; import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.ResourceType; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest; import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.services.managers.ClientSessionCode; @@ -632,14 +637,14 @@ public class TestingResourceProvider implements RealmResourceProvider { return ModelToRepresentation.toRepresentation(user); } - private RealmModel getRealmByName(String realmName) { - RealmProvider realmProvider = session.getProvider(RealmProvider.class); - return realmProvider.getRealmByName(realmName); - } - @Path("/export-import") public TestingExportImportResource getExportImportResource() { return new TestingExportImportResource(session); } + private RealmModel getRealmByName(String realmName) { + RealmProvider realmProvider = session.getProvider(RealmProvider.class); + return realmProvider.getRealmByName(realmName); + } + } 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 new file mode 100644 index 0000000000..6ea488f5c8 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java @@ -0,0 +1,137 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.testsuite.rest.resource; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.annotations.cache.NoCache; +import org.jboss.resteasy.spi.BadRequestException; +import org.keycloak.OAuth2Constants; +import org.keycloak.jose.jwk.JSONWebKeySet; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKBuilder; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory; + +/** + * @author Marek Posolda + */ +public class TestingOIDCEndpointsApplicationResource { + + public static final String PRIVATE_KEY = "privateKey"; + public static final String PUBLIC_KEY = "publicKey"; + + private final TestApplicationResourceProviderFactory.OIDCClientData clientData; + + public TestingOIDCEndpointsApplicationResource(TestApplicationResourceProviderFactory.OIDCClientData oidcClientData) { + this.clientData = oidcClientData; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/generate-keys") + @NoCache + public Map generateKeys() { + try { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + clientData.setSigningKeyPair(generator.generateKeyPair()); + } catch (NoSuchAlgorithmException e) { + throw new BadRequestException("Error generating signing keypair", e); + } + + String privateKeyPem = KeycloakModelUtils.getPemFromKey(clientData.getSigningKeyPair().getPrivate()); + String publicKeyPem = KeycloakModelUtils.getPemFromKey(clientData.getSigningKeyPair().getPublic()); + + Map res = new HashMap<>(); + res.put(PRIVATE_KEY, privateKeyPem); + res.put(PUBLIC_KEY, publicKeyPem); + return res; + } + + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/get-jwks") + @NoCache + public JSONWebKeySet getJwks() { + JSONWebKeySet keySet = new JSONWebKeySet(); + + if (clientData.getSigningKeyPair() == null) { + keySet.setKeys(new JWK[] {}); + } else { + keySet.setKeys(new JWK[] { JWKBuilder.create().rs256(clientData.getSigningKeyPair().getPublic()) }); + } + + return keySet; + } + + + @GET + @Path("/set-oidc-request") + @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) + @NoCache + public void setOIDCRequest(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId, + @QueryParam("redirectUri") String redirectUri, @QueryParam("maxAge") String maxAge, + @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 (maxAge != null) { + oidcRequest.put(OIDCLoginProtocol.MAX_AGE_PARAM, Integer.parseInt(maxAge)); + } + + Algorithm alg = Enum.valueOf(Algorithm.class, jwaAlgorithm); + if (alg == Algorithm.none) { + clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).none()); + } else if (alg == Algorithm.RS256) { + if (clientData.getSigningKeyPair() == null) { + throw new BadRequestException("Requested RS256, but signing key not set"); + } + + PrivateKey privateKey = clientData.getSigningKeyPair().getPrivate(); + clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).rsa256(privateKey)); + } else { + throw new BadRequestException("Unknown argument: " + jwaAlgorithm); + } + } + + + @GET + @Path("/get-oidc-request") + @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) + @NoCache + public String getOIDCRequest() { + return clientData.getOidcRequest(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java index 2efc26b3f7..2d277fd6e9 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java @@ -20,7 +20,9 @@ package org.keycloak.testsuite.client.resources; import org.keycloak.representations.adapters.action.LogoutAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.TestAvailabilityAction; +import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; +import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -53,4 +55,6 @@ public interface TestApplicationResource { @Path("/clear-admin-actions") Response clearAdminActions(); + @Path("/oidc-client-endpoints") + TestOIDCEndpointsApplicationResource oidcClientEndpoints(); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java new file mode 100644 index 0000000000..8c5f98b6d6 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.testsuite.client.resources; + +import javax.ws.rs.core.UriBuilder; + +import org.keycloak.testsuite.util.OAuthClient; + +/** + * @author Marek Posolda + */ +public class TestApplicationResourceUrls { + + private static UriBuilder oidcClientEndpoints() { + return UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT) + .path(TestApplicationResource.class) + .path(TestApplicationResource.class, "oidcClientEndpoints"); + } + + public static String clientRequestUri() { + UriBuilder builder = oidcClientEndpoints() + .path(TestOIDCEndpointsApplicationResource.class, "getOIDCRequest"); + + return builder.build().toString(); + } + + public static String clientJwksUri() { + UriBuilder builder = oidcClientEndpoints() + .path(TestOIDCEndpointsApplicationResource.class, "getJwks"); + + return builder.build().toString(); + } +} 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 new file mode 100644 index 0000000000..54d6c3573d --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.testsuite.client.resources; + +import java.util.Map; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import org.keycloak.jose.jwk.JSONWebKeySet; + +/** + * @author Marek Posolda + */ +public interface TestOIDCEndpointsApplicationResource { + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/generate-keys") + Map generateKeys(); + + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/get-jwks") + JSONWebKeySet getJwks(); + + + @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("/get-oidc-request") + @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) + String getOIDCRequest(); +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index 586351d1fd..2253828bd5 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -105,6 +105,10 @@ public class OAuthClient { private String nonce; + private String request; + + private String requestUri; + private Map publicKeys = new HashMap<>(); public void init(Keycloak adminClient, WebDriver driver) { @@ -121,6 +125,9 @@ public class OAuthClient { clientSessionState = null; clientSessionHost = null; maxAge = null; + nonce = null; + request = null; + requestUri = null; } public AuthorizationEndpointResponse doLogin(String username, String password) { @@ -536,6 +543,12 @@ public class OAuthClient { if (maxAge != null) { b.queryParam(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge); } + if (request != null) { + b.queryParam(OIDCLoginProtocol.REQUEST_PARAM, request); + } + if (requestUri != null) { + b.queryParam(OIDCLoginProtocol.REQUEST_URI_PARAM, requestUri); + } return b.build(realm).toString(); } @@ -644,6 +657,16 @@ public class OAuthClient { return this; } + public OAuthClient request(String request) { + this.request = request; + return this; + } + + public OAuthClient requestUri(String requestUri) { + this.requestUri = requestUri; + return this; + } + public String getRealm() { return realm; } 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 608d7a73c5..19d64135a3 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 @@ -50,12 +50,16 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.testsuite.Assert; +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.util.OAuthClient; import java.security.PrivateKey; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Map; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; @@ -236,8 +240,11 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS)); clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT); - // Corresponds to PRIVATE_KEY - JSONWebKeySet keySet = loadJson(getClass().getResourceAsStream("/clientreg-test/jwks.json"), JSONWebKeySet.class); + // Generate keys for client + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + Map generatedKeys = oidcClientEndpointsResource.generateKeys(); + + JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks(); clientRep.setJwks(keySet); OIDCClientRepresentation response = reg.oidc().create(clientRep); @@ -246,7 +253,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { Assert.assertNull(response.getClientSecretExpiresAt()); // Tries to authenticate client with privateKey JWT - String signedJwt = getClientSignedJWT(response.getClientId()); + String signedJwt = getClientSignedJWT(response.getClientId(), generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY)); OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt); Assert.assertEquals(200, accessTokenResponse.getStatusCode()); AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken()); @@ -260,8 +267,11 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS)); clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT); - // Use the realmKey for client authentication too - clientRep.setJwksUri(oauth.getCertsUrl(REALM_NAME)); + // Generate keys for client + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + Map generatedKeys = oidcClientEndpointsResource.generateKeys(); + + clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri()); OIDCClientRepresentation response = reg.oidc().create(clientRep); Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod()); @@ -269,7 +279,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { Assert.assertNull(response.getClientSecretExpiresAt()); // Tries to authenticate client with privateKey JWT - String signedJwt = getClientSignedJWT(response.getClientId()); + String signedJwt = getClientSignedJWT(response.getClientId(), generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY)); OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt); Assert.assertEquals(200, accessTokenResponse.getStatusCode()); AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken()); @@ -280,24 +290,27 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { public void testSignaturesRequired() throws Exception { OIDCClientRepresentation clientRep = createRep(); clientRep.setUserinfoSignedResponseAlg(Algorithm.RS256.toString()); + clientRep.setRequestObjectSigningAlg(Algorithm.RS256.toString()); OIDCClientRepresentation response = reg.oidc().create(clientRep); Assert.assertEquals(Algorithm.RS256.toString(), response.getUserinfoSignedResponseAlg()); + Assert.assertEquals(Algorithm.RS256.toString(), response.getRequestObjectSigningAlg()); Assert.assertNotNull(response.getClientSecret()); // Test Keycloak representation ClientRepresentation kcClient = getClient(response.getClientId()); OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient); Assert.assertEquals(config.getUserInfoSignedResponseAlg(), Algorithm.RS256); + Assert.assertEquals(config.getRequestObjectSignatureAlg(), Algorithm.RS256); } // Client auth with signedJWT - helper methods - private String getClientSignedJWT(String clientId) { + private String getClientSignedJWT(String clientId, String privateKeyPem) { String realmInfoUrl = KeycloakUriBuilder.fromUri(getAuthServerRoot()).path(ServiceUrlConstants.REALM_INFO_PATH).build(REALM_NAME).toString(); - PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(PRIVATE_KEY); + PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(privateKeyPem); // Use token-endpoint as audience as OIDC conformance testsuite is using it too. JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider() { 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 191581e600..e01057d418 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 @@ -19,30 +19,42 @@ package org.keycloak.testsuite.oidc; import java.util.List; - import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuthErrorException; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.common.util.Time; import org.keycloak.events.Details; +import org.keycloak.jose.jws.Algorithm; import org.keycloak.models.Constants; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.IDToken; +import org.keycloak.representations.idm.CertificateRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.TestRealmKeycloakTest; import org.keycloak.testsuite.admin.AbstractAdminTest; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; +import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource; import org.keycloak.testsuite.pages.AccountUpdateProfilePage; import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.OAuthGrantPage; +import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -68,6 +80,9 @@ public class OIDCAdvancedRequestParamsTest extends TestRealmKeycloakTest { @Page protected OAuthGrantPage grantPage; + @Page + protected ErrorPage errorPage; + @Override public void configureTestRealm(RealmRepresentation testRealm) { @@ -308,29 +323,98 @@ public class OIDCAdvancedRequestParamsTest extends TestRealmKeycloakTest { // REQUEST & REQUEST_URI @Test - public void requestParam() { - driver.navigate().to(oauth.getLoginFormUrl() + "&request=abc"); + public void requestParamUnsigned() throws Exception { + String validRedirectUri = oauth.getRedirectUri(); + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); - assertFalse(loginPage.isCurrent()); + // Send request object with invalid redirect uri. + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", "http://invalid", null, Algorithm.none.toString()); + String requestStr = oidcClientEndpointsResource.getOIDCRequest(); + + oauth.request(requestStr); + oauth.openLoginForm(); + Assert.assertTrue(errorPage.isCurrent()); + assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); + + // 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()); + requestStr = oidcClientEndpointsResource.getOIDCRequest(); + + oauth.request(requestStr); + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + Assert.assertNotNull(response.getCode()); + Assert.assertEquals("mystate", response.getState()); assertTrue(appPage.isCurrent()); - - // Assert error response was sent because not logged in - OAuthClient.AuthorizationEndpointResponse resp = new OAuthClient.AuthorizationEndpointResponse(oauth); - Assert.assertNull(resp.getCode()); - Assert.assertEquals(OAuthErrorException.REQUEST_NOT_SUPPORTED, resp.getError()); } @Test - public void requestUriParam() { - driver.navigate().to(oauth.getLoginFormUrl() + "&request_uri=https%3A%2F%2Flocalhost%3A60784%2Fexport%2FqzHTG11W48.jwt"); + public void requestUriParamUnsigned() throws Exception { + String validRedirectUri = oauth.getRedirectUri(); + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); - assertFalse(loginPage.isCurrent()); + // Send request object with invalid redirect uri. + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", "http://invalid", null, Algorithm.none.toString()); + + oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); + oauth.openLoginForm(); + Assert.assertTrue(errorPage.isCurrent()); + assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); + + // 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()); + + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + Assert.assertNotNull(response.getCode()); + Assert.assertEquals("mystate", response.getState()); + assertTrue(appPage.isCurrent()); + } + + @Test + public void requestUriParamSigned() throws Exception { + String validRedirectUri = oauth.getRedirectUri(); + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + + // Set required signature for request_uri + ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"); + ClientRepresentation clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectSignatureAlg(Algorithm.RS256); + clientResource.update(clientRep); + + // Verify unsigned request_uri will fail + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString()); + oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); + oauth.openLoginForm(); + Assert.assertTrue(errorPage.isCurrent()); + assertEquals("Invalid Request", errorPage.getError()); + + // Generate keypair for client + String clientPublicKeyPem = oidcClientEndpointsResource.generateKeys().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()); + oauth.openLoginForm(); + Assert.assertTrue(errorPage.isCurrent()); + assertEquals("Invalid Request", errorPage.getError()); + + + // Update clientModel with publicKey for signing + clientRep = clientResource.toRepresentation(); + CertificateRepresentation cert = new CertificateRepresentation(); + cert.setPublicKey(clientPublicKeyPem); + CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, cert, JWTClientAuthenticator.ATTR_PREFIX); + clientResource.update(clientRep); + + // Check signed request_uri will pass + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + Assert.assertNotNull(response.getCode()); + Assert.assertEquals("mystate", response.getState()); assertTrue(appPage.isCurrent()); - // Assert error response was sent because not logged in - OAuthClient.AuthorizationEndpointResponse resp = new OAuthClient.AuthorizationEndpointResponse(oauth); - Assert.assertNull(resp.getCode()); - Assert.assertEquals(OAuthErrorException.REQUEST_URI_NOT_SUPPORTED, resp.getError()); + // Revert requiring signature for client + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectSignatureAlg(null); + clientResource.update(clientRep); } // LOGIN_HINT diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java index 91ccc7e691..7fea65748b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java @@ -87,6 +87,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "public"); Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.RS256.toString()); Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), Algorithm.RS256.toString()); + Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), Algorithm.none.toString(), Algorithm.RS256.toString()); // Client authentication Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt"); @@ -101,8 +102,8 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { Assert.assertNames(oidcConfig.getScopesSupported(), OAuth2Constants.SCOPE_OPENID, OAuth2Constants.OFFLINE_ACCESS); // Request and Request_Uri - Assert.assertFalse(oidcConfig.getRequestParameterSupported()); - Assert.assertFalse(oidcConfig.getRequestUriParameterSupported()); + Assert.assertTrue(oidcConfig.getRequestParameterSupported()); + Assert.assertTrue(oidcConfig.getRequestUriParameterSupported()); } finally { client.close(); } 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 4f15d6d51e..356cbb2c5f 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 @@ -240,6 +240,8 @@ fine-oidc-endpoint-conf=Fine Grain OpenID Connect Configuration fine-oidc-endpoint-conf.tooltip=Expand this section to configure advanced settings of this client related to OpenID Connect protocol 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' ). 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 24efd88e68..3bbd471758 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 @@ -797,6 +797,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, "RS256" ]; + $scope.requestObjectSignatureAlgorithms = [ + "any", + "none", + "RS256" + ]; + $scope.realm = realm; $scope.samlAuthnStatement = false; $scope.samlMultiValuedRoles = false; @@ -898,7 +904,11 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, } } - $scope.userInfoSignedResponseAlg = getSignatureAlgorithm('user.info.response'); + var attrVal1 = $scope.client.attributes['user.info.response.signature.alg']; + $scope.userInfoSignedResponseAlg = attrVal1==null ? 'unsigned' : attrVal1; + + var attrVal2 = $scope.client.attributes['request.object.signature.alg']; + $scope.requestObjectSignatureAlg = attrVal2==null ? 'any' : attrVal2; } if (!$scope.create) { @@ -964,23 +974,20 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, }; $scope.changeUserInfoSignedResponseAlg = function() { - changeSignatureAlgorithm('user.info.response', $scope.userInfoSignedResponseAlg); + if ($scope.userInfoSignedResponseAlg === 'unsigned') { + $scope.client.attributes['user.info.response.signature.alg'] = null; + } else { + $scope.client.attributes['user.info.response.signature.alg'] = $scope.userInfoSignedResponseAlg; + } }; - function changeSignatureAlgorithm(attrPrefix, attrValue) { - var attrName = attrPrefix + '.signature.alg'; - if (attrValue === 'unsigned') { - $scope.client.attributes[attrName] = null; + $scope.changeRequestObjectSignatureAlg = function() { + if ($scope.requestObjectSignatureAlg === 'any') { + $scope.client.attributes['request.object.signature.alg'] = null; } else { - $scope.client.attributes[attrName] = attrValue; + $scope.client.attributes['request.object.signature.alg'] = $scope.requestObjectSignatureAlg; } - } - - function getSignatureAlgorithm(attrPrefix) { - var attrName = attrPrefix + '.signature.alg'; - var attrVal = $scope.client.attributes[attrName]; - return attrVal==null ? 'unsigned' : attrVal; - } + }; $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 af10a22d03..d8f0d246a1 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 @@ -348,6 +348,19 @@ {{:: 'user-info-signed-response-alg.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'request-object-signature-alg.tooltip' | translate}} +