KEYCLOAK-1925: SAML adapter multitenant support

This commit is contained in:
rmartinc 2018-05-25 15:05:17 +02:00 committed by Hynek Mlnařík
parent 3918dbed59
commit 4a82979792
29 changed files with 784 additions and 58 deletions

View file

@ -25,12 +25,21 @@ import org.keycloak.adapters.spi.HttpFacade;
*/ */
public class SamlDeploymentContext { public class SamlDeploymentContext {
private SamlDeployment deployment = null; private SamlDeployment deployment = null;
private SamlConfigResolver resolver = null;
public SamlDeploymentContext(SamlDeployment deployment) { public SamlDeploymentContext(SamlDeployment deployment) {
this.deployment = deployment; this.deployment = deployment;
} }
public SamlDeploymentContext(SamlConfigResolver resolver) {
this.resolver = resolver;
}
public SamlDeployment resolveDeployment(HttpFacade facade) { public SamlDeployment resolveDeployment(HttpFacade facade) {
return deployment; if (deployment != null) {
return deployment;
} else {
return resolver.resolve(facade.getRequest());
}
} }
} }

View file

@ -19,6 +19,7 @@ package org.keycloak.adapters.saml.servlet;
import org.keycloak.adapters.saml.DefaultSamlDeployment; import org.keycloak.adapters.saml.DefaultSamlDeployment;
import org.keycloak.adapters.saml.SamlAuthenticator; import org.keycloak.adapters.saml.SamlAuthenticator;
import org.keycloak.adapters.saml.SamlConfigResolver;
import org.keycloak.adapters.saml.SamlDeployment; import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlDeploymentContext; import org.keycloak.adapters.saml.SamlDeploymentContext;
import org.keycloak.adapters.saml.SamlSession; import org.keycloak.adapters.saml.SamlSession;
@ -74,15 +75,12 @@ public class SamlFilter implements Filter {
String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver"); String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver");
if (configResolverClass != null) { if (configResolverClass != null) {
try { try {
throw new RuntimeException("Not implemented yet"); SamlConfigResolver configResolver = (SamlConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance();
// KeycloakConfigResolver configResolver = (KeycloakConfigResolver) deploymentContext = new SamlDeploymentContext(configResolver);
// context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance(); log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
// deploymentContext = new SamlDeploymentContext(configResolver);
// log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.",
// configResolverClass);
} catch (Exception ex) { } catch (Exception ex) {
log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[] { configResolverClass, ex.getMessage() }); log.log(Level.WARNING, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[] { configResolverClass, ex.getMessage() });
// deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); deploymentContext = new SamlDeploymentContext(new DefaultSamlDeployment());
} }
} else { } else {
String fp = filterConfig.getInitParameter("keycloak.config.file"); String fp = filterConfig.getInitParameter("keycloak.config.file");

View file

@ -97,13 +97,12 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
String configResolverClass = context.getServletContext().getInitParameter("keycloak.config.resolver"); String configResolverClass = context.getServletContext().getInitParameter("keycloak.config.resolver");
if (configResolverClass != null) { if (configResolverClass != null) {
try { try {
throw new RuntimeException("Not implemented yet"); SamlConfigResolver configResolver = (SamlConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
//KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance(); deploymentContext = new SamlDeploymentContext(configResolver);
//deploymentContext = new SamlDeploymentContext(configResolver); log.infov("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
//log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
} catch (Exception ex) { } catch (Exception ex) {
log.errorv("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", configResolverClass, ex.getMessage()); log.errorv("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", configResolverClass, ex.getMessage());
//deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); deploymentContext = new SamlDeploymentContext(new DefaultSamlDeployment());
} }
} else { } else {
InputStream is = getConfigInputStream(context); InputStream is = getConfigInputStream(context);

View file

@ -120,13 +120,12 @@ public class SamlServletExtension implements ServletExtension {
SamlDeploymentContext deploymentContext = null; SamlDeploymentContext deploymentContext = null;
if (configResolverClass != null) { if (configResolverClass != null) {
try { try {
throw new RuntimeException("Not implemented yet"); configResolver = (SamlConfigResolver) deploymentInfo.getClassLoader().loadClass(configResolverClass).newInstance();
//configResolver = (SamlConfigResolver) deploymentInfo.getClassLoader().loadClass(configResolverClass).newInstance(); deploymentContext = new SamlDeploymentContext(configResolver);
//deploymentContext = new AdapterDeploymentContext(configResolver); log.infov("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
//log.info("Using " + configResolverClass + " to resolve Keycloak configuration on a per-request basis.");
} catch (Exception ex) { } catch (Exception ex) {
log.warn("The specified resolver " + configResolverClass + " could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: " + ex.getMessage()); log.warn("The specified resolver " + configResolverClass + " could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: " + ex.getMessage());
//deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); deploymentContext = new SamlDeploymentContext(new DefaultSamlDeployment());
} }
} else { } else {
InputStream is = getConfigInputStream(servletContext); InputStream is = getConfigInputStream(servletContext);

View file

@ -30,6 +30,7 @@ import org.jboss.logging.Logger;
import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.saml.AdapterConstants; import org.keycloak.adapters.saml.AdapterConstants;
import org.keycloak.adapters.saml.DefaultSamlDeployment; import org.keycloak.adapters.saml.DefaultSamlDeployment;
import org.keycloak.adapters.saml.SamlConfigResolver;
import org.keycloak.adapters.saml.SamlDeployment; import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlDeploymentContext; import org.keycloak.adapters.saml.SamlDeploymentContext;
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
@ -61,13 +62,12 @@ public class KeycloakConfigurationServletListener implements ServletContextListe
if (deploymentContext == null) { if (deploymentContext == null) {
if (configResolverClass != null) { if (configResolverClass != null) {
try { try {
throw new RuntimeException("Not implemented yet"); SamlConfigResolver configResolver = (SamlConfigResolver) servletContext.getClassLoader().loadClass(configResolverClass).newInstance();
//configResolver = (SamlConfigResolver) deploymentInfo.getClassLoader().loadClass(configResolverClass).newInstance(); deploymentContext = new SamlDeploymentContext(configResolver);
//deploymentContext = new AdapterDeploymentContext(configResolver); log.infov("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
//log.info("Using " + configResolverClass + " to resolve Keycloak configuration on a per-request basis.");
} catch (Exception ex) { } catch (Exception ex) {
log.warn("The specified resolver " + configResolverClass + " could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: " + ex.getMessage()); log.errorv("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[] { configResolverClass, ex.getMessage() });
//deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); deploymentContext = new SamlDeploymentContext(new DefaultSamlDeployment());
} }
} else { } else {
InputStream is = getConfigInputStream(servletContext); InputStream is = getConfigInputStream(servletContext);

View file

@ -40,7 +40,9 @@ public class EAPDeploymentArchiveProcessor implements ApplicationArchiveProcesso
modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH); modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH);
modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH_JS); modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH_JS);
modifySAMLAdapterConfig(archive); modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH);
modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH_TENANT1);
modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH_TENANT2);
} }
private void modifyWebXML(Archive<?> archive, TestClass testClass) { private void modifyWebXML(Archive<?> archive, TestClass testClass) {
@ -61,10 +63,10 @@ public class EAPDeploymentArchiveProcessor implements ApplicationArchiveProcesso
DeploymentArchiveProcessorUtils.modifyOIDCAdapterConfig(archive, adapterConfigPath); DeploymentArchiveProcessorUtils.modifyOIDCAdapterConfig(archive, adapterConfigPath);
} }
private void modifySAMLAdapterConfig(Archive<?> archive) { private void modifySAMLAdapterConfig(Archive<?> archive, String adapterConfigPath) {
if (!archive.contains(DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH)) return; if (!archive.contains(adapterConfigPath)) return;
log.debug("Modifying adapter config " + DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH + " in " + archive.getName()); log.debug("Modifying adapter config " + adapterConfigPath + " in " + archive.getName());
DeploymentArchiveProcessorUtils.modifySAMLAdapterConfig(archive); DeploymentArchiveProcessorUtils.modifySAMLAdapterConfig(archive, adapterConfigPath);
} }
} }

View file

@ -42,7 +42,9 @@ public class EAP6DeploymentArchiveProcessor implements ApplicationArchiveProcess
modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH); modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH);
modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH_JS); modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH_JS);
modifySAMLAdapterConfig(archive); modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH);
modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH_TENANT1);
modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH_TENANT2);
} }
private void modifyWebXML(Archive<?> archive, TestClass testClass) { private void modifyWebXML(Archive<?> archive, TestClass testClass) {
@ -72,10 +74,10 @@ public class EAP6DeploymentArchiveProcessor implements ApplicationArchiveProcess
DeploymentArchiveProcessorUtils.modifyOIDCAdapterConfig(archive, adapterConfigPath); DeploymentArchiveProcessorUtils.modifyOIDCAdapterConfig(archive, adapterConfigPath);
} }
private void modifySAMLAdapterConfig(Archive<?> archive) { private void modifySAMLAdapterConfig(Archive<?> archive, String adapterConfigPath) {
if (!archive.contains(DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH)) return; if (!archive.contains(adapterConfigPath)) return;
log.debug("Modifying adapter config " + DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH + " in " + archive.getName()); log.debug("Modifying adapter config " + adapterConfigPath + " in " + archive.getName());
DeploymentArchiveProcessorUtils.modifySAMLAdapterConfig(archive); DeploymentArchiveProcessorUtils.modifySAMLAdapterConfig(archive, adapterConfigPath);
} }
} }

