diff --git a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/SamlAuthenticatorValve.java b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/SamlAuthenticatorValve.java index 3ec61a7235..99d539387e 100755 --- a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/SamlAuthenticatorValve.java +++ b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/SamlAuthenticatorValve.java @@ -22,9 +22,10 @@ import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.core.StandardContext; import org.apache.catalina.deploy.LoginConfig; + import org.keycloak.adapters.jbossweb.JBossWebPrincipalFactory; -import org.keycloak.adapters.saml.AbstractSamlAuthenticatorValve; -import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.*; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; import org.keycloak.adapters.tomcat.GenericPrincipalFactory; import javax.servlet.http.HttpServletResponse; @@ -71,4 +72,11 @@ public class SamlAuthenticatorValve extends AbstractSamlAuthenticatorValve { protected GenericPrincipalFactory createPrincipalFactory() { return new JBossWebPrincipalFactory(); } + + @Override + protected void addTokenStoreUpdaters() { + context.addApplicationListenerInstance(new IdMapperUpdaterSessionListener(mapper)); + setIdMapperUpdater(SessionIdMapperUpdater.EXTERNAL); + super.addTokenStoreUpdaters(); + } } diff --git a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/InfinispanSessionCacheIdMapperUpdater.java b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/InfinispanSessionCacheIdMapperUpdater.java new file mode 100644 index 0000000000..b6f4c23f09 --- /dev/null +++ b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/InfinispanSessionCacheIdMapperUpdater.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017 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.adapters.saml.jbossweb.infinispan; + +import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.ServletContext; +import org.apache.catalina.Context; +import org.infinispan.Cache; +import org.infinispan.configuration.cache.CacheMode; +import org.infinispan.configuration.cache.Configuration; +import org.infinispan.manager.EmbeddedCacheManager; +import org.jboss.logging.Logger; + +/** + * + * @author hmlnarik + */ +public class InfinispanSessionCacheIdMapperUpdater { + + private static final Logger LOG = Logger.getLogger(InfinispanSessionCacheIdMapperUpdater.class); + + public static final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container/web"; + + private static final String DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheContainerJndi"; + private static final String DEPLOYMENT_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.deploymentCacheName"; + private static final String SSO_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheName"; + + public static SessionIdMapperUpdater addTokenStoreUpdaters(Context context, SessionIdMapper mapper, SessionIdMapperUpdater previousIdMapperUpdater) { + boolean distributable = context.getDistributable(); + + if (! distributable) { + LOG.warnv("Deployment {0} does not use supported distributed session cache mechanism", context.getName()); + return previousIdMapperUpdater; + } + + ServletContext servletContext = context.getServletContext(); + String cacheContainerLookup = (servletContext != null && servletContext.getInitParameter(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME) != null) + ? servletContext.getInitParameter(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME) + : DEFAULT_CACHE_CONTAINER_JNDI_NAME; + + // the following is based on https://github.com/jbossas/jboss-as/blob/7.2.0.Final/clustering/web-infinispan/src/main/java/org/jboss/as/clustering/web/infinispan/DistributedCacheManagerFactory.java#L116-L122 + String host = context.getParent() == null ? "" : context.getParent().getName(); + String contextPath = context.getPath(); + if ("/".equals(contextPath)) { + contextPath = "/ROOT"; + } + + boolean deploymentSessionCacheNamePreset = servletContext != null && servletContext.getInitParameter(DEPLOYMENT_CACHE_NAME_PARAM_NAME) != null; + String deploymentSessionCacheName = deploymentSessionCacheNamePreset + ? servletContext.getInitParameter(DEPLOYMENT_CACHE_NAME_PARAM_NAME) + : host + contextPath; + boolean ssoCacheNamePreset = servletContext != null && servletContext.getInitParameter(SSO_CACHE_NAME_PARAM_NAME) != null; + String ssoCacheName = ssoCacheNamePreset + ? servletContext.getInitParameter(SSO_CACHE_NAME_PARAM_NAME) + : deploymentSessionCacheName + ".ssoCache"; + + try { + EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup); + + Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(ssoCacheName); + if (ssoCacheConfiguration == null) { + Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName); + if (cacheConfiguration == null) { + LOG.debugv("Using default cache container configuration for SSO cache. lookup={0}, looked up configuration of cache={1}", cacheContainerLookup, deploymentSessionCacheName); + ssoCacheConfiguration = cacheManager.getDefaultCacheConfiguration(); + } else { + LOG.debugv("Using distributed HTTP session cache configuration for SSO cache. lookup={0}, configuration taken from cache={1}", cacheContainerLookup, deploymentSessionCacheName); + ssoCacheConfiguration = cacheConfiguration; + cacheManager.defineConfiguration(ssoCacheName, ssoCacheConfiguration); + } + } else { + LOG.debugv("Using custom configuration for SSO cache. lookup={0}, cache name={1}", cacheContainerLookup, ssoCacheName); + } + + CacheMode ssoCacheMode = ssoCacheConfiguration.clustering().cacheMode(); + if (ssoCacheMode != CacheMode.REPL_ASYNC && ssoCacheMode != CacheMode.REPL_SYNC) { + LOG.warnv("SSO cache mode is {0}, it is recommended to use replicated mode instead", ssoCacheConfiguration.clustering().cacheModeString()); + } + + Cache ssoCache = cacheManager.getCache(ssoCacheName, true); + ssoCache.addListener(new SsoSessionCacheListener(mapper)); + + LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, deploymentSessionCacheName); + + SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater); + + return updater; + } catch (NamingException ex) { + LOG.warnv("Failed to obtain distributed session cache container, lookup={0}", cacheContainerLookup); + return previousIdMapperUpdater; + } + } +} diff --git a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoCacheSessionIdMapperUpdater.java b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoCacheSessionIdMapperUpdater.java new file mode 100644 index 0000000000..f60e802929 --- /dev/null +++ b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoCacheSessionIdMapperUpdater.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017 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.adapters.saml.jbossweb.infinispan; + +import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; + +import org.infinispan.Cache; + +/** + * + * @author hmlnarik + */ +public class SsoCacheSessionIdMapperUpdater implements SessionIdMapperUpdater { + + private final SessionIdMapperUpdater delegate; + /** + * Cache where key is a HTTP session ID, and value is a pair (user session ID, principal name) of Strings. + */ + private final Cache httpSessionToSsoCache; + + public SsoCacheSessionIdMapperUpdater(Cache httpSessionToSsoCache, SessionIdMapperUpdater previousIdMapperUpdater) { + this.delegate = previousIdMapperUpdater; + this.httpSessionToSsoCache = httpSessionToSsoCache; + } + + // SessionIdMapperUpdater methods + + @Override + public void clear(SessionIdMapper idMapper) { + httpSessionToSsoCache.clear(); + this.delegate.clear(idMapper); + } + + @Override + public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) { + httpSessionToSsoCache.put(httpSessionId, new String[] {sso, principal}); + this.delegate.map(idMapper, sso, principal, httpSessionId); + } + + @Override + public void removeSession(SessionIdMapper idMapper, String httpSessionId) { + httpSessionToSsoCache.remove(httpSessionId); + this.delegate.removeSession(idMapper, httpSessionId); + } +} diff --git a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoSessionCacheListener.java b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoSessionCacheListener.java new file mode 100644 index 0000000000..ee100ad317 --- /dev/null +++ b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoSessionCacheListener.java @@ -0,0 +1,156 @@ +/* + * Copyright 2017 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.adapters.saml.jbossweb.infinispan; + +import org.keycloak.adapters.spi.SessionIdMapper; + +import java.util.*; +import java.util.concurrent.*; +import org.infinispan.notifications.Listener; +import org.infinispan.notifications.cachelistener.annotation.*; +import org.infinispan.notifications.cachelistener.event.*; +import org.infinispan.notifications.cachemanagerlistener.annotation.CacheStarted; +import org.infinispan.notifications.cachemanagerlistener.annotation.CacheStopped; +import org.infinispan.notifications.cachemanagerlistener.event.CacheStartedEvent; +import org.infinispan.notifications.cachemanagerlistener.event.CacheStoppedEvent; +import org.infinispan.transaction.xa.GlobalTransaction; +import org.jboss.logging.Logger; + +/** + * + * @author hmlnarik + */ +@Listener +public class SsoSessionCacheListener { + + private static final Logger LOG = Logger.getLogger(SsoSessionCacheListener.class); + + private final ConcurrentMap> map = new ConcurrentHashMap<>(); + + private final SessionIdMapper idMapper; + + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + public SsoSessionCacheListener(SessionIdMapper idMapper) { + this.idMapper = idMapper; + } + + @TransactionRegistered + public void startTransaction(TransactionRegisteredEvent event) { + map.put(event.getGlobalTransaction(), new ConcurrentLinkedQueue()); + } + + @CacheStarted + public void cacheStarted(CacheStartedEvent event) { + this.executor = Executors.newSingleThreadExecutor(); + } + + @CacheStopped + public void cacheStopped(CacheStoppedEvent event) { + this.executor.shutdownNow(); + } + + @CacheEntryCreated + @CacheEntryRemoved + @CacheEntryModified + public void addEvent(TransactionalEvent event) { + if (event.isPre() == false) { + map.get(event.getGlobalTransaction()).add(event); + } + } + + @TransactionCompleted + public void endTransaction(TransactionCompletedEvent event) { + Queue events = map.remove(event.getGlobalTransaction()); + + if (events == null || ! event.isTransactionSuccessful()) { + return; + } + + if (event.isOriginLocal()) { + // Local events are processed by local HTTP session listener + return; + } + + for (final Event e : events) { + switch (e.getType()) { + case CACHE_ENTRY_CREATED: + this.executor.submit(new Runnable() { + @Override public void run() { + cacheEntryCreated((CacheEntryCreatedEvent) e); + } + }); + break; + + case CACHE_ENTRY_MODIFIED: + this.executor.submit(new Runnable() { + @Override public void run() { + cacheEntryModified((CacheEntryModifiedEvent) e); + } + }); + break; + + case CACHE_ENTRY_REMOVED: + this.executor.submit(new Runnable() { + @Override public void run() { + cacheEntryRemoved((CacheEntryRemovedEvent) e); + } + }); + break; + } + } + } + + private void cacheEntryCreated(CacheEntryCreatedEvent event) { + if (! (event.getKey() instanceof String) || ! (event.getValue() instanceof String[])) { + return; + } + String httpSessionId = (String) event.getKey(); + String[] value = (String[]) event.getValue(); + String ssoId = value[0]; + String principal = value[1]; + + LOG.tracev("cacheEntryCreated {0}:{1}", httpSessionId, ssoId); + + this.idMapper.map(ssoId, principal, httpSessionId); + } + + private void cacheEntryModified(CacheEntryModifiedEvent event) { + if (! (event.getKey() instanceof String) || ! (event.getValue() instanceof String[])) { + return; + } + String httpSessionId = (String) event.getKey(); + String[] value = (String[]) event.getValue(); + String ssoId = value[0]; + String principal = value[1]; + + LOG.tracev("cacheEntryModified {0}:{1}", httpSessionId, ssoId); + + this.idMapper.removeSession(httpSessionId); + this.idMapper.map(ssoId, principal, httpSessionId); + } + + private void cacheEntryRemoved(CacheEntryRemovedEvent event) { + if (! (event.getKey() instanceof String)) { + return; + } + + LOG.tracev("cacheEntryRemoved {0}", event.getKey()); + + this.idMapper.removeSession((String) event.getKey()); + } +} diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java index bc23f17f3d..c356391af7 100755 --- a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java +++ b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java @@ -25,13 +25,10 @@ import org.apache.catalina.authenticator.FormAuthenticator; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.jboss.logging.Logger; + import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; import org.keycloak.adapters.saml.config.parsers.ResourceLoader; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.InMemorySessionIdMapper; -import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.*; import org.keycloak.adapters.tomcat.CatalinaHttpFacade; import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement; import org.keycloak.adapters.tomcat.GenericPrincipalFactory; @@ -46,6 +43,8 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.*; +import java.util.Map; /** * Keycloak authentication valve @@ -62,6 +61,7 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement(); protected SamlDeploymentContext deploymentContext; protected SessionIdMapper mapper = new InMemorySessionIdMapper(); + protected SessionIdMapperUpdater idMapperUpdater = SessionIdMapperUpdater.DIRECT; @Override public void lifecycleEvent(LifecycleEvent event) { @@ -69,7 +69,7 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i cache = false; } else if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) { keycloakInit(); - } else if (event.getType() == Lifecycle.BEFORE_STOP_EVENT) { + } else if (Lifecycle.BEFORE_STOP_EVENT.equals(event.getType())) { beforeStop(); } } @@ -129,6 +129,8 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i } context.getServletContext().setAttribute(SamlDeploymentContext.class.getName(), deploymentContext); + + addTokenStoreUpdaters(); } protected void beforeStop() { @@ -273,8 +275,68 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i protected SamlSessionStore createSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) { SamlSessionStore store; - store = new CatalinaSamlSessionStore(userSessionManagement, createPrincipalFactory(), mapper, request, this, facade, resolvedDeployment); + store = new CatalinaSamlSessionStore(userSessionManagement, createPrincipalFactory(), mapper, idMapperUpdater, request, this, facade, resolvedDeployment); return store; } + protected void addTokenStoreUpdaters() { + SessionIdMapperUpdater updater = getIdMapperUpdater(); + + try { + String idMapperSessionUpdaterClasses = context.getServletContext().getInitParameter("keycloak.sessionIdMapperUpdater.classes"); + if (idMapperSessionUpdaterClasses == null) { + return; + } + + for (String clazz : idMapperSessionUpdaterClasses.split("\\s*,\\s*")) { + if (! clazz.isEmpty()) { + updater = invokeAddTokenStoreUpdaterMethod(clazz, updater); + } + } + } finally { + setIdMapperUpdater(updater); + } + } + + private SessionIdMapperUpdater invokeAddTokenStoreUpdaterMethod(String idMapperSessionUpdaterClass, SessionIdMapperUpdater previousIdMapperUpdater) { + try { + Class clazz = context.getLoader().getClassLoader().loadClass(idMapperSessionUpdaterClass); + Method addTokenStoreUpdatersMethod = clazz.getMethod("addTokenStoreUpdaters", Context.class, SessionIdMapper.class, SessionIdMapperUpdater.class); + if (! Modifier.isStatic(addTokenStoreUpdatersMethod.getModifiers()) + || ! Modifier.isPublic(addTokenStoreUpdatersMethod.getModifiers()) + || ! SessionIdMapperUpdater.class.isAssignableFrom(addTokenStoreUpdatersMethod.getReturnType())) { + log.errorv("addTokenStoreUpdaters method in class {0} has to be public static. Ignoring class.", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } + + log.debugv("Initializing sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); + return (SessionIdMapperUpdater) addTokenStoreUpdatersMethod.invoke(null, context, mapper, previousIdMapperUpdater); + } catch (ClassNotFoundException ex) { + log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } catch (NoSuchMethodException ex) { + log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } catch (SecurityException ex) { + log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } catch (IllegalAccessException ex) { + log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } catch (IllegalArgumentException ex) { + log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } catch (InvocationTargetException ex) { + log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } + } + + public SessionIdMapperUpdater getIdMapperUpdater() { + return idMapperUpdater; + } + + public void setIdMapperUpdater(SessionIdMapperUpdater idMapperUpdater) { + this.idMapperUpdater = idMapperUpdater; + } } diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java index 281dd5901f..a74966b971 100755 --- a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java +++ b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java @@ -24,6 +24,7 @@ import org.apache.catalina.realm.GenericPrincipal; import org.jboss.logging.Logger; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement; import org.keycloak.adapters.tomcat.GenericPrincipalFactory; import org.keycloak.common.util.KeycloakUriBuilder; @@ -45,17 +46,20 @@ public class CatalinaSamlSessionStore implements SamlSessionStore { private final CatalinaUserSessionManagement sessionManagement; protected final GenericPrincipalFactory principalFactory; private final SessionIdMapper idMapper; + private final SessionIdMapperUpdater idMapperUpdater; protected final Request request; protected final AbstractSamlAuthenticatorValve valve; protected final HttpFacade facade; protected final SamlDeployment deployment; public CatalinaSamlSessionStore(CatalinaUserSessionManagement sessionManagement, GenericPrincipalFactory principalFactory, - SessionIdMapper idMapper, Request request, AbstractSamlAuthenticatorValve valve, HttpFacade facade, + SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater, + Request request, AbstractSamlAuthenticatorValve valve, HttpFacade facade, SamlDeployment deployment) { this.sessionManagement = sessionManagement; this.principalFactory = principalFactory; this.idMapper = idMapper; + this.idMapperUpdater = idMapperUpdater; this.request = request; this.valve = valve; this.facade = facade; @@ -89,11 +93,13 @@ public class CatalinaSamlSessionStore implements SamlSessionStore { Session sessionInternal = request.getSessionInternal(false); if (sessionInternal == null) return; HttpSession session = sessionInternal.getSession(); + List ids = new LinkedList(); if (session != null) { SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName()); if (samlSession != null) { if (samlSession.getSessionIndex() != null) { - idMapper.removeSession(session.getId()); + ids.add(session.getId()); + idMapperUpdater.removeSession(idMapper, session.getId()); } session.removeAttribute(SamlSession.class.getName()); } @@ -101,6 +107,7 @@ public class CatalinaSamlSessionStore implements SamlSessionStore { } sessionInternal.setPrincipal(null); sessionInternal.setAuthType(null); + logoutSessionIds(ids); } @Override @@ -111,7 +118,7 @@ public class CatalinaSamlSessionStore implements SamlSessionStore { ids.addAll(sessions); logoutSessionIds(ids); for (String id : ids) { - idMapper.removeSession(id); + idMapperUpdater.removeSession(idMapper, id); } } @@ -125,7 +132,7 @@ public class CatalinaSamlSessionStore implements SamlSessionStore { String sessionId = idMapper.getSessionFromSSO(id); if (sessionId != null) { sessionIds.add(sessionId); - idMapper.removeSession(sessionId); + idMapperUpdater.removeSession(idMapper, sessionId); } } @@ -141,7 +148,6 @@ public class CatalinaSamlSessionStore implements SamlSessionStore { @Override public boolean isLoggedIn() { Session session = request.getSessionInternal(false); - if (session == null) return false; if (session == null) { log.debug("session was null, returning null"); return false; @@ -193,7 +199,7 @@ public class CatalinaSamlSessionStore implements SamlSessionStore { request.setUserPrincipal(principal); request.setAuthType("KEYCLOAK-SAML"); String newId = changeSessionId(session); - idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), newId); + idMapperUpdater.map(idMapper, account.getSessionIndex(), account.getPrincipal().getSamlSubject(), newId); } diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/IdMapperUpdaterSessionListener.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/IdMapperUpdaterSessionListener.java new file mode 100644 index 0000000000..4fc78149fd --- /dev/null +++ b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/IdMapperUpdaterSessionListener.java @@ -0,0 +1,93 @@ +/* + * Copyright 2017 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.adapters.saml; + +import org.keycloak.adapters.spi.SessionIdMapper; + +import java.util.Objects; +import javax.servlet.http.*; + +/** + * + * @author hmlnarik + */ +public class IdMapperUpdaterSessionListener implements HttpSessionListener, HttpSessionAttributeListener { + + private final SessionIdMapper idMapper; + + public IdMapperUpdaterSessionListener(SessionIdMapper idMapper) { + this.idMapper = idMapper; + } + + @Override + public void sessionCreated(HttpSessionEvent hse) { + HttpSession session = hse.getSession(); + Object value = session.getAttribute(SamlSession.class.getName()); + map(session.getId(), value); + } + + @Override + public void sessionDestroyed(HttpSessionEvent hse) { + HttpSession session = hse.getSession(); + unmap(session.getId(), session.getAttribute(SamlSession.class.getName())); + } + + @Override + public void attributeAdded(HttpSessionBindingEvent hsbe) { + HttpSession session = hsbe.getSession(); + if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) { + map(session.getId(), hsbe.getValue()); + } + } + + @Override + public void attributeRemoved(HttpSessionBindingEvent hsbe) { + HttpSession session = hsbe.getSession(); + if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) { + unmap(session.getId(), hsbe.getValue()); + } + } + + @Override + public void attributeReplaced(HttpSessionBindingEvent hsbe) { + HttpSession session = hsbe.getSession(); + if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) { + unmap(session.getId(), hsbe.getValue()); + map(session.getId(), session.getAttribute(SamlSession.class.getName())); + } + } + + private void map(String sessionId, Object value) { + if (! (value instanceof SamlSession) || sessionId == null) { + return; + } + SamlSession account = (SamlSession) value; + + idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId); + } + + private void unmap(String sessionId, Object value) { + if (! (value instanceof SamlSession) || sessionId == null) { + return; + } + + SamlSession samlSession = (SamlSession) value; + if (samlSession.getSessionIndex() != null) { + idMapper.removeSession(sessionId); + } + } +} diff --git a/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/Tomcat8SamlSessionStore.java b/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/Tomcat8SamlSessionStore.java index a3d80fbdb1..57fef3e043 100755 --- a/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/Tomcat8SamlSessionStore.java +++ b/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/Tomcat8SamlSessionStore.java @@ -24,6 +24,7 @@ import org.keycloak.adapters.saml.CatalinaSamlSessionStore; import org.keycloak.adapters.saml.SamlDeployment; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement; import org.keycloak.adapters.tomcat.GenericPrincipalFactory; @@ -33,7 +34,7 @@ import org.keycloak.adapters.tomcat.GenericPrincipalFactory; */ public class Tomcat8SamlSessionStore extends CatalinaSamlSessionStore { public Tomcat8SamlSessionStore(CatalinaUserSessionManagement sessionManagement, GenericPrincipalFactory principalFactory, SessionIdMapper idMapper, Request request, AbstractSamlAuthenticatorValve valve, HttpFacade facade, SamlDeployment deployment) { - super(sessionManagement, principalFactory, idMapper, request, valve, facade, deployment); + super(sessionManagement, principalFactory, idMapper, SessionIdMapperUpdater.DIRECT, request, valve, facade, deployment); } @Override diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/IdMapperUpdaterSessionListener.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/IdMapperUpdaterSessionListener.java new file mode 100644 index 0000000000..692413e5aa --- /dev/null +++ b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/IdMapperUpdaterSessionListener.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017 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.adapters.saml.undertow; + +import org.keycloak.adapters.saml.SamlSession; +import org.keycloak.adapters.spi.SessionIdMapper; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.session.Session; +import io.undertow.server.session.SessionListener; +import java.util.Objects; + +/** + * + * @author hmlnarik + */ +public class IdMapperUpdaterSessionListener implements SessionListener { + + private final SessionIdMapper idMapper; + + public IdMapperUpdaterSessionListener(SessionIdMapper idMapper) { + this.idMapper = idMapper; + } + + @Override + public void sessionCreated(Session session, HttpServerExchange exchange) { + Object value = session.getAttribute(SamlSession.class.getName()); + map(session.getId(), value); + } + + @Override + public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) { + if (reason != SessionDestroyedReason.UNDEPLOY) { + unmap(session.getId(), session.getAttribute(SamlSession.class.getName())); + } + } + + @Override + public void attributeAdded(Session session, String name, Object value) { + if (Objects.equals(name, SamlSession.class.getName())) { + map(session.getId(), value); + } + } + + @Override + public void attributeUpdated(Session session, String name, Object newValue, Object oldValue) { + if (Objects.equals(name, SamlSession.class.getName())) { + unmap(session.getId(), oldValue); + map(session.getId(), newValue); + } + } + + @Override + public void attributeRemoved(Session session, String name, Object oldValue) { + if (Objects.equals(name, SamlSession.class.getName())) { + unmap(session.getId(), oldValue); + } + } + + @Override + public void sessionIdChanged(Session session, String oldSessionId) { + Object value = session.getAttribute(SamlSession.class.getName()); + if (value != null) { + unmap(oldSessionId, value); + map(session.getId(), value); + } + } + + private void map(String sessionId, Object value) { + if (! (value instanceof SamlSession) || sessionId == null) { + return; + } + SamlSession account = (SamlSession) value; + + idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId); + } + + private void unmap(String sessionId, Object value) { + if (! (value instanceof SamlSession) || sessionId == null) { + return; + } + + SamlSession samlSession = (SamlSession) value; + if (samlSession.getSessionIndex() != null) { + idMapper.removeSession(sessionId); + } + } + +} diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java index 0e6a1a1f80..4985dfb4ca 100755 --- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java +++ b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java @@ -154,7 +154,7 @@ public class SamlServletExtension implements ServletExtension { servletContext.setAttribute(SamlDeploymentContext.class.getName(), deploymentContext); UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement(); final ServletSamlAuthMech mech = createAuthMech(deploymentInfo, deploymentContext, userSessionManagement); - + mech.addTokenStoreUpdaters(deploymentInfo); // setup handlers diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlAuthMech.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlAuthMech.java index 88181712d8..b0a73391c3 100755 --- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlAuthMech.java +++ b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlAuthMech.java @@ -21,35 +21,89 @@ import io.undertow.security.api.SecurityContext; import io.undertow.server.HttpServerExchange; import io.undertow.servlet.handlers.ServletRequestContext; import io.undertow.util.Headers; + import org.keycloak.adapters.saml.SamlDeployment; import org.keycloak.adapters.saml.SamlDeploymentContext; import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.InMemorySessionIdMapper; -import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.*; import org.keycloak.adapters.undertow.ServletHttpFacade; import org.keycloak.adapters.undertow.UndertowHttpFacade; import org.keycloak.adapters.undertow.UndertowUserSessionManagement; +import io.undertow.servlet.api.DeploymentInfo; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.IOException; +import java.lang.reflect.*; +import java.util.Map; +import org.jboss.logging.Logger; /** * @author Bill Burke * @version $Revision: 1 $ */ public class ServletSamlAuthMech extends AbstractSamlAuthMech { + + private static final Logger LOG = Logger.getLogger(ServletSamlAuthMech.class); + protected SessionIdMapper idMapper = new InMemorySessionIdMapper(); + protected SessionIdMapperUpdater idMapperUpdater = SessionIdMapperUpdater.DIRECT; + public ServletSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) { super(deploymentContext, sessionManagement, errorPage); } + public void addTokenStoreUpdaters(DeploymentInfo deploymentInfo) { + deploymentInfo.addSessionListener(new IdMapperUpdaterSessionListener(idMapper)); // This takes care of HTTP sessions manipulated locally + SessionIdMapperUpdater updater = SessionIdMapperUpdater.EXTERNAL; + + try { + Map initParameters = deploymentInfo.getInitParameters(); + String idMapperSessionUpdaterClasses = initParameters == null + ? null + : initParameters.get("keycloak.sessionIdMapperUpdater.classes"); + if (idMapperSessionUpdaterClasses == null) { + return; + } + + for (String clazz : idMapperSessionUpdaterClasses.split("\\s*,\\s*")) { + if (! clazz.isEmpty()) { + updater = invokeAddTokenStoreUpdaterMethod(clazz, deploymentInfo, updater); + } + } + } finally { + setIdMapperUpdater(updater); + } + } + + private SessionIdMapperUpdater invokeAddTokenStoreUpdaterMethod(String idMapperSessionUpdaterClass, DeploymentInfo deploymentInfo, + SessionIdMapperUpdater previousIdMapperUpdater) { + try { + Class clazz = deploymentInfo.getClassLoader().loadClass(idMapperSessionUpdaterClass); + Method addTokenStoreUpdatersMethod = clazz.getMethod("addTokenStoreUpdaters", DeploymentInfo.class, SessionIdMapper.class, SessionIdMapperUpdater.class); + if (! Modifier.isStatic(addTokenStoreUpdatersMethod.getModifiers()) + || ! Modifier.isPublic(addTokenStoreUpdatersMethod.getModifiers()) + || ! SessionIdMapperUpdater.class.isAssignableFrom(addTokenStoreUpdatersMethod.getReturnType())) { + LOG.errorv("addTokenStoreUpdaters method in class {0} has to be public static. Ignoring class.", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } + + LOG.debugv("Initializing sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); + return (SessionIdMapperUpdater) addTokenStoreUpdatersMethod.invoke(null, deploymentInfo, idMapper, previousIdMapperUpdater); + } catch (ClassNotFoundException | NoSuchMethodException | SecurityException ex) { + LOG.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + LOG.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass); + return previousIdMapperUpdater; + } + } + @Override protected SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext) { - return new ServletSamlSessionStore(exchange, sessionManagement, securityContext, idMapper, deployment); + return new ServletSamlSessionStore(exchange, sessionManagement, securityContext, idMapper, idMapperUpdater, deployment); } @Override @@ -84,5 +138,11 @@ public class ServletSamlAuthMech extends AbstractSamlAuthMech { return null; } + public SessionIdMapperUpdater getIdMapperUpdater() { + return idMapperUpdater; + } + protected void setIdMapperUpdater(SessionIdMapperUpdater idMapperUpdater) { + this.idMapperUpdater = idMapperUpdater; + } } diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java index 72acda50a9..2bf2369ef1 100755 --- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java +++ b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java @@ -24,11 +24,13 @@ import io.undertow.server.session.SessionManager; import io.undertow.servlet.handlers.ServletRequestContext; import io.undertow.servlet.spec.HttpSessionImpl; import org.jboss.logging.Logger; + import org.keycloak.adapters.saml.SamlDeployment; import org.keycloak.adapters.saml.SamlSession; import org.keycloak.adapters.saml.SamlSessionStore; import org.keycloak.adapters.saml.SamlUtil; import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; import org.keycloak.adapters.undertow.ChangeSessionId; import org.keycloak.adapters.undertow.SavedRequest; import org.keycloak.adapters.undertow.ServletHttpFacade; @@ -44,6 +46,8 @@ import java.util.List; import java.util.Set; /** + * Session store manipulation methods per single HTTP exchange. + * * @author Bill Burke * @version $Revision: 1 $ */ @@ -55,17 +59,20 @@ public class ServletSamlSessionStore implements SamlSessionStore { private final UndertowUserSessionManagement sessionManagement; private final SecurityContext securityContext; private final SessionIdMapper idMapper; + private final SessionIdMapperUpdater idMapperUpdater; protected final SamlDeployment deployment; public ServletSamlSessionStore(HttpServerExchange exchange, UndertowUserSessionManagement sessionManagement, SecurityContext securityContext, - SessionIdMapper idMapper, SamlDeployment deployment) { + SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater, + SamlDeployment deployment) { this.exchange = exchange; this.sessionManagement = sessionManagement; this.securityContext = securityContext; this.idMapper = idMapper; this.deployment = deployment; + this.idMapperUpdater = idMapperUpdater; } @Override @@ -97,7 +104,7 @@ public class ServletSamlSessionStore implements SamlSessionStore { SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName()); if (samlSession != null) { if (samlSession.getSessionIndex() != null) { - idMapper.removeSession(session.getId()); + idMapperUpdater.removeSession(idMapper, session.getId()); } session.removeAttribute(SamlSession.class.getName()); } @@ -113,7 +120,7 @@ public class ServletSamlSessionStore implements SamlSessionStore { ids.addAll(sessions); logoutSessionIds(ids); for (String id : ids) { - idMapper.removeSession(id); + idMapperUpdater.removeSession(idMapper, id); } } @@ -127,7 +134,7 @@ public class ServletSamlSessionStore implements SamlSessionStore { String sessionId = idMapper.getSessionFromSSO(id); if (sessionId != null) { sessionIds.add(sessionId); - idMapper.removeSession(sessionId); + idMapperUpdater.removeSession(idMapper, sessionId); } } @@ -177,7 +184,7 @@ public class ServletSamlSessionStore implements SamlSessionStore { session.setAttribute(SamlSession.class.getName(), account); sessionManagement.login(servletRequestContext.getDeployment().getSessionManager()); String sessionId = changeSessionId(session); - idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId); + idMapperUpdater.map(idMapper, account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId); } diff --git a/adapters/saml/wildfly/wildfly-adapter/pom.xml b/adapters/saml/wildfly/wildfly-adapter/pom.xml index 463886dc5e..d798cab500 100755 --- a/adapters/saml/wildfly/wildfly-adapter/pom.xml +++ b/adapters/saml/wildfly/wildfly-adapter/pom.xml @@ -66,6 +66,10 @@ org.keycloak keycloak-jboss-adapter-core + + org.infinispan + infinispan-core + org.picketbox picketbox diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlAuthMech.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlAuthMech.java index bdf06063b7..ae4e242b6f 100755 --- a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlAuthMech.java +++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlAuthMech.java @@ -37,6 +37,6 @@ public class WildflySamlAuthMech extends ServletSamlAuthMech { @Override protected SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext) { - return new WildflySamlSessionStore(exchange, sessionManagement, securityContext, idMapper, deployment); + return new WildflySamlSessionStore(exchange, sessionManagement, securityContext, idMapper, getIdMapperUpdater(), deployment); } } diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlSessionStore.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlSessionStore.java index b4c213af6a..5f8d717d45 100755 --- a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlSessionStore.java +++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlSessionStore.java @@ -23,6 +23,7 @@ import org.keycloak.adapters.saml.SamlDeployment; import org.keycloak.adapters.saml.SamlSession; import org.keycloak.adapters.saml.undertow.ServletSamlSessionStore; import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; import org.keycloak.adapters.undertow.UndertowUserSessionManagement; /** @@ -31,8 +32,10 @@ import org.keycloak.adapters.undertow.UndertowUserSessionManagement; */ public class WildflySamlSessionStore extends ServletSamlSessionStore { public WildflySamlSessionStore(HttpServerExchange exchange, UndertowUserSessionManagement sessionManagement, - SecurityContext securityContext, SessionIdMapper idMapper, SamlDeployment resolvedDeployment) { - super(exchange, sessionManagement, securityContext, idMapper, resolvedDeployment); + SecurityContext securityContext, + SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater, + SamlDeployment resolvedDeployment) { + super(exchange, sessionManagement, securityContext, idMapper, idMapperUpdater, resolvedDeployment); } @Override diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/InfinispanSessionCacheIdMapperUpdater.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/InfinispanSessionCacheIdMapperUpdater.java new file mode 100644 index 0000000000..489d1d5c67 --- /dev/null +++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/InfinispanSessionCacheIdMapperUpdater.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017 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.adapters.saml.wildfly.infinispan; + +import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; + +import io.undertow.servlet.api.DeploymentInfo; +import java.util.*; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import org.infinispan.Cache; +import org.infinispan.configuration.cache.CacheMode; +import org.infinispan.configuration.cache.Configuration; +import org.infinispan.manager.EmbeddedCacheManager; +import org.jboss.logging.Logger; + +/** + * + * @author hmlnarik + */ +public class InfinispanSessionCacheIdMapperUpdater { + + private static final Logger LOG = Logger.getLogger(InfinispanSessionCacheIdMapperUpdater.class); + + public static final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container/web"; + + private static final String DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheContainerJndi"; + private static final String DEPLOYMENT_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.deploymentCacheName"; + private static final String SSO_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheName"; + + public static SessionIdMapperUpdater addTokenStoreUpdaters(DeploymentInfo deploymentInfo, SessionIdMapper mapper, SessionIdMapperUpdater previousIdMapperUpdater) { + boolean distributable = Objects.equals( + deploymentInfo.getSessionManagerFactory().getClass().getName(), + "org.wildfly.clustering.web.undertow.session.DistributableSessionManagerFactory" + ); + + if (! distributable) { + LOG.warnv("Deployment {0} does not use supported distributed session cache mechanism", deploymentInfo.getDeploymentName()); + return previousIdMapperUpdater; + } + + Map initParameters = deploymentInfo.getInitParameters(); + String cacheContainerLookup = (initParameters != null && initParameters.get(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME) != null) + ? initParameters.get(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME) + : DEFAULT_CACHE_CONTAINER_JNDI_NAME; + boolean deploymentSessionCacheNamePreset = initParameters != null && initParameters.get(DEPLOYMENT_CACHE_NAME_PARAM_NAME) != null; + String deploymentSessionCacheName = deploymentSessionCacheNamePreset + ? initParameters.get(DEPLOYMENT_CACHE_NAME_PARAM_NAME) + : deploymentInfo.getDeploymentName(); + boolean ssoCacheNamePreset = initParameters != null && initParameters.get(SSO_CACHE_NAME_PARAM_NAME) != null; + String ssoCacheName = ssoCacheNamePreset + ? initParameters.get(SSO_CACHE_NAME_PARAM_NAME) + : deploymentSessionCacheName + ".ssoCache"; + + try { + EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup); + + Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(ssoCacheName); + if (ssoCacheConfiguration == null) { + Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName); + if (cacheConfiguration == null) { + LOG.debugv("Using default cache container configuration for SSO cache. lookup={0}, looked up configuration of cache={1}", cacheContainerLookup, deploymentSessionCacheName); + ssoCacheConfiguration = cacheManager.getDefaultCacheConfiguration(); + } else { + LOG.debugv("Using distributed HTTP session cache configuration for SSO cache. lookup={0}, configuration taken from cache={1}", cacheContainerLookup, deploymentSessionCacheName); + ssoCacheConfiguration = cacheConfiguration; + cacheManager.defineConfiguration(ssoCacheName, ssoCacheConfiguration); + } + } else { + LOG.debugv("Using custom configuration of SSO cache. lookup={0}, cache name={1}", cacheContainerLookup, ssoCacheName); + } + + CacheMode ssoCacheMode = ssoCacheConfiguration.clustering().cacheMode(); + if (ssoCacheMode != CacheMode.REPL_ASYNC && ssoCacheMode != CacheMode.REPL_SYNC) { + LOG.warnv("SSO cache mode is {0}, it is recommended to use replicated mode instead", ssoCacheConfiguration.clustering().cacheModeString()); + } + + Cache ssoCache = cacheManager.getCache(ssoCacheName, true); + ssoCache.addListener(new SsoSessionCacheListener(mapper)); + + LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, deploymentSessionCacheName); + + SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater); + deploymentInfo.addSessionListener(updater); + + return updater; + } catch (NamingException ex) { + LOG.warnv("Failed to obtain distributed session cache container, lookup={0}", cacheContainerLookup); + return previousIdMapperUpdater; + } + } +} diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoCacheSessionIdMapperUpdater.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoCacheSessionIdMapperUpdater.java new file mode 100644 index 0000000000..e2b8ba22d4 --- /dev/null +++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoCacheSessionIdMapperUpdater.java @@ -0,0 +1,96 @@ +/* + * Copyright 2017 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.adapters.saml.wildfly.infinispan; + +import org.keycloak.adapters.saml.SamlSession; +import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.session.Session; +import io.undertow.server.session.SessionListener; +import org.infinispan.Cache; + +/** + * + * @author hmlnarik + */ +public class SsoCacheSessionIdMapperUpdater implements SessionIdMapperUpdater, SessionListener { + + private final SessionIdMapperUpdater delegate; + /** + * Cache where key is a HTTP session ID, and value is a pair (user session ID, principal name) of Strings. + */ + private final Cache httpSessionToSsoCache; + + public SsoCacheSessionIdMapperUpdater(Cache httpSessionToSsoCache, SessionIdMapperUpdater previousIdMapperUpdater) { + this.delegate = previousIdMapperUpdater; + this.httpSessionToSsoCache = httpSessionToSsoCache; + } + + // SessionIdMapperUpdater methods + + @Override + public void clear(SessionIdMapper idMapper) { + httpSessionToSsoCache.clear(); + this.delegate.clear(idMapper); + } + + @Override + public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) { + httpSessionToSsoCache.put(httpSessionId, new String[] {sso, principal}); + this.delegate.map(idMapper, sso, principal, httpSessionId); + } + + @Override + public void removeSession(SessionIdMapper idMapper, String httpSessionId) { + httpSessionToSsoCache.remove(httpSessionId); + this.delegate.removeSession(idMapper, httpSessionId); + } + + // Undertow HTTP session listener methods + + @Override + public void sessionCreated(Session session, HttpServerExchange exchange) { + } + + @Override + public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) { + } + + @Override + public void attributeAdded(Session session, String name, Object value) { + } + + @Override + public void attributeUpdated(Session session, String name, Object newValue, Object oldValue) { + } + + @Override + public void attributeRemoved(Session session, String name, Object oldValue) { + } + + @Override + public void sessionIdChanged(Session session, String oldSessionId) { + this.httpSessionToSsoCache.remove(oldSessionId); + Object value = session.getAttribute(SamlSession.class.getName()); + if (value instanceof SamlSession) { + SamlSession sess = (SamlSession) value; + httpSessionToSsoCache.put(session.getId(), new String[] {sess.getSessionIndex(), sess.getPrincipal().getSamlSubject()}); + } + } +} diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java new file mode 100644 index 0000000000..ccd102e713 --- /dev/null +++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java @@ -0,0 +1,131 @@ +/* + * Copyright 2017 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.adapters.saml.wildfly.infinispan; + +import org.keycloak.adapters.spi.SessionIdMapper; + +import java.util.*; +import java.util.concurrent.*; +import org.infinispan.notifications.Listener; +import org.infinispan.notifications.cachelistener.annotation.*; +import org.infinispan.notifications.cachelistener.event.*; +import org.infinispan.notifications.cachemanagerlistener.annotation.CacheStarted; +import org.infinispan.notifications.cachemanagerlistener.annotation.CacheStopped; +import org.infinispan.notifications.cachemanagerlistener.event.CacheStartedEvent; +import org.infinispan.notifications.cachemanagerlistener.event.CacheStoppedEvent; +import org.jboss.logging.Logger; + +/** + * + * @author hmlnarik + */ +@Listener +public class SsoSessionCacheListener { + + private static final Logger LOG = Logger.getLogger(SsoSessionCacheListener.class); + + private final ConcurrentMap> map = new ConcurrentHashMap<>(); + + private final SessionIdMapper idMapper; + + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + public SsoSessionCacheListener(SessionIdMapper idMapper) { + this.idMapper = idMapper; + } + + @TransactionRegistered + public void startTransaction(TransactionRegisteredEvent event) { + map.put(event.getGlobalTransaction().globalId(), new ConcurrentLinkedQueue()); + } + + @CacheStarted + public void cacheStarted(CacheStartedEvent event) { + this.executor = Executors.newSingleThreadExecutor(); + } + + @CacheStopped + public void cacheStopped(CacheStoppedEvent event) { + this.executor.shutdownNow(); + } + + @CacheEntryCreated + @CacheEntryRemoved + public void addEvent(TransactionalEvent event) { + if (event.isPre() == false) { + map.get(event.getGlobalTransaction().globalId()).add(event); + } + } + + @TransactionCompleted + public void endTransaction(TransactionCompletedEvent event) { + Queue events = map.remove(event.getGlobalTransaction().globalId()); + + if (events == null || ! event.isTransactionSuccessful()) { + return; + } + + if (event.isOriginLocal()) { + // Local events are processed by local HTTP session listener + return; + } + + for (final Event e : events) { + switch (e.getType()) { + case CACHE_ENTRY_CREATED: + this.executor.submit(new Runnable() { + @Override public void run() { + cacheEntryCreated((CacheEntryCreatedEvent) e); + } + }); + break; + + case CACHE_ENTRY_REMOVED: + this.executor.submit(new Runnable() { + @Override public void run() { + cacheEntryRemoved((CacheEntryRemovedEvent) e); + } + }); + break; + } + } + } + + private void cacheEntryCreated(CacheEntryCreatedEvent event) { + if (! (event.getKey() instanceof String) || ! (event.getValue() instanceof String[])) { + return; + } + String httpSessionId = (String) event.getKey(); + String[] value = (String[]) event.getValue(); + String ssoId = value[0]; + String principal = value[1]; + + LOG.tracev("cacheEntryCreated {0}:{1}", httpSessionId, ssoId); + + this.idMapper.map(ssoId, principal, httpSessionId); + } + + private void cacheEntryRemoved(CacheEntryRemovedEvent event) { + if (! (event.getKey() instanceof String)) { + return; + } + + LOG.tracev("cacheEntryRemoved {0}", event.getKey()); + + this.idMapper.removeSession((String) event.getKey()); + } +} diff --git a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapper.java b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapper.java index 3b467d70ef..b50e37b791 100755 --- a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapper.java +++ b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapper.java @@ -24,15 +24,43 @@ import java.util.Set; * @version $Revision: 1 $ */ public interface SessionIdMapper { + /** + * Returns {@code true} if the mapper contains mapping for the given HTTP session ID. + * @param id + * @return + */ boolean hasSession(String id); + /** + * Clears all mappings from this mapper. + */ void clear(); + /** + * Returns set of HTTP session IDs for the given principal. + * @param principal Principal + * @return + */ Set getUserSessions(String principal); + /** + * Returns HTTP session ID from the given user session ID. + * @param sso User session ID + * @return + */ String getSessionFromSSO(String sso); + /** + * Establishes mapping between user session ID, principal and HTTP session ID. + * @param sso User session ID + * @param principal Principal + * @param session HTTP session ID + */ void map(String sso, String principal, String session); + /** + * Removes mappings for the given HTTP session ID. + * @param session HTTP session ID. + */ void removeSession(String session); } diff --git a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapperUpdater.java b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapperUpdater.java new file mode 100644 index 0000000000..a0ea2ffdb4 --- /dev/null +++ b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapperUpdater.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017 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.adapters.spi; + +/** + * Classes implementing this interface represent a mechanism for updating {@link SessionIdMapper} entries. + * @author hmlnarik + */ +public interface SessionIdMapperUpdater { + /** + * {@link SessionIdMapper} entries are updated directly. + */ + public static final SessionIdMapperUpdater DIRECT = new SessionIdMapperUpdater() { + @Override public void clear(SessionIdMapper idMapper) { + idMapper.clear(); + } + + @Override public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) { + idMapper.map(sso, principal, httpSessionId); + } + + @Override public void removeSession(SessionIdMapper idMapper, String httpSessionId) { + idMapper.removeSession(httpSessionId); + } + }; + + /** + * Only HTTP session is manipulated with, {@link SessionIdMapper} entries are not updated by this updater and + * they have to be updated by some other means, e.g. by some listener of HTTP session changes. + */ + public static final SessionIdMapperUpdater EXTERNAL = new SessionIdMapperUpdater() { + @Override public void clear(SessionIdMapper idMapper) { } + + @Override public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) { } + + @Override public void removeSession(SessionIdMapper idMapper, String httpSessionId) { } + }; + + /** + * Delegates to {@link SessionIdMapper#clear} method.. + */ + public abstract void clear(SessionIdMapper idMapper); + + /** + * Delegates to {@link SessionIdMapper#map} method. + * @param idMapper Mapper + * @param sso User session ID + * @param principal Principal + * @param session HTTP session ID + */ + public abstract void map(SessionIdMapper idMapper, String sso, String principal, String session); + + /** + * Delegates to {@link SessionIdMapper#removeSession} method. + * @param idMapper Mapper + * @param session HTTP session ID. + */ + public abstract void removeSession(SessionIdMapper idMapper, String session); +} diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml index b85e56f2f0..fd9d2e4089 100755 --- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml +++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml @@ -42,6 +42,9 @@ + + + diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml index ee00fcc1ae..6cb3c73e76 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml @@ -42,6 +42,10 @@ + + + + diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeServletDistributable.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeServletDistributable.java new file mode 100644 index 0000000000..ed547c09ba --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeServletDistributable.java @@ -0,0 +1,49 @@ +/* + * 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.adapter.page; + +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.arquillian.test.api.ArquillianResource; + +import java.net.URL; +import org.openqa.selenium.WebDriver; + +/** + * @author mhajas + */ +public class EmployeeServletDistributable extends SAMLServlet { + public static final String DEPLOYMENT_NAME = "employee-distributable"; + + @ArquillianResource + @OperateOnDeployment(DEPLOYMENT_NAME) + private URL url; + + public EmployeeServletDistributable(WebDriver driver) { + super(); + this.driver = driver; + } + + @Override + public URL getInjectedUrl() { + return url; + } + + public void setUrl(URL url) { + this.url = url; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java index cc4a419725..2f226b929b 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java @@ -28,7 +28,7 @@ public abstract class SAMLServlet extends AbstractPageWithInjectedUrl { public void logout() { driver.navigate().to(getUriBuilder().queryParam("GLO", "true").build().toASCIIString()); - getUriBuilder().replaceQueryParam("GLO", null); + getUriBuilder().replaceQueryParam("GLO"); pause(300); } @@ -36,7 +36,7 @@ public abstract class SAMLServlet extends AbstractPageWithInjectedUrl { if (check) { getUriBuilder().queryParam("checkRoles", true); } else { - getUriBuilder().replaceQueryParam("checkRoles", null); + getUriBuilder().replaceQueryParam("checkRoles"); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java index bd6e5a051f..e3950d3306 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java @@ -51,7 +51,7 @@ public class DeploymentTargetModifier extends AnnotationDeploymentScenarioGenera if (appServerQualifier != null && !appServerQualifier.isEmpty()) { for (DeploymentDescription deployment : deployments) { - if (deployment.getTarget() == null || !deployment.getTarget().getName().equals(appServerQualifier)) { + if (deployment.getTarget() == null || !deployment.getTarget().getName().startsWith(appServerQualifier)) { log.debug("Setting target container for " + deployment.getName() + ": " + appServerQualifier); deployment.setTarget(new TargetDescription(appServerQualifier)); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java index cc018d2267..235d15f8e0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java @@ -65,11 +65,11 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest { return deployment; } - protected static WebArchive samlServletDeployment(String name, Class... servletClasses) { + public static WebArchive samlServletDeployment(String name, Class... servletClasses) { return samlServletDeployment(name, "web.xml", servletClasses); } - protected static WebArchive samlServletDeployment(String name, String webXMLPath, Class... servletClasses) { + public static WebArchive samlServletDeployment(String name, String webXMLPath, Class... servletClasses) { String baseSAMLPath = "/adapter-test/keycloak-saml/"; String webInfPath = baseSAMLPath + name + "/WEB-INF/"; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java new file mode 100644 index 0000000000..3868fb5cee --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java @@ -0,0 +1,237 @@ +/* + * Copyright 2017 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.cluster; + +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.*; +import org.keycloak.testsuite.Retry; +import org.keycloak.testsuite.adapter.page.EmployeeServletDistributable; +import org.keycloak.testsuite.adapter.page.SAMLServlet; +import org.keycloak.testsuite.auth.page.AuthRealm; +import org.keycloak.testsuite.auth.page.login.*; +import org.keycloak.testsuite.page.AbstractPage; +import org.keycloak.testsuite.util.WaitUtils; + +import io.undertow.Undertow; +import io.undertow.server.handlers.ResponseCodeHandler; +import io.undertow.server.handlers.proxy.LoadBalancingProxyClient; +import io.undertow.server.handlers.proxy.ProxyHandler; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.function.Consumer; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.jboss.arquillian.container.test.api.*; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.junit.*; + +import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; + +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.PageFactory; +import org.openqa.selenium.support.ui.WebDriverWait; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.AbstractAuthTest.createUserRepresentation; +import static org.keycloak.testsuite.admin.Users.setPasswordFor; +import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getNearestSuperclassWithAnnotation; +import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO; +import static org.keycloak.testsuite.util.IOUtil.loadRealm; +import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; + +/** + * + * @author hmlnarik + */ +public abstract class AbstractSAMLAdapterClusterTest extends AbstractServletsAdapterTest { + + protected static final String NODE_1_NAME = "ha-node-1"; + protected static final String NODE_2_NAME = "ha-node-2"; + + protected final String NODE_1_SERVER_NAME = getAppServerId() + "-" + NODE_1_NAME; + protected final String NODE_2_SERVER_NAME = getAppServerId() + "-" + NODE_2_NAME; + + protected static final int PORT_OFFSET_NODE_REVPROXY = NumberUtils.toInt(System.getProperty("app.server.reverse-proxy.port.offset"), -1); + protected static final int HTTP_PORT_NODE_REVPROXY = 8080 + PORT_OFFSET_NODE_REVPROXY; + protected static final int PORT_OFFSET_NODE_1 = NumberUtils.toInt(System.getProperty("app.server.1.port.offset"), -1); + protected static final int HTTP_PORT_NODE_1 = 8080 + PORT_OFFSET_NODE_1; + protected static final int PORT_OFFSET_NODE_2 = NumberUtils.toInt(System.getProperty("app.server.2.port.offset"), -1); + protected static final int HTTP_PORT_NODE_2 = 8080 + PORT_OFFSET_NODE_2; + protected static final URI NODE_1_URI = URI.create("http://localhost:" + HTTP_PORT_NODE_1); + protected static final URI NODE_2_URI = URI.create("http://localhost:" + HTTP_PORT_NODE_2); + + @BeforeClass + public static void checkPropertiesSet() { + Assume.assumeThat(PORT_OFFSET_NODE_1, not(is(-1))); + Assume.assumeThat(PORT_OFFSET_NODE_2, not(is(-1))); + Assume.assumeThat(PORT_OFFSET_NODE_REVPROXY, not(is(-1))); + } + + protected static void prepareServerDirectory(String targetSubdirectory) throws IOException { + Path path = Paths.get(System.getProperty("app.server.home"), targetSubdirectory); + File targetSubdirFile = path.toFile(); + FileUtils.deleteDirectory(targetSubdirFile); + FileUtils.forceMkdir(targetSubdirFile); + FileUtils.copyDirectoryToDirectory(Paths.get(System.getProperty("app.server.home"), "standalone", "deployments").toFile(), targetSubdirFile); + } + protected LoadBalancingProxyClient loadBalancerToNodes; + protected Undertow reverseProxyToNodes; + + @ArquillianResource + protected ContainerController controller; + + @ArquillianResource + protected Deployer deployer; + + @Page + LoginActions loginActionsPage; + + @Override + public void addTestRealms(List testRealms) { + testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml-behind-lb.json")); + } + + @Before + public void prepareReverseProxy() throws Exception { + loadBalancerToNodes = new LoadBalancingProxyClient().addHost(NODE_1_URI, NODE_1_NAME).setConnectionsPerThread(10); + reverseProxyToNodes = Undertow.builder().addHttpListener(HTTP_PORT_NODE_REVPROXY, "localhost").setIoThreads(2).setHandler(new ProxyHandler(loadBalancerToNodes, 5000, ResponseCodeHandler.HANDLE_404)).build(); + reverseProxyToNodes.start(); + } + + @After + public void stopReverseProxy() { + reverseProxyToNodes.stop(); + } + + @Before + public void startServer() throws Exception { + prepareServerDirectory("standalone-" + NODE_1_NAME); + controller.start(NODE_1_SERVER_NAME); + prepareWorkerNode(Integer.valueOf(System.getProperty("app.server.1.management.port"))); + prepareServerDirectory("standalone-" + NODE_2_NAME); + controller.start(NODE_2_SERVER_NAME); + prepareWorkerNode(Integer.valueOf(System.getProperty("app.server.2.management.port"))); + deployer.deploy(EmployeeServletDistributable.DEPLOYMENT_NAME); + deployer.deploy(EmployeeServletDistributable.DEPLOYMENT_NAME + "_2"); + } + + protected abstract void prepareWorkerNode(Integer managementPort) throws Exception; + + @After + public void stopServer() { + controller.stop(NODE_1_SERVER_NAME); + controller.stop(NODE_2_SERVER_NAME); + } + + @Override + public void setDefaultPageUriParameters() { + super.setDefaultPageUriParameters(); + + testRealmSAMLPostLoginPage.setAuthRealm(DEMO); + loginPage.setAuthRealm(DEMO); + loginActionsPage.setAuthRealm(DEMO); + } + + protected void testLogoutViaSessionIndex(URL employeeUrl, Consumer logoutFunction) { + EmployeeServletDistributable page = PageFactory.initElements(driver, EmployeeServletDistributable.class); + page.setUrl(employeeUrl); + page.getUriBuilder().port(HTTP_PORT_NODE_REVPROXY); + + UserRepresentation bburkeUser = createUserRepresentation("bburke", "bburke@redhat.com", "Bill", "Burke", true); + setPasswordFor(bburkeUser, CredentialRepresentation.PASSWORD); + + assertSuccessfulLogin(page, bburkeUser, testRealmSAMLPostLoginPage, "principal=bburke"); + + updateProxy(NODE_2_NAME, NODE_2_URI, NODE_1_URI); + logoutFunction.accept(page); + delayedCheckLoggedOut(page, loginActionsPage); + + updateProxy(NODE_1_NAME, NODE_1_URI, NODE_2_URI); + delayedCheckLoggedOut(page, loginActionsPage); + } + + @Test + public void testBackchannelLogout(@ArquillianResource + @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception { + testLogoutViaSessionIndex(employeeUrl, (EmployeeServletDistributable page) -> { + RealmResource demoRealm = adminClient.realm(DEMO); + String bburkeId = ApiUtil.findUserByUsername(demoRealm, "bburke").getId(); + demoRealm.users().get(bburkeId).logout(); + log.infov("Logged out via admin console"); + }); + } + + @Test + public void testFrontchannelLogout(@ArquillianResource + @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception { + testLogoutViaSessionIndex(employeeUrl, (EmployeeServletDistributable page) -> { + page.logout(); + log.infov("Logged out via application"); + }); + } + + protected void updateProxy(String hostToPointToName, URI hostToPointToUri, URI hostToRemove) { + loadBalancerToNodes.removeHost(hostToRemove); + loadBalancerToNodes.addHost(hostToPointToUri, hostToPointToName); + log.infov("Reverse proxy will direct requests to {0}", hostToPointToUri); + } + + protected void assertSuccessfulLogin(SAMLServlet page, UserRepresentation user, Login loginPage, String expectedString) { + page.navigateTo(); + assertCurrentUrlStartsWith(loginPage); + loginPage.form().login(user); + WebDriverWait wait = new WebDriverWait(driver, WaitUtils.PAGELOAD_TIMEOUT_MILLIS / 1000); + wait.until((WebDriver d) -> d.getPageSource().contains(expectedString)); + } + + protected void delayedCheckLoggedOut(AbstractPage page, AuthRealm loginPage) { + Retry.execute(() -> { + try { + checkLoggedOut(page, loginPage); + } catch (AssertionError | TimeoutException ex) { + driver.navigate().refresh(); + log.debug("[Retriable] Timed out waiting for login page"); + throw new RuntimeException(ex); + } + }, 10, 100); + } + + protected void checkLoggedOut(AbstractPage page, AuthRealm loginPage) { + page.navigateTo(); + WaitUtils.waitForPageToLoad(driver); + assertCurrentUrlStartsWith(loginPage); + } + + private String getAppServerId() { + Class annotatedClass = getNearestSuperclassWithAnnotation(this.getClass(), AppServerContainer.class); + + return (annotatedClass == null ? "" + : annotatedClass.getAnnotation(AppServerContainer.class).value()); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/keycloak-saml.xml new file mode 100644 index 0000000000..60f48de3ec --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/keycloak-saml.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml-behind-lb.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml-behind-lb.json new file mode 100644 index 0000000000..ae10da28d9 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml-behind-lb.json @@ -0,0 +1,164 @@ +{ + "id": "demo", + "realm": "demo", + "enabled": true, + "sslRequired": "external", + "registrationAllowed": true, + "resetPasswordAllowed": true, + "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" ], + "defaultRoles": [ "user" ], + "smtpServer": { + "from": "auto@keycloak.org", + "host": "localhost", + "port":"3025" + }, + "users" : [ + { + "username" : "bburke", + "enabled": true, + "email" : "bburke@redhat.com", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "attributes" : { + "phone": "617" + }, + "realmRoles": ["manager", "user"], + "applicationRoles": { + "http://localhost:8580/employee-distributable/": [ "employee" ] + } + }, + { + "username" : "unauthorized", + "enabled": true, + "email" : "unauthorized@redhat.com", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + }, + { + "username" : "topGroupUser", + "enabled": true, + "email" : "top@redhat.com", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "groups": [ + "/top" + ] + }, + { + "username" : "level2GroupUser", + "enabled": true, + "email" : "level2@redhat.com", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "groups": [ + "/top/level2" + ] + } + ], + "clients": [ + { + "clientId": "http://localhost:8580/employee-distributable/", + "enabled": true, + "protocol": "saml", + "fullScopeAllowed": true, + "baseUrl": "http://localhost:8580/employee-distributable", + "redirectUris": [ + "http://localhost:8580/employee-distributable/*" + ], + "attributes": { + "saml_assertion_consumer_url_post": "http://localhost:8580/employee-distributable/saml", + "saml_assertion_consumer_url_redirect": "http://localhost:8580/employee-distributable/saml", + "saml_single_logout_service_url_post": "http://localhost:8580/employee-distributable/saml", + "saml_single_logout_service_url_redirect": "http://localhost:8580/employee-distributable/saml", + "saml.authnstatement": "true" + }, + "protocolMappers": [ + { + "name": "email", + "protocol": "saml", + "protocolMapper": "saml-user-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "friendly.name": "email", + "attribute.name": "urn:oid:1.2.840.113549.1.9.1", + "attribute.nameformat": "URI Reference" + } + }, + { + "name": "phone", + "protocol": "saml", + "protocolMapper": "saml-user-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "phone", + "attribute.name": "phone", + "attribute.nameformat": "Basic" + } + }, + { + "name": "role-list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "attribute.name": "Role", + "attribute.nameformat": "Basic", + "single": "false" + } + } + ] + } + ], + "groups" : [ + { + "name": "top", + "attributes": { + "topAttribute": ["true"] + + }, + "realmRoles": ["manager"], + "subGroups": [ + { + "name": "level2", + "realmRoles": ["user"], + "attributes": { + "level2Attribute": ["true"] + + } + } + ] + } + ], + + "roles" : { + "realm" : [ + { + "name": "manager", + "description": "Have Manager privileges" + }, + { + "name": "user", + "description": "Have User privileges" + } + ], + "application" : { + "http://localhost:8580/employee-distributable/" : [ + { + "name": "employee", + "description": "Have Employee privileges" + } + ] + } + } +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl b/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl index 74541a0193..30e7dba1f0 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl @@ -48,6 +48,53 @@ + + + true + org.jboss.as.arquillian.container.managed.ManagedDeployableContainer + ${app.server.home} + ${app.server.java.home} + ${app.server.home}/standalone-ha-node-1 + standalone-ha.xml + + -Djboss.socket.binding.port-offset=${app.server.1.port.offset} + -Djboss.node.name=ha-node-1 + ${adapter.test.props} + + + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=7901 + ${app.server.memory.settings} + -Djava.net.preferIPv4Stack=true + + ${app.server.management.protocol} + ${app.server.1.management.port} + ${app.server.startup.timeout} + + + + + + true + org.jboss.as.arquillian.container.managed.ManagedDeployableContainer + ${app.server.home} + ${app.server.java.home} + ${app.server.home}/standalone-ha-node-2 + standalone-ha.xml + + -Djboss.socket.binding.port-offset=${app.server.2.port.offset} + -Djboss.node.name=ha-node-2 + ${adapter.test.props} + + + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=7902 + ${app.server.memory.settings} + -Djava.net.preferIPv4Stack=true + + ${app.server.management.protocol} + ${app.server.2.management.port} + ${app.server.startup.timeout} + + diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml index 7c56a488c2..35f9501783 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml @@ -30,12 +30,29 @@ integration-arquillian-tests-adapters-eap6 Adapter Tests - JBoss - EAP 6 - + + + + org.wildfly.extras.creaper + creaper-core + test + 1.5.0 + + + org.wildfly.core + wildfly-cli + test + 2.2.0.Final + + + eap6 remote ${app.server.management.port.jmx} + ${app.server.1.management.port.jmx} + ${app.server.2.management.port.jmx} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/cluster/EAP6SAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/cluster/EAP6SAMLAdapterClusterTest.java new file mode 100644 index 0000000000..f0a166bea4 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/cluster/EAP6SAMLAdapterClusterTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2017 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.cluster; + +import org.keycloak.testsuite.adapter.page.EmployeeServletDistributable; +import org.keycloak.testsuite.arquillian.annotation.*; + +import java.io.*; + +import org.keycloak.testsuite.adapter.servlet.cluster.AbstractSAMLAdapterClusterTest; +import org.keycloak.testsuite.adapter.servlet.SendUsernameServlet; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.TargetsContainer; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.wildfly.extras.creaper.core.*; +import org.wildfly.extras.creaper.core.online.*; +import org.wildfly.extras.creaper.core.online.operations.*; + +import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment; + +/** + * + * @author hmlnarik + */ +@AppServerContainer("app-server-eap6") +public class EAP6SAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTest { + + @TargetsContainer(value = "app-server-eap6-" + NODE_1_NAME) + @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME, managed = false) + protected static WebArchive employee() { + return samlServletDeployment(EmployeeServletDistributable.DEPLOYMENT_NAME, EmployeeServletDistributable.DEPLOYMENT_NAME + "/WEB-INF/web.xml", SendUsernameServlet.class); + } + + @TargetsContainer(value = "app-server-eap6-" + NODE_2_NAME) + @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME + "_2", managed = false) + protected static WebArchive employee2() { + return employee(); + } + + @Override + protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException { + log.infov("Preparing worker node ({0})", managementPort); + + OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions + .standalone() + .hostAndPort("localhost", managementPort) + .protocol(ManagementProtocol.REMOTE) + .build()); + Operations op = new Operations(clientWorkerNodeClient); + + Batch b = new Batch(); + Address tcppingStack = Address + .subsystem("jgroups") + .and("stack", "tcpping"); + b.add(tcppingStack); + b.add(tcppingStack.and("transport", "TRANSPORT"), Values.of("socket-binding", "jgroups-tcp").and("type", "TCP")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "TCPPING")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "initial_hosts"), Values.of("value", "localhost[" + (7600 + PORT_OFFSET_NODE_1) + "],localhost[" + (7600 + PORT_OFFSET_NODE_2) + "]")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "port_range"), Values.of("value", "0")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "num_initial_members"), Values.of("value", "2")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "timeout"), Values.of("value", "3000")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "MERGE2")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "FD_SOCK").and("socket-binding", "jgroups-tcp-fd")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "FD")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "VERIFY_SUSPECT")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "pbcast.NAKACK")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "UNICAST2")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "pbcast.STABLE")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "pbcast.GMS")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "UFC")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "MFC")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "FRAG2")); + b.invoke("add-protocol", tcppingStack, Values.of("type", "RSVP")); + Assert.assertTrue("Could not add TCPPING JGroups stack", op.batch(b).isSuccess()); + + Assert.assertTrue(op.writeAttribute(Address.subsystem("jgroups"), "default-stack", "tcpping").isSuccess()); + Assert.assertTrue(op.writeAttribute(Address.subsystem("web"), "instance-id", "${jboss.node.name}").isSuccess()); + Assert.assertTrue(op.add(Address.extension("org.keycloak.keycloak-saml-adapter-subsystem"), Values.of("module", "org.keycloak.keycloak-saml-adapter-subsystem")).isSuccess()); + Assert.assertTrue(op.add(Address.subsystem("keycloak-saml")).isSuccess()); + + clientWorkerNodeClient.execute("reload"); + + log.infov("Worker node ({0}) Prepared", managementPort); + } + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml new file mode 100644 index 0000000000..767eb6c161 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml @@ -0,0 +1,62 @@ + + + + + + + + + + %CONTEXT_PATH% + + + javax.ws.rs.core.Application + /* + + + + /error.html + + + + + Application + /* + + + manager + + + + + KEYCLOAK-SAML + demo + + + + manager + + + + keycloak.sessionIdMapperUpdater.classes + org.keycloak.adapters.saml.jbossweb.infinispan.InfinispanSessionCacheIdMapperUpdater + + diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml index 885a052904..912947ff44 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml @@ -30,7 +30,22 @@ integration-arquillian-tests-adapters-wildfly Adapter Tests - JBoss - Wildfly - + + + + org.wildfly.extras.creaper + creaper-core + test + 1.5.0 + + + org.wildfly.core + wildfly-cli + test + 2.2.0.Final + + + wildfly diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/cluster/WildflySAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/cluster/WildflySAMLAdapterClusterTest.java new file mode 100644 index 0000000000..eb7973c37d --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/cluster/WildflySAMLAdapterClusterTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017 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.cluster; + +import org.keycloak.testsuite.adapter.page.EmployeeServletDistributable; +import org.keycloak.testsuite.arquillian.annotation.*; + +import java.io.*; + +import org.keycloak.testsuite.adapter.servlet.cluster.AbstractSAMLAdapterClusterTest; +import org.keycloak.testsuite.adapter.servlet.SendUsernameServlet; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.TargetsContainer; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.wildfly.extras.creaper.core.*; +import org.wildfly.extras.creaper.core.online.*; +import org.wildfly.extras.creaper.core.online.operations.*; + +import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment; + +/** + * + * @author hmlnarik + */ +@AppServerContainer("app-server-wildfly") +public class WildflySAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTest { + + @TargetsContainer(value = "app-server-wildfly-" + NODE_1_NAME) + @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME, managed = false) + protected static WebArchive employee() { + return samlServletDeployment(EmployeeServletDistributable.DEPLOYMENT_NAME, EmployeeServletDistributable.DEPLOYMENT_NAME + "/WEB-INF/web.xml", SendUsernameServlet.class); + } + + @TargetsContainer(value = "app-server-wildfly-" + NODE_2_NAME) + @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME + "_2", managed = false) + protected static WebArchive employee2() { + return employee(); + } + + @Override + protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException { + log.infov("Preparing worker node ({0})", managementPort); + + OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions + .standalone() + .hostAndPort("localhost", managementPort) + .build()); + Operations op = new Operations(clientWorkerNodeClient); + + Batch b = new Batch(); + Address tcppingStack = Address + .subsystem("jgroups") + .and("stack", "tcpping"); + b.add(tcppingStack); + b.add(tcppingStack.and("transport", "TCP"), Values.of("socket-binding", "jgroups-tcp")); + b.add(tcppingStack.and("protocol", "TCPPING")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "initial_hosts"), Values.of("value", "localhost[" + (7600 + PORT_OFFSET_NODE_1) + "],localhost[" + (7600 + PORT_OFFSET_NODE_2) + "]")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "port_range"), Values.of("value", "0")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "num_initial_members"), Values.of("value", "2")); + b.add(tcppingStack.and("protocol", "TCPPING").and("property", "timeout"), Values.of("value", "3000")); + b.add(tcppingStack.and("protocol", "MERGE3")); + b.add(tcppingStack.and("protocol", "FD_SOCK"), Values.of("socket-binding", "jgroups-tcp-fd")); + b.add(tcppingStack.and("protocol", "FD")); + b.add(tcppingStack.and("protocol", "VERIFY_SUSPECT")); + b.add(tcppingStack.and("protocol", "pbcast.NAKACK2")); + b.add(tcppingStack.and("protocol", "UNICAST3")); + b.add(tcppingStack.and("protocol", "pbcast.STABLE")); + b.add(tcppingStack.and("protocol", "pbcast.GMS")); + b.add(tcppingStack.and("protocol", "MFC")); + b.add(tcppingStack.and("protocol", "FRAG2")); + b.writeAttribute(Address.subsystem("jgroups").and("channel", "ee"), "stack", "tcpping"); + op.batch(b); + + op.add(Address.extension("org.keycloak.keycloak-saml-adapter-subsystem"), Values.of("module", "org.keycloak.keycloak-saml-adapter-subsystem")); + op.add(Address.subsystem("keycloak-saml")); + + clientWorkerNodeClient.execute("reload"); + + log.infov("Worker node ({0}) Prepared", managementPort); + } + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml new file mode 100644 index 0000000000..b57928f585 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml @@ -0,0 +1,62 @@ + + + + + + + + + + %CONTEXT_PATH% + + + javax.ws.rs.core.Application + /* + + + + /error.html + + + + + Application + /* + + + manager + + + + + KEYCLOAK-SAML + demo + + + + manager + + + + keycloak.sessionIdMapperUpdater.classes + org.keycloak.adapters.saml.wildfly.infinispan.InfinispanSessionCacheIdMapperUpdater + + diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml index 6b6399738e..624839c9da 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml @@ -54,6 +54,14 @@ false + 500 + 300 + 10290 + 10299 + 400 + 10390 + 10399 + @@ -193,6 +201,14 @@ ${app.server.startup.timeout} ${app.server.memory.settings} + ${app.server.reverse-proxy.port.offset} + + ${app.server.1.port.offset} + ${app.server.1.management.port} + + ${app.server.2.port.offset} + ${app.server.2.management.port} + ${adapter.test.props} ${adapter.config.bundled}