diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java index c51b9db4f7..e8e7f8dd19 100755 --- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java +++ b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java @@ -43,6 +43,9 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -58,6 +61,8 @@ public class KeycloakOIDCFilter implements Filter { public static final String SKIP_PATTERN_PARAM = "keycloak.config.skipPattern"; + public static final String ID_MAPPER_PARAM = "keycloak.config.idMapper"; + public static final String CONFIG_RESOLVER_PARAM = "keycloak.config.resolver"; public static final String CONFIG_FILE_PARAM = "keycloak.config.file"; @@ -94,6 +99,28 @@ public class KeycloakOIDCFilter implements Filter { skipPattern = Pattern.compile(skipPatternDefinition, Pattern.DOTALL); } + String idMapperClassName = filterConfig.getInitParameter(ID_MAPPER_PARAM); + if (idMapperClassName != null) { + try { + final Class idMapperClass = getClass().getClassLoader().loadClass(idMapperClassName); + final Constructor idMapperConstructor = idMapperClass.getDeclaredConstructor(); + Object idMapperInstance = null; + // for KEYCLOAK-13745 test + if (idMapperConstructor.getModifiers() == Modifier.PRIVATE) { + idMapperInstance = idMapperClass.getMethod("getInstance").invoke(null); + } else { + idMapperInstance = idMapperConstructor.newInstance(); + } + if(idMapperInstance instanceof SessionIdMapper) { + this.idMapper = (SessionIdMapper) idMapperInstance; + } else { + log.log(Level.WARNING, "SessionIdMapper class {0} is not instance of org.keycloak.adapters.spi.SessionIdMapper", idMapperClassName); + } + } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + log.log(Level.WARNING, "SessionIdMapper class could not be instanced", e); + } + } + if (definedconfigResolver != null) { deploymentContext = new AdapterDeploymentContext(definedconfigResolver); log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", definedconfigResolver.getClass()); @@ -160,25 +187,7 @@ public class KeycloakOIDCFilter implements Filter { return; } - PreAuthActionsHandler preActions = new PreAuthActionsHandler(new UserSessionManagement() { - @Override - public void logoutAll() { - if (idMapper != null) { - idMapper.clear(); - } - } - - @Override - public void logoutHttpSessions(List ids) { - log.fine("**************** logoutHttpSessions"); - //System.err.println("**************** logoutHttpSessions"); - for (String id : ids) { - log.finest("removed idMapper: " + id); - idMapper.removeSession(id); - } - - } - }, deploymentContext, facade); + PreAuthActionsHandler preActions = new PreAuthActionsHandler(new IdMapperUserSessionManagement(), deploymentContext, facade); if (preActions.handleRequest()) { //System.err.println("**************** preActions.handleRequest happened!"); @@ -241,4 +250,24 @@ public class KeycloakOIDCFilter implements Filter { public void destroy() { } + + private class IdMapperUserSessionManagement implements UserSessionManagement { + @Override + public void logoutAll() { + if (idMapper != null) { + idMapper.clear(); + } + } + + @Override + public void logoutHttpSessions(List ids) { + log.fine("**************** logoutHttpSessions"); + //System.err.println("**************** logoutHttpSessions"); + for (String id : ids) { + log.finest("removed idMapper: " + id); + idMapper.removeSession(id); + } + + } + } } diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/spi/TestSessionIdMapper.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/spi/TestSessionIdMapper.java new file mode 100644 index 0000000000..9ab7c237ca --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/spi/TestSessionIdMapper.java @@ -0,0 +1,56 @@ +package org.keycloak.testsuite.adapter.spi; + +import org.keycloak.adapters.spi.SessionIdMapper; + +import java.util.HashSet; +import java.util.Set; + +public class TestSessionIdMapper implements SessionIdMapper { + + private static final TestSessionIdMapper SINGLETON = new TestSessionIdMapper(); + + private static Set whoCalled = new HashSet<>(); + + private TestSessionIdMapper() { + } + + public boolean isCalledBy(String className) { + return whoCalled.contains(className); + } + + public static TestSessionIdMapper getInstance() { + StackTraceElement[] ste = (new Throwable()).getStackTrace(); + for (int i = 0; i < ste.length; i++) { + whoCalled.add(ste[i].getClassName()); + } + return SINGLETON; + } + + @Override + public boolean hasSession(String id) { + return false; + } + + @Override + public void clear() { + whoCalled.clear(); + } + + @Override + public Set getUserSessions(String principal) { + return null; + } + + @Override + public String getSessionFromSSO(String sso) { + return null; + } + + @Override + public void map(String sso, String principal, String session) { + } + + @Override + public void removeSession(String session) { + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java index 5c55e3d54c..752a4f469f 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java @@ -250,23 +250,8 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor { } appendChildInDocument(webXmlDoc, "web-app", filter); - - // Limitation that all deployments of annotated class use same skipPattern. Refactor if something more flexible is needed (would require more tricky web.xml parsing though...) - String skipPattern = testClass.getAnnotation(UseServletFilter.class).skipPattern(); - if (skipPattern != null && !skipPattern.isEmpty()) { - Element initParam = webXmlDoc.createElement("init-param"); - - Element paramName = webXmlDoc.createElement("param-name"); - paramName.setTextContent(KeycloakOIDCFilter.SKIP_PATTERN_PARAM); - - Element paramValue = webXmlDoc.createElement("param-value"); - paramValue.setTextContent(skipPattern); - - initParam.appendChild(paramName); - initParam.appendChild(paramValue); - - filter.appendChild(initParam); - } + addInitParam(webXmlDoc, filter, KeycloakOIDCFilter.SKIP_PATTERN_PARAM, testClass.getAnnotation(UseServletFilter.class).skipPattern()); + addInitParam(webXmlDoc, filter, KeycloakOIDCFilter.ID_MAPPER_PARAM, testClass.getAnnotation(UseServletFilter.class).idMapper()); appendChildInDocument(webXmlDoc, "web-app", filter); @@ -298,7 +283,25 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor { archive.add(new StringAsset((documentToString(webXmlDoc))), WEBXML_PATH); } - + + private void addInitParam(Document webXmlDoc, Element filter, String initParamName, String initParamValue) { + // Limitation that all deployments of annotated class use same skipPattern. Refactor if something more flexible is needed (would require more tricky web.xml parsing though...) + if (initParamValue != null && !initParamValue.isEmpty()) { + Element initParam = webXmlDoc.createElement("init-param"); + + Element paramName = webXmlDoc.createElement("param-name"); + paramName.setTextContent(initParamName); + + Element paramValue = webXmlDoc.createElement("param-value"); + paramValue.setTextContent(initParamValue); + + initParam.appendChild(paramName); + initParam.appendChild(paramValue); + + filter.appendChild(initParam); + } + } + private String getKeycloakResolverClass(Document doc) { try { XPathFactory factory = XPathFactory.newInstance(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoFilterServletAdapterTestForCustomizedIdMapper.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoFilterServletAdapterTestForCustomizedIdMapper.java new file mode 100644 index 0000000000..843c6c3c94 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoFilterServletAdapterTestForCustomizedIdMapper.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 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.adapter.servlet; + +import org.junit.Test; +import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; +import org.keycloak.testsuite.adapter.page.CustomerPortal; +import org.keycloak.testsuite.adapter.spi.TestSessionIdMapper; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; +import org.keycloak.testsuite.auth.page.login.OIDCLogin; +import org.keycloak.testsuite.util.JavascriptBrowser; +import org.keycloak.testsuite.utils.annotation.UseServletFilter; +import org.keycloak.testsuite.utils.arquillian.ContainerConstants; +import org.openqa.selenium.WebDriver; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.shrinkwrap.api.spec.WebArchive; + +import static org.junit.Assert.assertTrue; + +@AppServerContainer(ContainerConstants.APP_SERVER_UNDERTOW) +@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY) +@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED) +@AppServerContainer(ContainerConstants.APP_SERVER_EAP) +@AppServerContainer(ContainerConstants.APP_SERVER_EAP6) +@AppServerContainer(ContainerConstants.APP_SERVER_EAP71) +@UseServletFilter(filterName = "oidc-filter", filterClass = "org.keycloak.adapters.servlet.KeycloakOIDCFilter", + filterDependency = "org.keycloak:keycloak-servlet-filter-adapter", skipPattern = "/error.html", + idMapper = "org.keycloak.testsuite.adapter.spi.TestSessionIdMapper") +public class DemoFilterServletAdapterTestForCustomizedIdMapper extends AbstractServletsAdapterTest { + + @Drone + @JavascriptBrowser + protected WebDriver jsDriver; + + @Page + protected CustomerPortal customerPortal; + + @Deployment(name = CustomerPortal.DEPLOYMENT_NAME) + protected static WebArchive customerPortal() { + return servletDeployment(CustomerPortal.DEPLOYMENT_NAME, CustomerServlet.class, ErrorServlet.class, ServletTestUtils.class); + } + + // KEYCLOAK-13745 + @Test + public void testCustomizedSessionIdMapper() { + customerPortal.navigateTo(); + TestSessionIdMapper singleton = TestSessionIdMapper.getInstance(); + assertTrue(singleton.isCalledBy(getClass().getAnnotation(UseServletFilter.class).filterClass())); + singleton.clear(); + } +} diff --git a/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/annotation/UseServletFilter.java b/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/annotation/UseServletFilter.java index 82cfca2d83..647f0fc118 100644 --- a/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/annotation/UseServletFilter.java +++ b/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/annotation/UseServletFilter.java @@ -23,4 +23,5 @@ public @interface UseServletFilter { String filterPattern() default "/*"; String dispatcherType() default ""; String skipPattern() default ""; + String idMapper() default ""; } diff --git a/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/arquillian/DeploymentArchiveProcessorUtils.java b/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/arquillian/DeploymentArchiveProcessorUtils.java index 599ef4847f..7610df9c38 100644 --- a/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/arquillian/DeploymentArchiveProcessorUtils.java +++ b/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/arquillian/DeploymentArchiveProcessorUtils.java @@ -91,8 +91,8 @@ public class DeploymentArchiveProcessorUtils { } //We need to add filter declaration to web.xml - log.info("Adding filter to " + testClass.getAnnotation(UseServletFilter.class).filterClass() + - " with mapping " + testClass.getAnnotation(UseServletFilter.class).filterPattern() + + log.info("Adding filter to " + testClass.getAnnotation(UseServletFilter.class).filterClass() + + " with mapping " + testClass.getAnnotation(UseServletFilter.class).filterPattern() + " for " + archive.getName()); Element filter = webXmlDoc.createElement("filter"); @@ -124,21 +124,9 @@ public class DeploymentArchiveProcessorUtils { // Limitation that all deployments of annotated class use same skipPattern. Refactor if // something more flexible is needed (would require more tricky web.xml parsing though...) - String skipPattern = testClass.getAnnotation(UseServletFilter.class).skipPattern(); - if (skipPattern != null && !skipPattern.isEmpty()) { - Element initParam = webXmlDoc.createElement("init-param"); + addInitParam(webXmlDoc, filter, KeycloakOIDCFilter.SKIP_PATTERN_PARAM, testClass.getAnnotation(UseServletFilter.class).skipPattern()); + addInitParam(webXmlDoc, filter, KeycloakOIDCFilter.ID_MAPPER_PARAM, testClass.getAnnotation(UseServletFilter.class).idMapper()); - Element paramName = webXmlDoc.createElement("param-name"); - paramName.setTextContent(KeycloakOIDCFilter.SKIP_PATTERN_PARAM); - - Element paramValue = webXmlDoc.createElement("param-value"); - paramValue.setTextContent(skipPattern); - - initParam.appendChild(paramName); - initParam.appendChild(paramValue); - - filter.appendChild(initParam); - } IOUtil.appendChildInDocument(webXmlDoc, "web-app", filter); @@ -165,10 +153,28 @@ public class DeploymentArchiveProcessorUtils { IOUtil.removeElementsFromDoc(webXmlDoc, "web-app", "security-constraint"); IOUtil.removeElementsFromDoc(webXmlDoc, "web-app", "login-config"); IOUtil.removeElementsFromDoc(webXmlDoc, "web-app", "security-role"); - + archive.add(new StringAsset((IOUtil.documentToString(webXmlDoc))), WEBXML_PATH); } - + + private static void addInitParam(Document webXmlDoc, Element filter, String initParamName, String initParamValue) { + // Limitation that all deployments of annotated class use same skipPattern. Refactor if something more flexible is needed (would require more tricky web.xml parsing though...) + if (initParamValue != null && !initParamValue.isEmpty()) { + Element initParam = webXmlDoc.createElement("init-param"); + + Element paramName = webXmlDoc.createElement("param-name"); + paramName.setTextContent(initParamName); + + Element paramValue = webXmlDoc.createElement("param-value"); + paramValue.setTextContent(initParamValue); + + initParam.appendChild(paramName); + initParam.appendChild(paramValue); + + filter.appendChild(initParam); + } + } + public static String getKeycloakResolverClass(Document doc) { try { XPathFactory factory = XPathFactory.newInstance(); @@ -188,7 +194,7 @@ public class DeploymentArchiveProcessorUtils { public static void addFilterDependencies(Archive archive, TestClass testClass) { log.info("Adding filter dependencies to " + archive.getName()); - + String dependency = testClass.getAnnotation(UseServletFilter.class).filterDependency(); ((WebArchive) archive).addAsLibraries(KeycloakDependenciesResolver.resolveDependencies((dependency + ":" + System.getProperty("project.version"))));