View file

@ -40,7 +40,9 @@ public class WildflyDeploymentArchiveProcessor implements ApplicationArchiveProc
modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH); modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH);
modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH_JS); modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH_JS);
modifySAMLAdapterConfig(archive); modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH);
modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH_TENANT1);
modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH_TENANT2);
} }
private void modifyWebXML(Archive<?> archive, TestClass testClass) { private void modifyWebXML(Archive<?> archive, TestClass testClass) {
@ -61,10 +63,10 @@ public class WildflyDeploymentArchiveProcessor implements ApplicationArchiveProc
DeploymentArchiveProcessorUtils.modifyOIDCAdapterConfig(archive, adapterConfigPath); DeploymentArchiveProcessorUtils.modifyOIDCAdapterConfig(archive, adapterConfigPath);
} }
private void modifySAMLAdapterConfig(Archive<?> archive) { private void modifySAMLAdapterConfig(Archive<?> archive, String adapterConfigPath) {
if (!archive.contains(DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH)) return; if (!archive.contains(adapterConfigPath)) return;
log.debug("Modifying adapter config " + DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH + " in " + archive.getName()); log.debug("Modifying adapter config " + adapterConfigPath + " in " + archive.getName());
DeploymentArchiveProcessorUtils.modifySAMLAdapterConfig(archive); DeploymentArchiveProcessorUtils.modifySAMLAdapterConfig(archive, adapterConfigPath);
} }
} }

View file

@ -41,7 +41,9 @@ public class Wildfly10DeploymentArchiveProcessor implements ApplicationArchivePr
modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH); modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH);
modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH_JS); modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH_JS);
modifySAMLAdapterConfig(archive); modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH);
modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH_TENANT1);
modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH_TENANT2);
} }
private void modifyWebXML(Archive<?> archive, TestClass testClass) { private void modifyWebXML(Archive<?> archive, TestClass testClass) {
@ -62,10 +64,10 @@ public class Wildfly10DeploymentArchiveProcessor implements ApplicationArchivePr
DeploymentArchiveProcessorUtils.modifyOIDCAdapterConfig(archive, adapterConfigPath); DeploymentArchiveProcessorUtils.modifyOIDCAdapterConfig(archive, adapterConfigPath);
} }
private void modifySAMLAdapterConfig(Archive<?> archive) { private void modifySAMLAdapterConfig(Archive<?> archive, String adapterConfigPath) {
if (!archive.contains(DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH)) return; if (!archive.contains(adapterConfigPath)) return;
log.debug("Modifying adapter config " + DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH + " in " + archive.getName()); log.debug("Modifying adapter config " + adapterConfigPath + " in " + archive.getName());
DeploymentArchiveProcessorUtils.modifySAMLAdapterConfig(archive); DeploymentArchiveProcessorUtils.modifySAMLAdapterConfig(archive, adapterConfigPath);
} }
} }

View file

@ -41,7 +41,9 @@ public class Wildfly9DeploymentArchiveProcessor implements ApplicationArchivePro
modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH); modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH);
modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH_JS); modifyOIDCAdapterConfig(archive, DeploymentArchiveProcessorUtils.ADAPTER_CONFIG_PATH_JS);
modifySAMLAdapterConfig(archive); modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH);
modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH_TENANT1);
modifySAMLAdapterConfig(archive, DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH_TENANT2);
} }
private void modifyWebXML(Archive<?> archive, TestClass testClass) { private void modifyWebXML(Archive<?> archive, TestClass testClass) {
@ -62,10 +64,10 @@ public class Wildfly9DeploymentArchiveProcessor implements ApplicationArchivePro
DeploymentArchiveProcessorUtils.modifyOIDCAdapterConfig(archive, adapterConfigPath); DeploymentArchiveProcessorUtils.modifyOIDCAdapterConfig(archive, adapterConfigPath);
} }
private void modifySAMLAdapterConfig(Archive<?> archive) { private void modifySAMLAdapterConfig(Archive<?> archive, String adapterConfigPath) {
if (!archive.contains(DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH)) return; if (!archive.contains(adapterConfigPath)) return;
log.debug("Modifying adapter config " + DeploymentArchiveProcessorUtils.SAML_ADAPTER_CONFIG_PATH + " in " + archive.getName()); log.debug("Modifying adapter config " + adapterConfigPath + " in " + archive.getName());
DeploymentArchiveProcessorUtils.modifySAMLAdapterConfig(archive); DeploymentArchiveProcessorUtils.modifySAMLAdapterConfig(archive, adapterConfigPath);
} }
} }

View file

@ -50,6 +50,14 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-core</artifactId> <artifactId>keycloak-saml-core</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-adapter-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-core-public</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View file

@ -0,0 +1,60 @@
/*
* 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 java.io.InputStream;
import org.keycloak.adapters.saml.SamlConfigResolver;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.saml.common.exceptions.ParsingException;
/**
*
* @author rmartinc
*/
public class SamlMultiTenantResolver implements SamlConfigResolver {
@Override
public SamlDeployment resolve(HttpFacade.Request request) {
String realm = request.getQueryParamValue("realm");
if (realm == null) {
throw new IllegalStateException("Not able to resolve realm from the request path!");
}
InputStream is = getClass().getResourceAsStream("/" + realm + "-keycloak-saml.xml");
if (is == null) {
throw new IllegalStateException("Not able to find the file /" + realm + "-keycloak-saml.xml");
}
ResourceLoader loader = new ResourceLoader() {
@Override
public InputStream getResourceAsStream(String path) {
return getClass().getResourceAsStream(path);
}
};
try {
return new DeploymentBuilder().build(is, loader);
} catch (ParsingException e) {
throw new IllegalStateException("Cannot load SAML deployment", e);
}
}
}

View file

@ -205,4 +205,14 @@ public class SendUsernameServlet extends HttpServlet {
return output; return output;
} }
@GET
@Path("getAssertionIssuer")
public Response getAssertionIssuer() throws IOException {
sentPrincipal = httpServletRequest.getUserPrincipal();
SamlPrincipal principal = (SamlPrincipal) sentPrincipal;
return Response.ok(principal.getAssertion().getIssuer().getValue())
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_TYPE + ";charset=UTF-8").build();
}
} }

View file

@ -0,0 +1,52 @@
/*
* 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.page;
import java.net.MalformedURLException;
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
import org.jboss.arquillian.test.api.ArquillianResource;
import java.net.URL;
import static org.keycloak.testsuite.util.WaitUtils.pause;
/**
* @author rmartinc
*/
public class MultiTenant1Saml extends SAMLServlet {
public static final String DEPLOYMENT_NAME = "multi-tenant-saml";
@ArquillianResource
@OperateOnDeployment(DEPLOYMENT_NAME)
private URL url;
@Override
public URL getInjectedUrl() {
try {
return new URL(url + "/?realm=tenant1");
} catch (MalformedURLException e) {
throw new IllegalStateException(e);
}
}
@Override
public void logout() {
driver.navigate().to(getUriBuilder().queryParam("GLO", "true").queryParam("realm", "tenant1").build().toASCIIString());
getUriBuilder().replaceQueryParam("GLO");
pause(300);
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.page;
import java.net.MalformedURLException;
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
import org.jboss.arquillian.test.api.ArquillianResource;
import java.net.URL;
import static org.keycloak.testsuite.util.WaitUtils.pause;
/**
* @author rmartinc
*/
public class MultiTenant2Saml extends SAMLServlet {
public static final String DEPLOYMENT_NAME = "multi-tenant-saml";
@ArquillianResource
@OperateOnDeployment(DEPLOYMENT_NAME)
private URL url;
@Override
public URL getInjectedUrl() {
try {
return new URL(url + "/?realm=tenant2");
} catch (MalformedURLException e) {
throw new IllegalStateException(e);
}
}
@Override
public void logout() {
driver.navigate().to(getUriBuilder().queryParam("GLO", "true").queryParam("realm", "tenant2").build().toASCIIString());
getUriBuilder().replaceQueryParam("GLO");
pause(300);
}
}

View file

@ -37,9 +37,16 @@ import org.keycloak.testsuite.utils.io.IOUtil;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.DOMException;
import org.w3c.dom.NodeList;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isEAP6AppServer; import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isEAP6AppServer;
import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isEAPAppServer; import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isEAPAppServer;
@ -81,6 +88,8 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
public static final String ADAPTER_CONFIG_PATH_JS = "/keycloak.json"; public static final String ADAPTER_CONFIG_PATH_JS = "/keycloak.json";
public static final String SAML_ADAPTER_CONFIG_PATH = "/WEB-INF/keycloak-saml.xml"; public static final String SAML_ADAPTER_CONFIG_PATH = "/WEB-INF/keycloak-saml.xml";
public static final String JBOSS_DEPLOYMENT_XML_PATH = "/WEB-INF/jboss-deployment-structure.xml"; public static final String JBOSS_DEPLOYMENT_XML_PATH = "/WEB-INF/jboss-deployment-structure.xml";
public static final String SAML_ADAPTER_CONFIG_PATH_TENANT1 = "/WEB-INF/classes/tenant1-keycloak-saml.xml";
public static final String SAML_ADAPTER_CONFIG_PATH_TENANT2 = "/WEB-INF/classes/tenant2-keycloak-saml.xml";
@Inject @Inject
@ClassScoped @ClassScoped
@ -131,15 +140,17 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
modifyAdapterConfig(archive, ADAPTER_CONFIG_PATH_TENANT2, relative); modifyAdapterConfig(archive, ADAPTER_CONFIG_PATH_TENANT2, relative);
modifyAdapterConfig(archive, ADAPTER_CONFIG_PATH_JS, relative); modifyAdapterConfig(archive, ADAPTER_CONFIG_PATH_JS, relative);
modifyAdapterConfig(archive, SAML_ADAPTER_CONFIG_PATH, relative); modifyAdapterConfig(archive, SAML_ADAPTER_CONFIG_PATH, relative);
modifyAdapterConfig(archive, SAML_ADAPTER_CONFIG_PATH_TENANT1, relative);
modifyAdapterConfig(archive, SAML_ADAPTER_CONFIG_PATH_TENANT2, relative);
} }
protected void modifyAdapterConfig(Archive<?> archive, String adapterConfigPath, boolean relative) { protected void modifyAdapterConfig(Archive<?> archive, String adapterConfigPath, boolean relative) {
if (archive.contains(adapterConfigPath)) { if (archive.contains(adapterConfigPath)) {
log.info("Modifying adapter config " + adapterConfigPath + " in " + archive.getName()); log.info("Modifying adapter config " + adapterConfigPath + " in " + archive.getName());
if (adapterConfigPath.equals(SAML_ADAPTER_CONFIG_PATH)) { // SAML adapter config if (adapterConfigPath.endsWith(".xml")) { // SAML adapter config
log.info("Modifying saml adapter config in " + archive.getName()); log.info("Modifying saml adapter config in " + archive.getName());
Document doc = loadXML(archive.get("WEB-INF/keycloak-saml.xml").getAsset().openStream()); Document doc = loadXML(archive.get(adapterConfigPath).getAsset().openStream());
if (AUTH_SERVER_SSL_REQUIRED) { if (AUTH_SERVER_SSL_REQUIRED) {
modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", "8080", System.getProperty("auth.server.https.port")); modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", "8080", System.getProperty("auth.server.https.port"));
modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", "http", "https"); modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", "http", "https");
@ -225,7 +236,6 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
} }
if (testClass.getJavaClass().isAnnotationPresent(UseServletFilter.class) && archive.contains(JBOSS_DEPLOYMENT_XML_PATH)) { if (testClass.getJavaClass().isAnnotationPresent(UseServletFilter.class) && archive.contains(JBOSS_DEPLOYMENT_XML_PATH)) {
addFilterDependencies(archive, testClass); addFilterDependencies(archive, testClass);
//We need to add filter declaration to web.xml //We need to add filter declaration to web.xml
@ -240,6 +250,20 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
filter.appendChild(filterName); filter.appendChild(filterName);
filter.appendChild(filterClass); filter.appendChild(filterClass);
// check if there was a resolver for OIDC and set as a filter param
String keycloakResolverClass = getKeycloakResolverClass(webXmlDoc);
if (keycloakResolverClass != null) {
Element initParam = webXmlDoc.createElement("init-param");
Element paramName = webXmlDoc.createElement("param-name");
paramName.setTextContent("keycloak.config.resolver");
Element paramValue = webXmlDoc.createElement("param-value");
paramValue.setTextContent(keycloakResolverClass);
initParam.appendChild(paramName);
initParam.appendChild(paramValue);
filter.appendChild(initParam);
}
appendChildInDocument(webXmlDoc, "web-app", filter); appendChildInDocument(webXmlDoc, "web-app", filter);
filter.appendChild(filterName); filter.appendChild(filterName);
@ -292,4 +316,21 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
archive.add(new StringAsset((documentToString(webXmlDoc))), WEBXML_PATH); archive.add(new StringAsset((documentToString(webXmlDoc))), WEBXML_PATH);
} }
private String getKeycloakResolverClass(Document doc) {
try {
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
XPathExpression expr = xpath.compile("//web-app/context-param[param-name='keycloak.config.resolver']/param-value/text()");
NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
if (nodes != null && nodes.getLength() > 0) {
return nodes.item(0).getNodeValue();
}
} catch(DOMException e) {
throw new IllegalStateException(e);
} catch (XPathExpressionException e) {
throw new IllegalStateException(e);
}
return null;
}
} }

View file

@ -0,0 +1,28 @@
/*
* 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.auth.page.login;
/**
* @author rmartinc
*/
public class SAMLPostLoginTenant1 extends Login {
SAMLPostLoginTenant1() {
setProtocol(LOGIN_ACTION);
setAuthRealm("tenant1");
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.auth.page.login;
/**
* @author rmartinc
*/
public class SAMLPostLoginTenant2 extends Login {
SAMLPostLoginTenant2() {
setProtocol(LOGIN_ACTION);
setAuthRealm("tenant2");
}
}

View file

@ -32,6 +32,7 @@ import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List; import java.util.List;
import org.jboss.shrinkwrap.api.asset.UrlAsset;
import org.junit.Assert; import org.junit.Assert;
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO; import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
@ -110,6 +111,49 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest {
return deployment; return deployment;
} }
public static WebArchive samlServletDeploymentMultiTenant(String name, String webXMLPath,
String config1, String config2,
String keystore1, String keystore2, Class... servletClasses) {
String baseSAMLPath = "/adapter-test/keycloak-saml/";
String webInfPath = baseSAMLPath + name + "/WEB-INF/";
URL webXML = AbstractServletsAdapterTest.class.getResource(baseSAMLPath + webXMLPath);
Assert.assertNotNull("web.xml should be in " + baseSAMLPath + webXMLPath, webXML);
WebArchive deployment = ShrinkWrap.create(WebArchive.class, name + ".war")
.addClasses(servletClasses)
.addAsWebInfResource(jbossDeploymentStructure, JBOSS_DEPLOYMENT_STRUCTURE_XML);
String webXMLContent;
try {
webXMLContent = IOUtils.toString(webXML.openStream(), Charset.forName("UTF-8"))
.replace("%CONTEXT_PATH%", name);
} catch (IOException e) {
throw new RuntimeException(e);
}
deployment.add(new StringAsset(webXMLContent), "/WEB-INF/web.xml");
// add the xml for each tenant in classes
URL config1Url = AbstractServletsAdapterTest.class.getResource(webInfPath + config1);
Assert.assertNotNull("config1Url should be in " + webInfPath + config1, config1Url);
deployment.add(new UrlAsset(config1Url), "/WEB-INF/classes/" + config1);
URL config2Url = AbstractServletsAdapterTest.class.getResource(webInfPath + config2);
Assert.assertNotNull("config2Url should be in " + webInfPath + config2, config2Url);
deployment.add(new UrlAsset(config2Url), "/WEB-INF/classes/" + config2);
// add the keystores for each tenant in classes
URL keystore1Url = AbstractServletsAdapterTest.class.getResource(webInfPath + keystore1);
Assert.assertNotNull("keystore1Url should be in " + webInfPath + keystore1, keystore1Url);
deployment.add(new UrlAsset(keystore1Url), "/WEB-INF/classes/" + keystore1);
URL keystore2Url = AbstractServletsAdapterTest.class.getResource(webInfPath + keystore2);
Assert.assertNotNull("keystore2Url should be in " + webInfPath + keystore2, keystore2Url);
deployment.add(new UrlAsset(keystore2Url), "/WEB-INF/classes/" + keystore2);
addContextXml(deployment, name);
return deployment;
}
@Override @Override
public void addAdapterTestRealms(List<RealmRepresentation> testRealms) { public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
testRealms.add(IOUtil.loadRealm("/adapter-test/demorealm.json")); testRealms.add(IOUtil.loadRealm("/adapter-test/demorealm.json"));

View file

@ -135,6 +135,8 @@ import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
import org.keycloak.testsuite.arquillian.containers.ContainerConstants; import org.keycloak.testsuite.arquillian.containers.ContainerConstants;
import org.keycloak.testsuite.auth.page.login.Login; import org.keycloak.testsuite.auth.page.login.Login;
import org.keycloak.testsuite.auth.page.login.SAMLIDPInitiatedLogin; import org.keycloak.testsuite.auth.page.login.SAMLIDPInitiatedLogin;
import org.keycloak.testsuite.auth.page.login.SAMLPostLoginTenant1;
import org.keycloak.testsuite.auth.page.login.SAMLPostLoginTenant2;
import org.keycloak.testsuite.page.AbstractPage; import org.keycloak.testsuite.page.AbstractPage;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.SamlClient; import org.keycloak.testsuite.util.SamlClient;
@ -255,6 +257,18 @@ public class SAMLServletAdapterTest extends AbstractServletsAdapterTest {
@Page @Page
protected EcpSP ecpSPPage; protected EcpSP ecpSPPage;
@Page
protected MultiTenant1Saml mutiTenant1SamlPage;
@Page
protected MultiTenant2Saml mutiTenant2SamlPage;
@Page
protected SAMLPostLoginTenant1 tenant1RealmSAMLPostLoginPage;
@Page
protected SAMLPostLoginTenant2 tenant2RealmSAMLPostLoginPage;
public static final String FORBIDDEN_TEXT = "HTTP status code: 403"; public static final String FORBIDDEN_TEXT = "HTTP status code: 403";
public static final String WEBSPHERE_FORBIDDEN_TEXT = "Error reported: 403"; public static final String WEBSPHERE_FORBIDDEN_TEXT = "Error reported: 403";
@ -399,9 +413,19 @@ public class SAMLServletAdapterTest extends AbstractServletsAdapterTest {
return samlServletDeployment(EcpSP.DEPLOYMENT_NAME, SendUsernameServlet.class); return samlServletDeployment(EcpSP.DEPLOYMENT_NAME, SendUsernameServlet.class);
} }
@Deployment(name = MultiTenant1Saml.DEPLOYMENT_NAME)
protected static WebArchive multiTenant() {
return samlServletDeploymentMultiTenant(MultiTenant1Saml.DEPLOYMENT_NAME, "multi-tenant-saml/WEB-INF/web.xml",
"tenant1-keycloak-saml.xml", "tenant2-keycloak-saml.xml",
"keystore-tenant1.jks", "keystore-tenant2.jks",
SendUsernameServlet.class, SamlMultiTenantResolver.class);
}
@Override @Override
public void addAdapterTestRealms(List<RealmRepresentation> testRealms) { public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/testsaml.json")); testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/tenant1-realm.json"));
testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/tenant2-realm.json"));
} }
@Override @Override
@ -437,6 +461,14 @@ public class SAMLServletAdapterTest extends AbstractServletsAdapterTest {
|| driver.getPageSource().contains(WEBSPHERE_FORBIDDEN_TEXT)); // WebSphere || driver.getPageSource().contains(WEBSPHERE_FORBIDDEN_TEXT)); // WebSphere
} }
private void assertFailedLogin(AbstractPage page, UserRepresentation user, Login loginPage) {
page.navigateTo();
assertCurrentUrlStartsWith(loginPage);
loginPage.form().login(user);
// we remain in login
assertCurrentUrlStartsWith(loginPage);
}
private void assertSuccessfulLogin(AbstractPage page, UserRepresentation user, Login loginPage, String expectedString) { private void assertSuccessfulLogin(AbstractPage page, UserRepresentation user, Login loginPage, String expectedString) {
page.navigateTo(); page.navigateTo();
assertCurrentUrlStartsWith(loginPage); assertCurrentUrlStartsWith(loginPage);
@ -554,6 +586,42 @@ public class SAMLServletAdapterTest extends AbstractServletsAdapterTest {
assertSuccessfulLogin(employeeAcsServletPage, bburkeUser, testRealmSAMLPostLoginPage, "principal=bburke"); assertSuccessfulLogin(employeeAcsServletPage, bburkeUser, testRealmSAMLPostLoginPage, "principal=bburke");
} }
@Test
public void multiTenant1SamlTest() throws Exception {
UserRepresentation user1 = createUserRepresentation("user-tenant1", "user-tenant1@redhat.com", "Bill", "Burke", true);
setPasswordFor(user1, "user-tenant1");
// check the user in the tenant logs in ok
assertSuccessfulLogin(mutiTenant1SamlPage, user1, tenant1RealmSAMLPostLoginPage, "principal=user-tenant1");
// check the issuer is the correct tenant
driver.navigate().to(mutiTenant1SamlPage.getUriBuilder().path("getAssertionIssuer").build().toASCIIString());
waitUntilElement(By.xpath("//body")).text().contains("/auth/realms/tenant1");
// check logout
mutiTenant1SamlPage.logout();
checkLoggedOut(mutiTenant1SamlPage, tenant1RealmSAMLPostLoginPage);
// check a user in the other tenant doesn't login
UserRepresentation user2 = createUserRepresentation("user-tenant2", "user-tenant2@redhat.com", "Bill", "Burke", true);
setPasswordFor(user2, "user-tenant2");
assertFailedLogin(mutiTenant1SamlPage, user2, tenant1RealmSAMLPostLoginPage);
}
@Test
public void multiTenant2SamlTest() throws Exception {
UserRepresentation user2 = createUserRepresentation("user-tenant2", "user-tenant2@redhat.com", "Bill", "Burke", true);
setPasswordFor(user2, "user-tenant2");
// check the user in the tenant logs in ok
assertSuccessfulLogin(mutiTenant2SamlPage, user2, tenant2RealmSAMLPostLoginPage, "principal=user-tenant2");
// check the issuer is the correct tenant
driver.navigate().to(mutiTenant2SamlPage.getUriBuilder().path("getAssertionIssuer").build().toASCIIString());
waitUntilElement(By.xpath("//body")).text().contains("/auth/realms/tenant2");
// check logout
mutiTenant2SamlPage.logout();
checkLoggedOut(mutiTenant2SamlPage, tenant2RealmSAMLPostLoginPage);
// check a user in the other tenant doesn't login
UserRepresentation user1 = createUserRepresentation("user-tenant1", "user-tenant1@redhat.com", "Bill", "Burke", true);
setPasswordFor(user1, "user-tenant1");
assertFailedLogin(mutiTenant2SamlPage, user1, tenant2RealmSAMLPostLoginPage);
}
private static final KeyPair NEW_KEY_PAIR = KeyUtils.generateRsaKeyPair(1024); private static final KeyPair NEW_KEY_PAIR = KeyUtils.generateRsaKeyPair(1024);
private static final String NEW_KEY_PRIVATE_KEY_PEM = PemUtils.encodeKey(NEW_KEY_PAIR.getPrivate()); private static final String NEW_KEY_PRIVATE_KEY_PEM = PemUtils.encodeKey(NEW_KEY_PAIR.getPrivate());

View file

@ -26,6 +26,7 @@
<!--required by SAML test servlets--> <!--required by SAML test servlets-->
<module name="org.keycloak.keycloak-adapter-spi" /> <module name="org.keycloak.keycloak-adapter-spi" />
<module name="org.keycloak.keycloak-saml-core" /> <module name="org.keycloak.keycloak-saml-core" />
<module name="org.keycloak.keycloak-saml-core-public" />
</dependencies> </dependencies>
</deployment> </deployment>

View file

@ -0,0 +1,36 @@
<keycloak-saml-adapter>
<SP entityID="multi-tenant"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp?realm=tenant1"
forceAuthentication="false">
<Keys>
<Key signing="true">
<KeyStore resource="/keystore-tenant1.jks" password="changeit">
<PrivateKey alias="multi-tenant" password="changeit"/>
<Certificate alias="multi-tenant"/>
</KeyStore>
</Key>
</Keys>
<PrincipalNameMapping policy="FROM_NAME_ID"/>
<RoleIdentifiers>
<Attribute name="Role"/>
</RoleIdentifiers>
<IDP entityID="idp"
signatureAlgorithm="RSA_SHA256">
<SingleSignOnService signRequest="true"
validateResponseSignature="true"
validateAssertionSignature="false"
requestBinding="POST"
bindingUrl="http://localhost:8080/auth/realms/tenant1/protocol/saml"/>
<SingleLogoutService signRequest="true"
signResponse="true"
validateRequestSignature="true"
validateResponseSignature="true"
requestBinding="POST"
responseBinding="POST"
postBindingUrl="http://localhost:8080/auth/realms/tenant1/protocol/saml"
redirectBindingUrl="http://localhost:8080/auth/realms/tenant1/protocol/saml"/>
</IDP>
</SP>
</keycloak-saml-adapter>

View file

@ -0,0 +1,36 @@
<keycloak-saml-adapter>
<SP entityID="multi-tenant"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp?realm=tenant2"
forceAuthentication="false">
<Keys>
<Key signing="true">
<KeyStore resource="/keystore-tenant2.jks" password="changeit">
<PrivateKey alias="multi-tenant" password="changeit"/>
<Certificate alias="multi-tenant"/>
</KeyStore>
</Key>
</Keys>
<PrincipalNameMapping policy="FROM_NAME_ID"/>
<RoleIdentifiers>
<Attribute name="Role"/>
</RoleIdentifiers>
<IDP entityID="idp"
signatureAlgorithm="RSA_SHA256">
<SingleSignOnService signRequest="true"
validateResponseSignature="true"
validateAssertionSignature="false"
requestBinding="POST"
bindingUrl="http://localhost:8080/auth/realms/tenant2/protocol/saml"/>
<SingleLogoutService signRequest="true"
signResponse="true"
validateRequestSignature="true"
validateResponseSignature="true"
requestBinding="POST"
responseBinding="POST"
postBindingUrl="http://localhost:8080/auth/realms/tenant2/protocol/saml"
redirectBindingUrl="http://localhost:8080/auth/realms/tenant2/protocol/saml"/>
</IDP>
</SP>
</keycloak-saml-adapter>

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>%CONTEXT_PATH%</module-name>
<servlet>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<error-page>
<location>/error.html</location>
</error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>Application</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>KEYCLOAK-SAML</auth-method>
<realm-name>demo</realm-name>
</login-config>
<security-role>
<role-name>manager</role-name>
</security-role>
<context-param>
<param-name>keycloak.config.resolver</param-name>
<param-value>org.keycloak.testsuite.adapter.servlet.SamlMultiTenantResolver</param-value>
</context-param>
</web-app>

View file

@ -0,0 +1,73 @@
{
"id": "tenant1",
"realm": "tenant1",
"enabled": true,
"accessTokenLifespan": 3000,
"accessCodeLifespan": 10,
"accessCodeLifespanUserAction": 6000,
"sslRequired": "external",
"registrationAllowed": false,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
"users" : [
{
"username" : "bburke@redhat.com",
"enabled": true,
"email" : "bburke@redhat.com",
"firstName": "Bill",
"lastName": "Burke",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"realmRoles": [ "user" ]
},
{
"username" : "user-tenant1",
"enabled": true,
"email" : "user-tenant1@redhat.com",
"firstName": "Bill",
"lastName": "Burke",
"credentials" : [
{ "type" : "password",
"value" : "user-tenant1" }
],
"realmRoles": [ "user" ]
}
],
"roles" : {
"realm" : [
{
"name": "user",
"description": "User privileges"
}
]
},
"clients": [
{
"clientId": "multi-tenant",
"name": "multi-tenant",
"enabled": true,
"protocol": "saml",
"fullScopeAllowed": true,
"frontchannelLogout": true,
"baseUrl": "http://localhost:8080/multi-tenant-saml/",
"redirectUris": [
"http://localhost:8080/multi-tenant-saml/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8080/multi-tenant-saml/saml?realm=tenant1",
"saml_assertion_consumer_url_redirect": "http://localhost:8080/multi-tenant-saml/saml?realm=tenant1",
"saml_single_logout_service_url_post": "http://localhost:8080/multi-tenant-saml/saml?realm=tenant1",
"saml_single_logout_service_url_redirect": "http://localhost:8080/multi-tenant-saml/saml?realm=tenant1",
"saml.server.signature": "true",
"saml.client.signature": "true",
"saml.signature.algorithm": "RSA_SHA256",
"saml.authnstatement": "true",
"saml.signing.certificate": "MIICwTCCAakCBgFjh8UCJDANBgkqhkiG9w0BAQsFADAkMSIwIAYDVQQDDBltdWx0aS10ZW5hbnQtc2FtbC10ZW5hbnQxMB4XDTE4MDUyMjEyMTIwNVoXDTI4MDUyMjEyMTM0NVowJDEiMCAGA1UEAwwZbXVsdGktdGVuYW50LXNhbWwtdGVuYW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJFoRVHp55NFqAbzQ9WRWKNP3yUrAT5HT0klZg8ttuMq5JhuGvl5aADnoOUhD/SbJST73ObF3JMqRSW8899yKByYxG5HH03KEpbGbB+gT2dYwzHSqN7E2G0h+VSpfQvjOyMFdRQORbBicnTVN+a828JlTf7uxmQ2ifgKuuZgUtUydLRh9vbcbYMP0sqKBNXAJKzJuqv6yQPmn78OGvtDZz+oThcJ44QQ19A/PaNrDNPKvjQsibfBfyV/tCaRs6UKIdROy272ZDL8aqVrZqnFzF5mTNOa+Ko91UTLaWKsSxBPk3Tv2tZJkeoWztFBOjVEV3Uz76BraUSZeg78oNfuuJMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEABVma/QDyE2ZyNZuFjoJ9+FuUlTElcoHsId1W6UY1tfpi4kqBneLtxJfOUtCHveogNgFaJJHiAfJDtpj3iNuEtziyRQuSlg7NiHHhKcxAUpBBs6So2zlTfTrwC647IOQWPFQRTQruJJnSMQHQ0PRBG6HBQKpVtltk5PEMOi0YK/y0XxMMqi2/0TCyVEuQlLbu+gN95xIkXTh90dte1Vh7PJk54Mby9Yk3Q0Qq0aVF805/GimA9o/rGYSruH6Tj8qGp6rvVN6StM4o9sDnZIEcZMEWTvaeLZxQ0A8TD4NWgp+M2MmRH/hahyOkiYL3nv/S/MTe0VmC908h6+R0QqGTYQ==",
"saml.signing.private.key": "MIIEowIBAAKCAQEAkWhFUennk0WoBvND1ZFYo0/fJSsBPkdPSSVmDy224yrkmG4a+XloAOeg5SEP9JslJPvc5sXckypFJbzz33IoHJjEbkcfTcoSlsZsH6BPZ1jDMdKo3sTYbSH5VKl9C+M7IwV1FA5FsGJydNU35rzbwmVN/u7GZDaJ+Aq65mBS1TJ0tGH29txtgw/SyooE1cAkrMm6q/rJA+afvw4a+0NnP6hOFwnjhBDX0D89o2sM08q+NCyJt8F/JX+0JpGzpQoh1E7LbvZkMvxqpWtmqcXMXmZM05r4qj3VRMtpYqxLEE+TdO/a1kmR6hbO0UE6NURXdTPvoGtpRJl6Dvyg1+64kwIDAQABAoIBAA3Xrlm4+cnEZNWcjQWk25pYfTbNnEWwhjTBcbDaOkHwEGkOelTroOINKv0FI762klet/n6dsXz1FjYcgd7wwC7QwEp7TNib9x8RbrOoEEcXZSW2F0t10+C3zkOoCvZ5wGR6HYY2QZ4kER9cOQEnU4hzGnS9iHd71bCeXOKXousWyJGMg3Bl5MSIqNabSIMNTqOHVR8TEMOSGjkYQO4oTP+YcySbIatAVj95aMGTUWJZxMVDBHt2CyjvKIRq1nJV8nhuUH3ui2wZJIscjPuSWFHAuB/dDli2XK8aZUyMGrbD2AjFhegrG73/w71QvCqZ14hOiaX3SCRKJFqwsZ+iW/kCgYEA2emTez8HDvxW5dhbV+qIl+PSBhGBz7w0Wr6dc7Nakriue9ad3lBr2LjrI2hLUHcrS/TKhnyLfe67lODysYPd9HHwFDMxgXQLTrZudQkUt8ebNi//1MGRuZ1Z6W/7MW9oN/JwoqWcYkXN+4bIQyTkKQA4lvkaBOuKEIFUn059I50CgYEAqtJ56pc9gJDB+nnJ778dYlLLxjsGIZawWXf9BgxnPnoIRn5ejOusfB/BExeLxngmMOxNI0IAL59NmkNPCse/SSXGM8GS2llSqEtZBCtTfYuhhyAhoqA4qwrm9WJybuYaKeBllmmrbOX/TmzhC0dwy0anUG0tm4NFTV8AK95hje8CgYEAm3woeWYteSngLzxDYOW99PLfpujTARC/Ioij/CxbUhloloA6QKiNayP201rVcmK1iArwfylats6jFcW0Jal7s7GgpikpB79vWgido/CI0eEhBHcXSg2cFx8JSqFWUJ23dUQNzl/wx8YbBX/UYORv0DmSJ1cyk5Qk/UXqxYjRjZkCgYACwfcZ5Gsnwi5/fqvV5P3ycme7wYQt0qLyLs+040pfZdTwXmXkXIGiV1jkmAK3p4TmUUpFgXFDU40LKn8CK4tZAPUcLMnUIJEHCoBbYt+sLS7kYY5pc7C2giyMVZSHWcueVXMOZJJR5byjZXqUlgiqH2/gCoMr+YiK4Te9fY+RnQKBgGekolH+YH4HiUtYKn5qGKveiSpOy/X90zMLLKFWVjngsYEqDj7Bi8S8t1MReaMPMMo/sEBwYE+jomnCH3Dj+HlKf00WjFa/5pbFWqbiIcGGdXslfJSNSDeQ5bpdc6MqFnumZHumExqUGGGXUxVXnhf2NyhfGixNiukWXAIhzlW2"
}
}
]
}

View file

@ -0,0 +1,73 @@
{
"id": "tenant2",
"realm": "tenant2",
"enabled": true,
"accessTokenLifespan": 3000,
"accessCodeLifespan": 10,
"accessCodeLifespanUserAction": 6000,
"sslRequired": "external",
"registrationAllowed": false,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
"users" : [
{
"username" : "bburke@redhat.com",
"enabled": true,
"email" : "bburke@redhat.com",
"firstName": "Bill",
"lastName": "Burke",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"realmRoles": [ "user" ]
},
{
"username" : "user-tenant2",
"enabled": true,
"email" : "user-tenant2@redhat.com",
"firstName": "Bill",
"lastName": "Burke",
"credentials" : [
{ "type" : "password",
"value" : "user-tenant2" }
],
"realmRoles": [ "user" ]
}
],
"roles" : {
"realm" : [
{
"name": "user",
"description": "User privileges"
}
]
},
"clients": [
{
"clientId": "multi-tenant",
"name": "multi-tenant",
"enabled": true,
"protocol": "saml",
"fullScopeAllowed": true,
"frontchannelLogout": true,
"baseUrl": "http://localhost:8080/multi-tenant-saml/",
"redirectUris": [
"http://localhost:8080/multi-tenant-saml/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8080/multi-tenant-saml/saml?realm=tenant2",
"saml_assertion_consumer_url_redirect": "http://localhost:8080/multi-tenant-saml/saml?realm=tenant2",
"saml_single_logout_service_url_post": "http://localhost:8080/multi-tenant-saml/saml?realm=tenant2",
"saml_single_logout_service_url_redirect": "http://localhost:8080/multi-tenant-saml/saml?realm=tenant2",
"saml.server.signature": "true",
"saml.client.signature": "true",
"saml.signature.algorithm": "RSA_SHA256",
"saml.authnstatement": "true",
"saml.signing.certificate": "MIICpzCCAY8CBgFjh/X1zDANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxtdWx0aS10ZW5hbnQwHhcNMTgwNTIyMTMwNTMzWhcNMjgwNTIyMTMwNzEzWjAXMRUwEwYDVQQDDAxtdWx0aS10ZW5hbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEIM1mJ9KE9OtVDl+6N9Cyg1byP8g8NARWIxstKR69eb9MlrXdzAZtv6S/DyLHuNpjX1DXisuHYHn4Si/2xa0XpRos151lOrk4YrJPokQWz+/Qej3vpJOeWC/EXeISs59t3zrUf/v2OtI57lln7ItABe6+nbTmGuLVCXYxHTaLxo4PRMh9IdsWIWjGi1F4uUR1P253ty9GgYFiRzNMxT9M4yOc5nXqih0kid7iZp+Idz1qLdxphgwUY5ElV4aBy1g5eudll8UFsGqaYUNC+V048BRoGyjstzoN8nYDN7yn89Vj5mjEzPH+FQwd1YlT2f7jH92UMBDdHpWrVOEF0DPVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACaadxu5ap+jr1wsXUX7KrrS5zc5yrNndEduFRLCetqeQ2hAmQkawqVDJnbjGH+uK5pNQ9Qk+ez77hbjMd8TlDwWD6wKdnIgZybxZ8WQi2/IWGXhrHwyITKSJukG1aDu1c9gwO9ho8J17bjpiaINx35s6Gf+iAttwv+Az7DytU42nhXxbSIgG4kb/RQkqOyIagCDJtExUgAs0mdG/Qm0uIMBQ3OZon06tAkj8H2M6PagRHDxIvCuu+MvOzv7WKOtOBP8p/cw5+1tagb9zO7U5yzT9iexYKfn5soU80sbUFgy5PZPq5MEF/xqzgwJdZVkRk0XAcIDTLjwquxd0R+u9tg=",
"saml.signing.private.key": "MIIEpAIBAAKCAQEAxCDNZifShPTrVQ5fujfQsoNW8j/IPDQEViMbLSkevXm/TJa13cwGbb+kvw8ix7jaY19Q14rLh2B5+Eov9sWtF6UaLNedZTq5OGKyT6JEFs/v0Ho976STnlgvxF3iErOfbd861H/79jrSOe5ZZ+yLQAXuvp205hri1Ql2MR02i8aOD0TIfSHbFiFoxotReLlEdT9ud7cvRoGBYkczTMU/TOMjnOZ16oodJIne4mafiHc9ai3caYYMFGORJVeGgctYOXrnZZfFBbBqmmFDQvldOPAUaBso7Lc6DfJ2Aze8p/PVY+ZoxMzx/hUMHdWJU9n+4x/dlDAQ3R6Vq1ThBdAz1QIDAQABAoIBACKrbcOuLG+mX+dUOCXR8glsYDVIgxvpUg7r+8Ta7P0vhVqDlbiUdVp3Myc3BL3rdmd0lPTVKy9OJaF3c80amoOAgwUERGV9oPpPsBeVppWlwk3HHiW7oQCvtBnxQqJtsDQa7upbiW24bishcBqH3QG/SrnVZQH8JLbmCkeaU2cXrbqZchGTj4nMVlHaFgOqb9A6y911Jlbyh9F2lP5aNRPekcNUV17+JhxKxCeyPesrvwey4DcbTWC0Li+BtnO2YwGADRteiz23g8xz+miQO4vTcOy/rctgNkmlo6U9vSwfuvRmMlVy1q1JnAiyATr/nzAz5zYPSHuQjeA/i9vZU00CgYEA9fSKyd3l+AZDPivVb+uqOu6pWAs3UoVZwE7SeEvvt93o+eOMWn+e9K5XimCfT/EM+B09L186hx5RL46ZNuUGWDtCSZXWpD5MRIt7e3gLSmVUyg9BM5UJk7zRwi933A5+VqwpTw/ivT0XSVbErRPSirmbXLO2n+i00a7J9BmsP/cCgYEAzCNRkaiW4dEMGsdACa+FW+QATvGXtcW10xBD3NuvBo8XvNRf/d2o3Pd99cljTlKiBLMZYap3+AuJzHtHRb9bEhJDKmitVlZpcNCcC0RA3t+9CfNdvHCrpW+lZEdVUFmIbkvgkqeFG2e7o03Q2G1avDnjzQxCworO5XJQMBcYD5MCgYEA4RXSjbsM4laY4ySqR6qcNyKCx5g8IMD4yg1Yf86+qr3ioA2mPIvepH2Ij5KtOTOYctgPTnMP1Ofh1Gvju2EM1WIl38HIlLaOhYxAjVXmv0bMub4MJXCXOyTpsZRPVIvPAvK7OyeGkTh/PxaxFtO1Mk955vRwhRcpo1saZtG32TECgYEAtyWg0xv8cpEJWSUWkRoGfdDrbehXAmBlpv1axVXbi/jphSLNFIjALa9mNRP/oo+EiM7eoL8+by567RhVc4AhBu+Xjv7nNSTF6M9gkMMlqE/33GuZ160GcqDeND/DjRkmzD4LN8hQJaxFrlfsXaCO3XzaomazprK+uSB8TQkLLz0CgYAWja2b42bn9vqXWnhfOorgD+HaBpVhhOAzyJu+U2YJViOSdtZpUgQCmz8XUOm5en2CwAIFOG0NvKtpLMmqNKnC7Gigg6ow2hw0v+VaH2trU295Z/lkRaX6fUT//My04aXPHg5bR3DXyd3YKOUV1MfvpTQelG1MKLX3YaMGxcmZ5w=="
}
}
]
}

View file

@ -18,6 +18,11 @@ package org.keycloak.testsuite.utils.arquillian;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.jboss.arquillian.test.spi.TestClass; import org.jboss.arquillian.test.spi.TestClass;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.Archive;
@ -28,8 +33,10 @@ import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.testsuite.utils.annotation.UseServletFilter; import org.keycloak.testsuite.utils.annotation.UseServletFilter;
import org.keycloak.testsuite.utils.io.IOUtil; import org.keycloak.testsuite.utils.io.IOUtil;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/** /**
* *
@ -49,6 +56,8 @@ public class DeploymentArchiveProcessorUtils {
public static final String ADAPTER_CONFIG_PATH_JS = "/keycloak.json"; public static final String ADAPTER_CONFIG_PATH_JS = "/keycloak.json";
public static final String SAML_ADAPTER_CONFIG_PATH = "/WEB-INF/keycloak-saml.xml"; public static final String SAML_ADAPTER_CONFIG_PATH = "/WEB-INF/keycloak-saml.xml";
public static final String JBOSS_DEPLOYMENT_XML_PATH = "/WEB-INF/jboss-deployment-structure.xml"; public static final String JBOSS_DEPLOYMENT_XML_PATH = "/WEB-INF/jboss-deployment-structure.xml";
public static final String SAML_ADAPTER_CONFIG_PATH_TENANT1 = "/WEB-INF/classes/tenant1-keycloak-saml.xml";
public static final String SAML_ADAPTER_CONFIG_PATH_TENANT2 = "/WEB-INF/classes/tenant2-keycloak-saml.xml";
/** /**
* @return true iff archive's name equals run-on-server-classes.war * @return true iff archive's name equals run-on-server-classes.war
@ -85,6 +94,19 @@ public class DeploymentArchiveProcessorUtils {
filter.appendChild(filterName); filter.appendChild(filterName);
filter.appendChild(filterClass); filter.appendChild(filterClass);
// check if there was a resolver for OIDC and set as a filter param
String keycloakResolverClass = getKeycloakResolverClass(webXmlDoc);
if (keycloakResolverClass != null) {
Element initParam = webXmlDoc.createElement("init-param");
Element paramName = webXmlDoc.createElement("param-name");
paramName.setTextContent("keycloak.config.resolver");
Element paramValue = webXmlDoc.createElement("param-value");
paramValue.setTextContent(keycloakResolverClass);
initParam.appendChild(paramName);
initParam.appendChild(paramValue);
filter.appendChild(initParam);
}
// Limitation that all deployments of annotated class use same skipPattern. Refactor if // 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...) // something more flexible is needed (would require more tricky web.xml parsing though...)
String skipPattern = testClass.getAnnotation(UseServletFilter.class).skipPattern(); String skipPattern = testClass.getAnnotation(UseServletFilter.class).skipPattern();
@ -132,6 +154,23 @@ public class DeploymentArchiveProcessorUtils {
archive.add(new StringAsset((IOUtil.documentToString(webXmlDoc))), WEBXML_PATH); archive.add(new StringAsset((IOUtil.documentToString(webXmlDoc))), WEBXML_PATH);
} }
public static String getKeycloakResolverClass(Document doc) {
try {
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
XPathExpression expr = xpath.compile("//web-app/context-param[param-name='keycloak.config.resolver']/param-value/text()");
NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
if (nodes != null && nodes.getLength() > 0) {
return nodes.item(0).getNodeValue();
}
} catch(DOMException e) {
throw new IllegalStateException(e);
} catch (XPathExpressionException e) {
throw new IllegalStateException(e);
}
return null;
}
public static void addFilterDependencies(Archive<?> archive, TestClass testClass) { public static void addFilterDependencies(Archive<?> archive, TestClass testClass) {
log.info("Adding filter dependencies to " + archive.getName()); log.info("Adding filter dependencies to " + archive.getName());
@ -163,8 +202,8 @@ public class DeploymentArchiveProcessorUtils {
} }
} }
public static void modifySAMLAdapterConfig(Archive<?> archive) { public static void modifySAMLAdapterConfig(Archive<?> archive, String adapterConfigPath) {
Document doc = IOUtil.loadXML(archive.get(SAML_ADAPTER_CONFIG_PATH).getAsset().openStream()); Document doc = IOUtil.loadXML(archive.get(adapterConfigPath).getAsset().openStream());
if (AUTH_SERVER_SSL_REQUIRED) { if (AUTH_SERVER_SSL_REQUIRED) {
IOUtil.modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", "8080", System.getProperty("auth.server.https.port")); IOUtil.modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", "8080", System.getProperty("auth.server.https.port"));
@ -185,7 +224,7 @@ public class DeploymentArchiveProcessorUtils {
IOUtil.modifyDocElementAttribute(doc, "SP", "logoutPage", "8081", System.getProperty("app.server.http.port")); IOUtil.modifyDocElementAttribute(doc, "SP", "logoutPage", "8081", System.getProperty("app.server.http.port"));
} }
archive.add(new StringAsset(IOUtil.documentToString(doc)), SAML_ADAPTER_CONFIG_PATH); archive.add(new StringAsset(IOUtil.documentToString(doc)), adapterConfigPath);
((WebArchive) archive).addAsResource(new File(DeploymentArchiveProcessorUtils.class.getResource("/keystore/keycloak.truststore").getFile())); ((WebArchive) archive).addAsResource(new File(DeploymentArchiveProcessorUtils.class.getResource("/keystore/keycloak.truststore").getFile()));
} }