keycloak-uncached

Changes

Details

diff --git a/adapters/saml/as7-eap6/adapter/pom.xml b/adapters/saml/as7-eap6/adapter/pom.xml
index dd8aff5..02f6dea 100755
--- a/adapters/saml/as7-eap6/adapter/pom.xml
+++ b/adapters/saml/as7-eap6/adapter/pom.xml
@@ -79,6 +79,18 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>org.infinispan</groupId>
+            <artifactId>infinispan-core</artifactId>
+            <scope>provided</scope>
+            <version>5.2.20.Final</version> <!-- override version to match EAP's -->
+        </dependency>
+        <dependency>
+            <groupId>org.infinispan</groupId>
+            <artifactId>infinispan-cachestore-remote</artifactId>
+            <scope>provided</scope>
+            <version>5.2.20.Final</version> <!-- override version to match EAP's -->
+        </dependency>
+        <dependency>
             <groupId>org.keycloak</groupId>
             <artifactId>keycloak-saml-tomcat-adapter-core</artifactId>
             <exclusions>
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
index b6f4c23..dd19a7b 100644
--- 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
@@ -16,9 +16,11 @@
  */
 package org.keycloak.adapters.saml.jbossweb.infinispan;
 
+import org.keycloak.adapters.saml.AdapterConstants;
 import org.keycloak.adapters.spi.SessionIdMapper;
 import org.keycloak.adapters.spi.SessionIdMapperUpdater;
 
+import java.util.List;
 import javax.naming.InitialContext;
 import javax.naming.NamingException;
 import javax.servlet.ServletContext;
@@ -26,6 +28,8 @@ import org.apache.catalina.Context;
 import org.infinispan.Cache;
 import org.infinispan.configuration.cache.CacheMode;
 import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.loaders.CacheLoaderManager;
+import org.infinispan.loaders.remote.RemoteCacheStore;
 import org.infinispan.manager.EmbeddedCacheManager;
 import org.jboss.logging.Logger;
 
@@ -37,24 +41,12 @@ 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 final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container";
 
     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;
+        String containerName = servletContext == null ? null : servletContext.getInitParameter(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME);
+        String cacheName = servletContext == null ? null : servletContext.getInitParameter(AdapterConstants.REPLICATION_CONFIG_SSO_CACHE_PARAM_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();
@@ -62,43 +54,48 @@ public class InfinispanSessionCacheIdMapperUpdater {
         if ("/".equals(contextPath)) {
             contextPath = "/ROOT";
         }
+        String deploymentSessionCacheName = host + contextPath;
+
+        if (containerName == null || cacheName == null || deploymentSessionCacheName == null) {
+            LOG.warnv("Cannot determine parameters of SSO cache for deployment {0}.", host + contextPath);
+
+            return previousIdMapperUpdater;
+        }
 
-        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";
+        String cacheContainerLookup = DEFAULT_CACHE_CONTAINER_JNDI_NAME + "/" + containerName;
 
         try {
             EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
 
-            Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(ssoCacheName);
+            Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(cacheName);
             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);
+                    LOG.debugv("Using default configuration for SSO cache {0}.{1}.", containerName, cacheName);
                     ssoCacheConfiguration = cacheManager.getDefaultCacheConfiguration();
                 } else {
-                    LOG.debugv("Using distributed HTTP session cache configuration for SSO cache. lookup={0}, configuration taken from cache={1}", cacheContainerLookup, deploymentSessionCacheName);
+                    LOG.debugv("Using distributed HTTP session cache configuration for SSO cache {0}.{1}, configuration taken from cache {2}",
+                      containerName, cacheName, deploymentSessionCacheName);
                     ssoCacheConfiguration = cacheConfiguration;
-                    cacheManager.defineConfiguration(ssoCacheName, ssoCacheConfiguration);
+                    cacheManager.defineConfiguration(cacheName, ssoCacheConfiguration);
                 }
             } else {
-                LOG.debugv("Using custom configuration for SSO cache. lookup={0}, cache name={1}", cacheContainerLookup, ssoCacheName);
+                LOG.debugv("Using custom configuration of SSO cache {0}.{1}.", containerName, cacheName);
             }
 
             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());
+                LOG.warnv("SSO cache mode is {0}, it is recommended to use replicated mode instead.", ssoCacheConfiguration.clustering().cacheModeString());
             }
 
-            Cache<String, String[]> ssoCache = cacheManager.getCache(ssoCacheName, true);
-            ssoCache.addListener(new SsoSessionCacheListener(mapper));
+            Cache<String, String[]> ssoCache = cacheManager.getCache(cacheName, true);
+            final SsoSessionCacheListener listener = new SsoSessionCacheListener(ssoCache, mapper);
+            ssoCache.addListener(listener);
+
+            // Not possible to add listener for cross-DC support because of too old Infinispan in AS 7
+            warnIfRemoteStoreIsUsed(ssoCache);
 
-            LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, deploymentSessionCacheName);
+            LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, cacheName);
 
             SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater);
 
@@ -108,4 +105,17 @@ public class InfinispanSessionCacheIdMapperUpdater {
             return previousIdMapperUpdater;
         }
     }
+
+    private static void warnIfRemoteStoreIsUsed(Cache<String, String[]> ssoCache) {
+        final List<RemoteCacheStore> stores = getRemoteStores(ssoCache);
+        if (stores == null || stores.isEmpty()) {
+            return;
+        }
+
+        LOG.warnv("Unable to listen for events on remote stores configured for cache {0} (unsupported in this Infinispan limitations), logouts will not be propagated.", ssoCache.getName());
+    }
+
+    public static List<RemoteCacheStore> getRemoteStores(Cache ssoCache) {
+        return ssoCache.getAdvancedCache().getComponentRegistry().getComponent(CacheLoaderManager.class).getCacheLoaders(RemoteCacheStore.class);
+    }
 }
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
index ee100ad..aded4a3 100644
--- 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
@@ -20,6 +20,7 @@ import org.keycloak.adapters.spi.SessionIdMapper;
 
 import java.util.*;
 import java.util.concurrent.*;
+import org.infinispan.Cache;
 import org.infinispan.notifications.Listener;
 import org.infinispan.notifications.cachelistener.annotation.*;
 import org.infinispan.notifications.cachelistener.event.*;
@@ -43,9 +44,12 @@ public class SsoSessionCacheListener {
 
     private final SessionIdMapper idMapper;
 
+    private final Cache<String, String[]> ssoCache;
+
     private ExecutorService executor = Executors.newSingleThreadExecutor();
 
-    public SsoSessionCacheListener(SessionIdMapper idMapper) {
+    public SsoSessionCacheListener(Cache<String, String[]> ssoCache, SessionIdMapper idMapper) {
+        this.ssoCache = ssoCache;
         this.idMapper = idMapper;
     }
 
@@ -68,8 +72,10 @@ public class SsoSessionCacheListener {
     @CacheEntryRemoved
     @CacheEntryModified
     public void addEvent(TransactionalEvent event) {
-        if (event.isPre() == false) {
+        if (event.getGlobalTransaction() != null) {
             map.get(event.getGlobalTransaction()).add(event);
+        } else {
+            processEvent(event);
         }
     }
 
@@ -87,40 +93,53 @@ public class SsoSessionCacheListener {
         }
 
         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;
-            }
+            processEvent(e);
+        }
+    }
+
+    private void processEvent(final Event e) {
+        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[])) {
+        if (! (event.getKey() instanceof String)) {
             return;
         }
+
         String httpSessionId = (String) event.getKey();
-        String[] value = (String[]) event.getValue();
+
+        if (idMapper.hasSession(httpSessionId)) {
+            // Ignore local events generated by remote store
+            LOG.tracev("IGNORING cacheEntryCreated {0}", httpSessionId);
+            return;
+        }
+
+        String[] value = ssoCache.get((String) httpSessionId);
+
         String ssoId = value[0];
         String principal = value[1];
 
diff --git a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakClusteredSsoDeploymentProcessor.java b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakClusteredSsoDeploymentProcessor.java
new file mode 100644
index 0000000..0333bc9
--- /dev/null
+++ b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakClusteredSsoDeploymentProcessor.java
@@ -0,0 +1,157 @@
+/*
+ * 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.subsystem.saml.as7;
+
+import org.keycloak.adapters.saml.AdapterConstants;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.jboss.as.server.deployment.DeploymentPhaseContext;
+import org.jboss.as.server.deployment.DeploymentUnit;
+import org.jboss.as.server.deployment.DeploymentUnitProcessingException;
+import org.jboss.as.server.deployment.DeploymentUnitProcessor;
+import org.jboss.as.web.deployment.WarMetaData;
+import org.jboss.logging.Logger;
+import org.jboss.metadata.javaee.spec.ParamValueMetaData;
+import org.jboss.metadata.web.jboss.JBossWebMetaData;
+import org.jboss.metadata.web.spec.LoginConfigMetaData;
+import org.jboss.msc.service.ServiceName;
+import org.jboss.msc.service.ServiceTarget;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class KeycloakClusteredSsoDeploymentProcessor implements DeploymentUnitProcessor {
+
+    private static final Logger LOG = Logger.getLogger(KeycloakClusteredSsoDeploymentProcessor.class);
+
+    private static final String DEFAULT_CACHE_CONTAINER = "web";
+    private static final String SSO_CACHE_CONTAINER_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.containerName";
+    private static final String SSO_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheName";
+
+    @Override
+    public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException {
+        final DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit();
+
+        if (isKeycloakSamlAuthMethod(deploymentUnit) && isDistributable(deploymentUnit)) {
+            addSamlReplicationConfiguration(deploymentUnit, phaseContext);
+        }
+    }
+
+    public static boolean isDistributable(final DeploymentUnit deploymentUnit) {
+        WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
+        if (warMetaData == null) {
+            return false;
+        }
+        JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
+        if (webMetaData == null) {
+            return false;
+        }
+
+        return webMetaData.getDistributable() != null || webMetaData.getReplicationConfig() != null;
+    }
+
+    public static boolean isKeycloakSamlAuthMethod(final DeploymentUnit deploymentUnit) {
+        WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
+        if (warMetaData == null) {
+            return false;
+        }
+        JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
+        if (webMetaData == null) {
+            return false;
+        }
+
+        if (Configuration.INSTANCE.isSecureDeployment(deploymentUnit)) {
+            return true;
+        }
+
+        LoginConfigMetaData loginConfig = webMetaData.getLoginConfig();
+
+        return loginConfig != null && Objects.equals(loginConfig.getAuthMethod(), "KEYCLOAK-SAML");
+    }
+
+    @Override
+    public void undeploy(DeploymentUnit du) {
+        
+    }
+
+    private void addSamlReplicationConfiguration(DeploymentUnit deploymentUnit, DeploymentPhaseContext context) {
+        WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
+        if (warMetaData == null) {
+            return;
+        }
+
+        JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
+        if (webMetaData == null) {
+            webMetaData = new JBossWebMetaData();
+            warMetaData.setMergedJBossWebMetaData(webMetaData);
+        }
+
+        // Find out default names of cache container and cache
+        String cacheContainer = DEFAULT_CACHE_CONTAINER;
+        String deploymentSessionCacheName =
+          (deploymentUnit.getParent() == null
+              ? ""
+              : deploymentUnit.getParent().getName() + ".")
+          + deploymentUnit.getName();
+
+        // Update names from jboss-web.xml's <replicationConfig>
+        if (webMetaData.getReplicationConfig() != null && webMetaData.getReplicationConfig().getCacheName() != null) {
+            ServiceName sn = ServiceName.parse(webMetaData.getReplicationConfig().getCacheName());
+            cacheContainer = sn.getParent().getSimpleName();
+            deploymentSessionCacheName = sn.getSimpleName();
+        }
+        String ssoCacheName = deploymentSessionCacheName + ".ssoCache";
+
+        // Override if they were set in the context parameters
+        List<ParamValueMetaData> contextParams = webMetaData.getContextParams();
+        if (contextParams == null) {
+            contextParams = new ArrayList<>();
+        }
+        for (ParamValueMetaData contextParam : contextParams) {
+            if (Objects.equals(contextParam.getParamName(), SSO_CACHE_CONTAINER_NAME_PARAM_NAME)) {
+                cacheContainer = contextParam.getParamValue();
+            } else if (Objects.equals(contextParam.getParamName(), SSO_CACHE_NAME_PARAM_NAME)) {
+                ssoCacheName = contextParam.getParamValue();
+            }
+        }
+
+        LOG.debugv("Determined SSO cache container configuration: container: {0}, cache: {1}", cacheContainer, ssoCacheName);
+//        addCacheDependency(context, deploymentUnit, cacheContainer, cacheName);
+
+        // Set context parameters for SSO cache container/name
+        ParamValueMetaData paramContainer = new ParamValueMetaData();
+        paramContainer.setParamName(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME);
+        paramContainer.setParamValue(cacheContainer);
+        contextParams.add(paramContainer);
+
+        ParamValueMetaData paramSsoCache = new ParamValueMetaData();
+        paramSsoCache.setParamName(AdapterConstants.REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME);
+        paramSsoCache.setParamValue(ssoCacheName);
+        contextParams.add(paramSsoCache);
+
+        webMetaData.setContextParams(contextParams);
+    }
+
+    private void addCacheDependency(DeploymentPhaseContext context, DeploymentUnit deploymentUnit, String cacheContainer, String cacheName) {
+        ServiceName jbossAsCacheContainerService = ServiceName.of("jboss", "infinispan", cacheContainer);
+        ServiceTarget st = context.getServiceTarget();
+        st.addDependency(jbossAsCacheContainerService.append(cacheName));
+    }
+
+}
diff --git a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java
index d583db4..30a853f 100755
--- a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java
+++ b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java
@@ -48,6 +48,10 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler {
                         Phase.POST_MODULE, // PHASE
                         Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY
                         chooseConfigDeploymentProcessor());
+                processorTarget.addDeploymentProcessor(KeycloakSamlExtension.SUBSYSTEM_NAME,
+                        Phase.POST_MODULE, // PHASE
+                        Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY
+                        chooseClusteredSsoDeploymentProcessor());
             }
         }, OperationContext.Stage.RUNTIME);
     }
@@ -60,6 +64,10 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler {
         return new KeycloakAdapterConfigDeploymentProcessor();
     }
 
+    private DeploymentUnitProcessor chooseClusteredSsoDeploymentProcessor() {
+        return new KeycloakClusteredSsoDeploymentProcessor();
+    }
+
     @Override
     protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException {
     }
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java
index 8b94068..3646ed4 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java
@@ -23,4 +23,6 @@ package org.keycloak.adapters.saml;
  */
 public class AdapterConstants {
     public static final String AUTH_DATA_PARAM_NAME="org.keycloak.saml.xml.adapterConfig";
+    public static final String REPLICATION_CONFIG_CONTAINER_PARAM_NAME = "org.keycloak.saml.replication.container";
+    public static final String REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME = "org.keycloak.saml.replication.cache.sso";
 }
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 2bf2369..7e8fb83 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
@@ -152,12 +152,19 @@ public class ServletSamlSessionStore implements SamlSessionStore {
     public boolean isLoggedIn() {
         HttpSession session = getSession(false);
         if (session == null) {
-            log.debug("session was null, returning null");
+            log.debug("Session was not found");
             return false;
         }
+
+        if (! idMapper.hasSession(session.getId())) {
+            log.debugf("Session %s has expired on some other node", session.getId());
+            session.removeAttribute(SamlSession.class.getName());
+            return false;
+        }
+
         final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
         if (samlSession == null) {
-            log.debug("SamlSession was not in session, returning null");
+            log.debug("SamlSession was not found in the session");
             return false;
         }
 
diff --git a/adapters/saml/wildfly/wildfly-adapter/pom.xml b/adapters/saml/wildfly/wildfly-adapter/pom.xml
index 3be5e7e..73135d5 100755
--- a/adapters/saml/wildfly/wildfly-adapter/pom.xml
+++ b/adapters/saml/wildfly/wildfly-adapter/pom.xml
@@ -71,6 +71,10 @@
             <artifactId>infinispan-core</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.infinispan</groupId>
+            <artifactId>infinispan-cachestore-remote</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.picketbox</groupId>
             <artifactId>picketbox</artifactId>
             <version>4.0.20.Final</version>
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
index 489d1d5..c35db63 100644
--- 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
@@ -16,6 +16,7 @@
  */
 package org.keycloak.adapters.saml.wildfly.infinispan;
 
+import org.keycloak.adapters.saml.AdapterConstants;
 import org.keycloak.adapters.spi.SessionIdMapper;
 import org.keycloak.adapters.spi.SessionIdMapperUpdater;
 
@@ -27,6 +28,8 @@ import org.infinispan.Cache;
 import org.infinispan.configuration.cache.CacheMode;
 import org.infinispan.configuration.cache.Configuration;
 import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
 import org.jboss.logging.Logger;
 
 /**
@@ -37,64 +40,55 @@ 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 final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container";
 
     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"
-        );
+        Map<String, String> initParameters = deploymentInfo.getInitParameters();
+        String containerName = initParameters == null ? null : initParameters.get(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME);
+        String cacheName = initParameters == null ? null : initParameters.get(AdapterConstants.REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME);
+
+        if (containerName == null || cacheName == null) {
+            LOG.warnv("Cannot determine parameters of SSO cache for deployment {0}.", deploymentInfo.getDeploymentName());
 
-        if (! distributable) {
-            LOG.warnv("Deployment {0} does not use supported distributed session cache mechanism", deploymentInfo.getDeploymentName());
             return previousIdMapperUpdater;
         }
 
-        Map<String, String> 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";
+        String cacheContainerLookup = DEFAULT_CACHE_CONTAINER_JNDI_NAME + "/" + containerName;
+        String deploymentSessionCacheName = deploymentInfo.getDeploymentName();
 
         try {
             EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
 
-            Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(ssoCacheName);
+            Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(cacheName);
             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);
+                    LOG.debugv("Using default configuration for SSO cache {0}.{1}.", containerName, cacheName);
                     ssoCacheConfiguration = cacheManager.getDefaultCacheConfiguration();
                 } else {
-                    LOG.debugv("Using distributed HTTP session cache configuration for SSO cache. lookup={0}, configuration taken from cache={1}", cacheContainerLookup, deploymentSessionCacheName);
+                    LOG.debugv("Using distributed HTTP session cache configuration for SSO cache {0}.{1}, configuration taken from cache {2}",
+                      containerName, cacheName, deploymentSessionCacheName);
                     ssoCacheConfiguration = cacheConfiguration;
-                    cacheManager.defineConfiguration(ssoCacheName, ssoCacheConfiguration);
+                    cacheManager.defineConfiguration(cacheName, ssoCacheConfiguration);
                 }
             } else {
-                LOG.debugv("Using custom configuration of SSO cache. lookup={0}, cache name={1}", cacheContainerLookup, ssoCacheName);
+                LOG.debugv("Using custom configuration of SSO cache {0}.{1}.", containerName, cacheName);
             }
 
             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());
+                LOG.warnv("SSO cache mode is {0}, it is recommended to use replicated mode instead.", ssoCacheConfiguration.clustering().cacheModeString());
             }
 
-            Cache<String, String[]> ssoCache = cacheManager.getCache(ssoCacheName, true);
-            ssoCache.addListener(new SsoSessionCacheListener(mapper));
+            Cache<String, String[]> ssoCache = cacheManager.getCache(cacheName, true);
+            final SsoSessionCacheListener listener = new SsoSessionCacheListener(ssoCache, mapper);
+            ssoCache.addListener(listener);
 
-            LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, deploymentSessionCacheName);
+            addSsoCacheCrossDcListener(ssoCache, listener);
 
+            LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, cacheName);
+
+            LOG.debugv("Adding session listener for SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, cacheName);
             SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater);
             deploymentInfo.addSessionListener(updater);
 
@@ -104,4 +98,25 @@ public class InfinispanSessionCacheIdMapperUpdater {
             return previousIdMapperUpdater;
         }
     }
+
+    private static void addSsoCacheCrossDcListener(Cache<String, String[]> ssoCache, SsoSessionCacheListener listener) {
+        if (ssoCache.getCacheConfiguration().persistence() == null) {
+            return;
+        }
+
+        final Set<RemoteStore> stores = getRemoteStores(ssoCache);
+        if (stores == null || stores.isEmpty()) {
+            return;
+        }
+
+        LOG.infov("Listening for events on remote stores configured for cache {0}", ssoCache.getName());
+
+        for (RemoteStore store : stores) {
+            store.getRemoteCache().addClientListener(listener);
+        }
+    }
+
+    public static Set<RemoteStore> getRemoteStores(Cache ispnCache) {
+        return ispnCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class);
+    }
 }
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
index ccd102e..6d53485 100644
--- 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
@@ -20,6 +20,12 @@ import org.keycloak.adapters.spi.SessionIdMapper;
 
 import java.util.*;
 import java.util.concurrent.*;
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved;
+import org.infinispan.client.hotrod.annotation.ClientListener;
+import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent;
 import org.infinispan.notifications.Listener;
 import org.infinispan.notifications.cachelistener.annotation.*;
 import org.infinispan.notifications.cachelistener.event.*;
@@ -34,6 +40,7 @@ import org.jboss.logging.Logger;
  * @author hmlnarik
  */
 @Listener
+@ClientListener
 public class SsoSessionCacheListener {
 
     private static final Logger LOG = Logger.getLogger(SsoSessionCacheListener.class);
@@ -42,14 +49,21 @@ public class SsoSessionCacheListener {
 
     private final SessionIdMapper idMapper;
 
+    private final Cache<String, String[]> ssoCache;
+
     private ExecutorService executor = Executors.newSingleThreadExecutor();
 
-    public SsoSessionCacheListener(SessionIdMapper idMapper) {
+    public SsoSessionCacheListener(Cache<String, String[]> ssoCache, SessionIdMapper idMapper) {
+        this.ssoCache = ssoCache;
         this.idMapper = idMapper;
     }
 
     @TransactionRegistered
     public void startTransaction(TransactionRegisteredEvent event) {
+        if (event.getGlobalTransaction() == null) {
+            return;
+        }
+
         map.put(event.getGlobalTransaction().globalId(), new ConcurrentLinkedQueue<Event>());
     }
 
@@ -66,42 +80,56 @@ public class SsoSessionCacheListener {
     @CacheEntryCreated
     @CacheEntryRemoved
     public void addEvent(TransactionalEvent event) {
-        if (event.isPre() == false) {
+        if (event.isOriginLocal()) {
+            // Local events are processed by local HTTP session listener
+            return;
+        }
+
+        if (event.isPre()) {    // only handle post events
+            return;
+        }
+
+        if (event.getGlobalTransaction() != null) {
             map.get(event.getGlobalTransaction().globalId()).add(event);
+        } else {
+            processEvent(event);
         }
     }
 
     @TransactionCompleted
     public void endTransaction(TransactionCompletedEvent event) {
+        if (event.getGlobalTransaction() == null) {
+            return;
+        }
+
         Queue<Event> 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) {
+            processEvent(e);
         }
+    }
 
-        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 processEvent(final Event e) {
+        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;
         }
     }
 
@@ -128,4 +156,40 @@ public class SsoSessionCacheListener {
 
         this.idMapper.removeSession((String) event.getKey());
     }
+
+    @ClientCacheEntryCreated
+    public void remoteCacheEntryCreated(ClientCacheEntryCreatedEvent event) {
+        if (! (event.getKey() instanceof String)) {
+            return;
+        }
+
+        String httpSessionId = (String) event.getKey();
+
+        if (idMapper.hasSession(httpSessionId)) {
+            // Ignore local events generated by remote store
+            LOG.tracev("IGNORING remoteCacheEntryCreated {0}", httpSessionId);
+            return;
+        }
+
+        String[] value = ssoCache.get((String) httpSessionId);
+
+        if (value != null) {
+            String ssoId = value[0];
+            String principal = value[1];
+
+            LOG.tracev("remoteCacheEntryCreated {0}:{1}", httpSessionId, ssoId);
+
+            this.idMapper.map(ssoId, principal, httpSessionId);
+        } else {
+            LOG.tracev("remoteCacheEntryCreated {0}", event.getKey());
+
+        }
+    }
+
+    @ClientCacheEntryRemoved
+    public void remoteCacheEntryRemoved(ClientCacheEntryRemovedEvent event) {
+        LOG.tracev("remoteCacheEntryRemoved {0}", event.getKey());
+
+        this.idMapper.removeSession((String) event.getKey());
+    }
 }
diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakClusteredSsoDeploymentProcessor.java b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakClusteredSsoDeploymentProcessor.java
new file mode 100644
index 0000000..3be66de
--- /dev/null
+++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakClusteredSsoDeploymentProcessor.java
@@ -0,0 +1,178 @@
+/*
+ * 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.subsystem.adapter.saml.extension;
+
+import org.keycloak.adapters.saml.AdapterConstants;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.jboss.as.controller.capability.CapabilityServiceSupport;
+import org.jboss.as.server.deployment.Attachments;
+import org.jboss.as.server.deployment.DeploymentPhaseContext;
+import org.jboss.as.server.deployment.DeploymentUnit;
+import org.jboss.as.server.deployment.DeploymentUnitProcessingException;
+import org.jboss.as.server.deployment.DeploymentUnitProcessor;
+import org.jboss.as.web.common.WarMetaData;
+import org.jboss.logging.Logger;
+import org.jboss.metadata.javaee.spec.ParamValueMetaData;
+import org.jboss.metadata.web.jboss.JBossWebMetaData;
+import org.jboss.metadata.web.spec.LoginConfigMetaData;
+import org.jboss.msc.service.ServiceController;
+import org.jboss.msc.service.ServiceName;
+import org.jboss.msc.service.ServiceTarget;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class KeycloakClusteredSsoDeploymentProcessor implements DeploymentUnitProcessor {
+
+    private static final Logger LOG = Logger.getLogger(KeycloakClusteredSsoDeploymentProcessor.class);
+
+    private static final String DEFAULT_CACHE_CONTAINER = "web";
+    private static final String SSO_CACHE_CONTAINER_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.containerName";
+    private static final String SSO_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheName";
+
+    @Override
+    public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException {
+        final DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit();
+
+        if (isKeycloakSamlAuthMethod(deploymentUnit) && isDistributable(deploymentUnit)) {
+            addSamlReplicationConfiguration(deploymentUnit, phaseContext);
+        }
+    }
+
+    public static boolean isDistributable(final DeploymentUnit deploymentUnit) {
+        WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
+        if (warMetaData == null) {
+            return false;
+        }
+        JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
+        if (webMetaData == null) {
+            return false;
+        }
+
+        return webMetaData.getDistributable() != null || webMetaData.getReplicationConfig() != null;
+    }
+
+    public static boolean isKeycloakSamlAuthMethod(final DeploymentUnit deploymentUnit) {
+        if (Configuration.INSTANCE.getSecureDeployment(deploymentUnit) != null) {
+            return true;
+        }
+
+        WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
+        if (warMetaData == null) {
+            return false;
+        }
+        JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
+        if (webMetaData == null) {
+            return false;
+        }
+
+        LoginConfigMetaData loginConfig = webMetaData.getLoginConfig();
+
+        return loginConfig != null && Objects.equals(loginConfig.getAuthMethod(), "KEYCLOAK-SAML");
+    }
+
+    @Override
+    public void undeploy(DeploymentUnit du) {
+        
+    }
+
+    private void addSamlReplicationConfiguration(DeploymentUnit deploymentUnit, DeploymentPhaseContext context) {
+        WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
+        if (warMetaData == null) {
+            return;
+        }
+
+        JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
+        if (webMetaData == null) {
+            webMetaData = new JBossWebMetaData();
+            warMetaData.setMergedJBossWebMetaData(webMetaData);
+        }
+
+        // Find out default names of cache container and cache
+        String cacheContainer = DEFAULT_CACHE_CONTAINER;
+        String deploymentSessionCacheName =
+          (deploymentUnit.getParent() == null
+              ? ""
+              : deploymentUnit.getParent().getName() + ".")
+          + deploymentUnit.getName();
+
+        // Update names from jboss-web.xml's <replicationConfig>
+        if (webMetaData.getReplicationConfig() != null && webMetaData.getReplicationConfig().getCacheName() != null) {
+            ServiceName sn = ServiceName.parse(webMetaData.getReplicationConfig().getCacheName());
+            cacheContainer = sn.getParent().getSimpleName();
+            deploymentSessionCacheName = sn.getSimpleName();
+        }
+        String ssoCacheName = deploymentSessionCacheName + ".ssoCache";
+
+        // Override if they were set in the context parameters
+        List<ParamValueMetaData> contextParams = webMetaData.getContextParams();
+        if (contextParams == null) {
+            contextParams = new ArrayList<>();
+        }
+        for (ParamValueMetaData contextParam : contextParams) {
+            if (Objects.equals(contextParam.getParamName(), SSO_CACHE_CONTAINER_NAME_PARAM_NAME)) {
+                cacheContainer = contextParam.getParamValue();
+            } else if (Objects.equals(contextParam.getParamName(), SSO_CACHE_NAME_PARAM_NAME)) {
+                ssoCacheName = contextParam.getParamValue();
+            }
+        }
+
+        LOG.debugv("Determined SSO cache container configuration: container: {0}, cache: {1}", cacheContainer, ssoCacheName);
+        addCacheDependency(context, deploymentUnit, cacheContainer, ssoCacheName);
+
+        // Set context parameters for SSO cache container/name
+        ParamValueMetaData paramContainer = new ParamValueMetaData();
+        paramContainer.setParamName(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME);
+        paramContainer.setParamValue(cacheContainer);
+        contextParams.add(paramContainer);
+
+        ParamValueMetaData paramSsoCache = new ParamValueMetaData();
+        paramSsoCache.setParamName(AdapterConstants.REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME);
+        paramSsoCache.setParamValue(ssoCacheName);
+        contextParams.add(paramSsoCache);
+
+        webMetaData.setContextParams(contextParams);
+    }
+
+    private void addCacheDependency(DeploymentPhaseContext context, DeploymentUnit deploymentUnit, String cacheContainer, String cacheName) {
+        ServiceName wf10CacheContainerServiceName = ServiceName.of("jboss", "infinispan", cacheContainer);
+        final ServiceController<?> wf10CacheContainerService = context.getServiceRegistry().getService(wf10CacheContainerServiceName);
+
+        boolean legacy = wf10CacheContainerService != null;
+        ServiceTarget st = context.getServiceTarget();
+
+        if (legacy) {
+            ServiceName cacheServiceName = wf10CacheContainerServiceName.append(cacheName);
+            ServiceController<?> cacheService = context.getServiceRegistry().getService(cacheServiceName);
+            if (cacheService != null) {
+                st.addDependency(cacheServiceName);
+            }
+        } else {
+            CapabilityServiceSupport support = deploymentUnit.getAttachment(Attachments.CAPABILITY_SERVICE_SUPPORT);
+
+            ServiceName cacheServiceName = support.getCapabilityServiceName("org.wildfly.clustering.infinispan.cache." + cacheContainer + "." + cacheName);
+            ServiceController<?> cacheService = context.getServiceRegistry().getService(cacheServiceName);
+            if (cacheService != null) {
+                st.addDependency(cacheServiceName);
+            }
+        }
+    }
+
+}
diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemAdd.java b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemAdd.java
index 79a4981..e9ef1a3 100755
--- a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemAdd.java
+++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemAdd.java
@@ -43,6 +43,10 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler {
                         Phase.POST_MODULE, // PHASE
                         Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY
                         chooseConfigDeploymentProcessor());
+                processorTarget.addDeploymentProcessor(KeycloakSamlExtension.SUBSYSTEM_NAME,
+                        Phase.POST_MODULE, // PHASE
+                        Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY
+                        chooseClusteredSsoDeploymentProcessor());
             }
         }, OperationContext.Stage.RUNTIME);
     }
@@ -54,4 +58,8 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler {
     private DeploymentUnitProcessor chooseConfigDeploymentProcessor() {
         return new KeycloakAdapterConfigDeploymentProcessor();
     }
+
+    private DeploymentUnitProcessor chooseClusteredSsoDeploymentProcessor() {
+        return new KeycloakClusteredSsoDeploymentProcessor();
+    }
 }
diff --git a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java
index a00ae82..7ca8af6 100755
--- a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java
+++ b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java
@@ -67,6 +67,11 @@ public class InMemorySessionIdMapper implements SessionIdMapper {
             ssoToSession.put(sso, session);
             sessionToSso.put(session, sso);
         }
+
+        if (principal == null) {
+            return;
+        }
+
         Set<String> userSessions = principalToSession.get(principal);
         if (userSessions == null) {
             final Set<String> tmp = Collections.synchronizedSet(new HashSet<String>());
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 fd9d2e4..885470f 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
@@ -34,7 +34,6 @@
         <module name="org.jboss.as.security"/>
         <module name="org.jboss.as.web"/>
         <module name="org.picketbox"/>
-        <module name="org.keycloak.keycloak-saml-as7-adapter"/>
         <module name="org.keycloak.keycloak-adapter-spi"/>
         <module name="org.keycloak.keycloak-saml-core-public"/>
         <module name="org.keycloak.keycloak-saml-core"/>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml
index 857a8e3..b61266d 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml
@@ -40,5 +40,6 @@
         <module name="org.jboss.as.web-common"/>
         <module name="org.jboss.metadata"/>
         <module name="org.apache.httpcomponents"/>
+        <module name="org.infinispan.cachestore.remote"/>
     </dependencies>
 </module>
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
index b6fbd2e..9a68ecf 100644
--- a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
@@ -39,6 +39,8 @@
             <local-cache name="loginFailures" configuration="sessions-cfg" />
             <local-cache name="actionTokens" configuration="sessions-cfg" />
             <local-cache name="work" configuration="sessions-cfg" />
+            <local-cache name="employee-distributable-cache.ssoCache" configuration="sessions-cfg"/>
+            <local-cache name="employee-distributable-cache" configuration="sessions-cfg"/>
         </xsl:copy>
     </xsl:template>
 
@@ -57,6 +59,8 @@
             <replicated-cache name="loginFailures" configuration="sessions-cfg" />
             <replicated-cache name="actionTokens" configuration="sessions-cfg" />
             <replicated-cache name="work" configuration="sessions-cfg" />
+            <replicated-cache name="employee-distributable-cache.ssoCache" configuration="sessions-cfg"/>
+            <replicated-cache name="employee-distributable-cache" configuration="sessions-cfg"/>
         </xsl:copy>
     </xsl:template>
 
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java
index a17a75a..257afc0 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java
@@ -51,6 +51,7 @@ import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.security.PrivateKey;
 import java.security.PublicKey;
+import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
@@ -289,6 +290,18 @@ public class SamlClient {
         }
     }
 
+    public void execute(Step... steps) {
+        executeAndTransform(resp -> null, Arrays.asList(steps));
+    }
+
+    public void execute(List<Step> steps) {
+        executeAndTransform(resp -> null, steps);
+    }
+
+    public <T> T executeAndTransform(ResultExtractor<T> resultTransformer, Step... steps) {
+        return executeAndTransform(resultTransformer, Arrays.asList(steps));
+    }
+
     public <T> T executeAndTransform(ResultExtractor<T> resultTransformer, List<Step> steps) {
         CloseableHttpResponse currentResponse = null;
         URI currentUri = URI.create("about:blank");
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java
index 89d3092..3879447 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java
@@ -33,6 +33,10 @@ import org.keycloak.testsuite.util.saml.IdPInitiatedLoginBuilder;
 import org.keycloak.testsuite.util.saml.LoginBuilder;
 import org.keycloak.testsuite.util.saml.ModifySamlResponseStepBuilder;
 import org.keycloak.testsuite.util.saml.RequiredConsentBuilder;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.hamcrest.Matcher;
+import org.junit.Assert;
 import org.w3c.dom.Document;
 
 /**
@@ -43,6 +47,19 @@ public class SamlClientBuilder {
 
     private final List<Step> steps = new LinkedList<>();
 
+    /**
+     * Execute the current steps without any work on the final response.
+     * @return Client that executed the steps
+     */
+    public SamlClient execute() {
+        return execute(resp -> {});
+    }
+
+    /**
+     * Execute the current steps and pass the final response to the {@code resultConsumer} for processing.
+     * @param resultConsumer This function is given the final response
+     * @return Client that executed the steps
+     */
     public SamlClient execute(Consumer<CloseableHttpResponse> resultConsumer) {
         final SamlClient samlClient = new SamlClient();
         samlClient.executeAndTransform(r -> {
@@ -52,6 +69,11 @@ public class SamlClientBuilder {
         return samlClient;
     }
 
+    /**
+     * Execute the current steps and pass the final response to the {@code resultTransformer} for processing.
+     * @param resultTransformer This function is given the final response and processes it into some value
+     * @return Value returned by {@code resultTransformer}
+     */
     public <T> T executeAndTransform(ResultExtractor<T> resultTransformer) {
         return new SamlClient().executeAndTransform(resultTransformer, steps);
     }
@@ -60,11 +82,48 @@ public class SamlClientBuilder {
         return steps;
     }
 
-    public <T extends Step> T addStep(T step) {
+    public <T extends Step> T addStepBuilder(T step) {
         steps.add(step);
         return step;
     }
 
+    /**
+     * Adds a single generic step
+     * @param step
+     * @return This builder
+     */
+    public SamlClientBuilder addStep(Step step) {
+        steps.add(step);
+        return this;
+    }
+
+    /**
+     * Adds a single generic step
+     * @param step
+     * @return This builder
+     */
+    public SamlClientBuilder addStep(Runnable stepWithNoParameters) {
+        addStep((client, currentURI, currentResponse, context) -> {
+            stepWithNoParameters.run();
+            return null;
+        });
+        return this;
+    }
+
+    public SamlClientBuilder assertResponse(Matcher<HttpResponse> matcher) {
+        steps.add((client, currentURI, currentResponse, context) -> {
+            Assert.assertThat(currentResponse, matcher);
+            return null;
+        });
+        return this;
+    }
+
+    /**
+     * When executing the {@link HttpUriRequest} obtained from the previous step,
+     * do not to follow HTTP redirects but pass the first response immediately
+     * to the following step.
+     * @return This builder
+     */
     public SamlClientBuilder doNotFollowRedirects() {
         this.steps.add(new DoNotFollowRedirectStep());
         return this;
@@ -80,32 +139,32 @@ public class SamlClientBuilder {
 
     /** Creates fresh and issues an AuthnRequest to the SAML endpoint */
     public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, String issuer, String assertionConsumerURL, Binding requestBinding) {
-        return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, issuer, assertionConsumerURL, requestBinding, this));
+        return addStepBuilder(new CreateAuthnRequestStepBuilder(authServerSamlUrl, issuer, assertionConsumerURL, requestBinding, this));
     }
 
     /** Issues the given AuthnRequest to the SAML endpoint */
     public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, Document authnRequestDocument, Binding requestBinding) {
-        return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, authnRequestDocument, requestBinding, this));
+        return addStepBuilder(new CreateAuthnRequestStepBuilder(authServerSamlUrl, authnRequestDocument, requestBinding, this));
     }
 
     /** Issues the given AuthnRequest to the SAML endpoint */
     public CreateLogoutRequestStepBuilder logoutRequest(URI authServerSamlUrl, String issuer, Binding requestBinding) {
-        return addStep(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this));
+        return addStepBuilder(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this));
     }
 
     /** Handles login page */
     public LoginBuilder login() {
-        return addStep(new LoginBuilder(this));
+        return addStepBuilder(new LoginBuilder(this));
     }
 
     /** Starts IdP-initiated flow for the given client */
     public IdPInitiatedLoginBuilder idpInitiatedLogin(URI authServerSamlUrl, String clientId) {
-        return addStep(new IdPInitiatedLoginBuilder(authServerSamlUrl, clientId, this));
+        return addStepBuilder(new IdPInitiatedLoginBuilder(authServerSamlUrl, clientId, this));
     }
 
     /** Handles "Requires consent" page */
     public RequiredConsentBuilder consentRequired() {
-        return addStep(new RequiredConsentBuilder(this));
+        return addStepBuilder(new RequiredConsentBuilder(this));
     }
 
     /** Returns SAML request or response as replied from server. Note that the redirects are disabled for this to work. */
@@ -119,20 +178,16 @@ public class SamlClientBuilder {
     public ModifySamlResponseStepBuilder processSamlResponse(Binding responseBinding) {
         return
           doNotFollowRedirects()
-          .addStep(new ModifySamlResponseStepBuilder(responseBinding, this));
+          .addStepBuilder(new ModifySamlResponseStepBuilder(responseBinding, this));
     }
 
     public SamlClientBuilder navigateTo(String httpGetUri) {
-        steps.add((client, currentURI, currentResponse, context) -> {
-            return new HttpGet(httpGetUri);
-        });
+        steps.add((client, currentURI, currentResponse, context) -> new HttpGet(httpGetUri));
         return this;
     }
 
     public SamlClientBuilder navigateTo(URI httpGetUri) {
-        steps.add((client, currentURI, currentResponse, context) -> {
-            return new HttpGet(httpGetUri);
-        });
+        steps.add((client, currentURI, currentResponse, context) -> new HttpGet(httpGetUri));
         return this;
     }
 
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
index ce95631..f71b757 100644
--- 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
@@ -37,7 +37,6 @@ 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.*;
@@ -49,13 +48,21 @@ import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
 import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
 
+import org.keycloak.testsuite.util.Matchers;
+import org.keycloak.testsuite.util.SamlClient;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import java.net.MalformedURLException;
+import java.util.function.BiConsumer;
+import org.apache.http.client.methods.HttpGet;
 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.keycloak.testsuite.AbstractAuthTest.createUserRepresentation;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
 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;
@@ -130,15 +137,21 @@ public abstract class AbstractSAMLAdapterClusterTest extends AbstractServletsAda
     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")));
+        prepareWorkerNode(0, 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")));
+        prepareWorkerNode(1, 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;
+    /**
+     * Prepares a worker node
+     * @param nodeIndex Node index, counting from 0
+     * @param managementPort Port for management operations on this node
+     * @throws Exception
+     */
+    protected abstract void prepareWorkerNode(int nodeIndex, Integer managementPort) throws Exception;
 
     @After
     public void stopServer() {
@@ -155,41 +168,103 @@ public abstract class AbstractSAMLAdapterClusterTest extends AbstractServletsAda
         loginActionsPage.setAuthRealm(DEMO);
     }
 
-    protected void testLogoutViaSessionIndex(URL employeeUrl, Consumer<EmployeeServletDistributable> 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);
+    protected void testLogoutViaSessionIndex(URL employeeUrl, boolean forceRefreshAtOtherNode, BiConsumer<SamlClientBuilder, String> logoutFunction) {
         setPasswordFor(bburkeUser, CredentialRepresentation.PASSWORD);
 
-        assertSuccessfulLogin(page, bburkeUser, testRealmSAMLPostLoginPage, "principal=bburke");
+        final String employeeUrlString;
+        try {
+            URL employeeUrlAtRevProxy = new URL(employeeUrl.getProtocol(), employeeUrl.getHost(), HTTP_PORT_NODE_REVPROXY, employeeUrl.getFile());
+            employeeUrlString = employeeUrlAtRevProxy.toString();
+        } catch (MalformedURLException ex) {
+            throw new RuntimeException(ex);
+        }
+
+        SamlClientBuilder builder = new SamlClientBuilder()
+          // Go to employee URL at reverse proxy which is set to forward to first node
+          .navigateTo(employeeUrlString)
+
+          // process redirection to login page
+          .processSamlResponse(Binding.POST).build()
+          .login().user(bburkeUser).build()
+          .processSamlResponse(Binding.POST).build()
+
+          // Returned to the page
+          .assertResponse(Matchers.bodyHC(containsString("principal=bburke")))
+
+          // Update the proxy to forward to the second node.
+          .addStep(() -> updateProxy(NODE_2_NAME, NODE_2_URI, NODE_1_URI));
+
+        if (forceRefreshAtOtherNode) {
+            // Go to employee URL at reverse proxy which is set to forward to _second_ node now
+            builder
+              .navigateTo(employeeUrlString)
+              .doNotFollowRedirects()
+              .assertResponse(Matchers.bodyHC(containsString("principal=bburke")));
+        }
 
-        updateProxy(NODE_2_NAME, NODE_2_URI, NODE_1_URI);
-        logoutFunction.accept(page);
-        delayedCheckLoggedOut(page, loginActionsPage);
+        // Logout at the _second_ node
+        logoutFunction.accept(builder, employeeUrlString);
 
+        SamlClient samlClient = builder.execute();
+        delayedCheckLoggedOut(samlClient, employeeUrlString);
+
+        // Update the proxy to forward to the first node.
         updateProxy(NODE_1_NAME, NODE_1_URI, NODE_2_URI);
-        delayedCheckLoggedOut(page, loginActionsPage);
+        delayedCheckLoggedOut(samlClient, employeeUrlString);
+    }
+
+    private void delayedCheckLoggedOut(SamlClient samlClient, String url) {
+        Retry.execute(() -> {
+          samlClient.execute(
+            (client, currentURI, currentResponse, context) -> new HttpGet(url),
+            (client, currentURI, currentResponse, context) -> {
+              assertThat(currentResponse, Matchers.bodyHC(not(containsString("principal=bburke"))));
+              return null;
+            }
+          );
+        }, 10, 300);
+    }
+
+    private void logoutViaAdminConsole() {
+        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 testAdminInitiatedBackchannelLogout(@ArquillianResource
+      @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception {
+        testLogoutViaSessionIndex(employeeUrl, false, (builder, url) -> builder.addStep(this::logoutViaAdminConsole));
+    }
+
+    @Test
+    public void testAdminInitiatedBackchannelLogoutWithAssertionOfLoggedIn(@ArquillianResource
+      @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception {
+        testLogoutViaSessionIndex(employeeUrl, true, (builder, url) -> builder.addStep(this::logoutViaAdminConsole));
     }
 
     @Test
-    public void testBackchannelLogout(@ArquillianResource
+    public void testUserInitiatedFrontchannelLogout(@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");
+        testLogoutViaSessionIndex(employeeUrl, false, (builder, url) -> {
+            builder
+              .navigateTo(url + "?GLO=true")
+              .processSamlResponse(Binding.POST).build()    // logout request
+              .processSamlResponse(Binding.POST).build()    // logout response
+            ;
         });
     }
 
     @Test
-    public void testFrontchannelLogout(@ArquillianResource
+    public void testUserInitiatedFrontchannelLogoutWithAssertionOfLoggedIn(@ArquillianResource
       @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception {
-        testLogoutViaSessionIndex(employeeUrl, (EmployeeServletDistributable page) -> {
-            page.logout();
-            log.infov("Logged out via application");
+        testLogoutViaSessionIndex(employeeUrl, true, (builder, url) -> {
+            builder
+              .navigateTo(url + "?GLO=true")
+              .processSamlResponse(Binding.POST).build()    // logout request
+              .processSamlResponse(Binding.POST).build()    // logout response
+            ;
         });
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee2/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee2/WEB-INF/keycloak-saml.xml
index 14bd44e..16798d0 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee2/WEB-INF/keycloak-saml.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee2/WEB-INF/keycloak-saml.xml
@@ -23,7 +23,7 @@
         nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
         logoutPage="/logout.jsp"
         forceAuthentication="false">
-        <PrincipalNameMapping policy="FROM_NAME_ID"/>
+        <PrincipalNameMapping policy="FROM_ATTRIBUTE" attribute="email"/>
         <RoleIdentifiers>
             <Attribute name="memberOf"/>
             <Attribute name="Role"/>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/keycloak-subsystem.xsl b/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/keycloak-subsystem.xsl
index 114d875..24fe3e2 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/keycloak-subsystem.xsl
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/keycloak-subsystem.xsl
@@ -13,25 +13,29 @@
             <xsl:copy>
                 <xsl:apply-templates select="@* | node()" />
 
-                <secure-deployment name="customer-portal-subsystem.war">
-                    <realm>demo</realm>
-                    <realm-public-key>MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</realm-public-key>
-                    <auth-server-url><xsl:value-of select="$auth-server-host"/>/auth</auth-server-url>
-                    <ssl-required>EXTERNAL</ssl-required>
-                    <resource>customer-portal-subsystem</resource>
-                    <credential name="secret">password</credential>
-                </secure-deployment>
-                
-                <secure-deployment name="product-portal-subsystem.war">
-                    <realm>demo</realm>
-                    <realm-public-key>MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</realm-public-key>
-                    <auth-server-url><xsl:value-of select="$auth-server-host"/>/auth</auth-server-url>
-                    <ssl-required>EXTERNAL</ssl-required>
-                    <resource>product-portal-subsystem</resource>
-                    <credential name="secret">password</credential>
-                </secure-deployment>
+                <xsl:if test="not(*[local-name() = 'secure-deployment'])">
+
+                    <secure-deployment name="customer-portal-subsystem.war">
+                        <realm>demo</realm>
+                        <realm-public-key>MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</realm-public-key>
+                        <auth-server-url><xsl:value-of select="$auth-server-host"/>/auth</auth-server-url>
+                        <ssl-required>EXTERNAL</ssl-required>
+                        <resource>customer-portal-subsystem</resource>
+                        <credential name="secret">password</credential>
+                    </secure-deployment>
                 
+                    <secure-deployment name="product-portal-subsystem.war">
+                        <realm>demo</realm>
+                        <realm-public-key>MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</realm-public-key>
+                        <auth-server-url><xsl:value-of select="$auth-server-host"/>/auth</auth-server-url>
+                        <ssl-required>EXTERNAL</ssl-required>
+                        <resource>product-portal-subsystem</resource>
+                        <credential name="secret">password</credential>
+                    </secure-deployment>
+
+                </xsl:if>
             </xsl:copy>
+
     </xsl:template>
 
     <xsl:template match="@*|node()">
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
index f0a166b..b52a8de 100644
--- 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
@@ -54,8 +54,8 @@ public class EAP6SAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTest {
     }
 
     @Override
-    protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException {
-        log.infov("Preparing worker node ({0})", managementPort);
+    protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException {
+        log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort);
 
         OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions
           .standalone()
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/crossdc/EAP6SAMLAdapterCrossDCTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/crossdc/EAP6SAMLAdapterCrossDCTest.java
new file mode 100644
index 0000000..3a726ce
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/crossdc/EAP6SAMLAdapterCrossDCTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.crossdc;
+
+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.apache.commons.lang3.math.NumberUtils;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.TargetsContainer;
+import org.jboss.dmr.ModelNode;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.wildfly.extras.creaper.core.*;
+import org.wildfly.extras.creaper.core.online.*;
+import org.wildfly.extras.creaper.core.online.operations.*;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@Ignore("Infinispan version 5 does not support remote cache events, hence this test is left here for development purposes only")
+@AppServerContainer("app-server-eap6")
+public class EAP6SAMLAdapterCrossDCTest extends AbstractSAMLAdapterClusterTest {
+
+    @BeforeClass
+    public static void checkCrossDcTest() {
+        Assume.assumeThat("Seems not to be running cross-DC tests", System.getProperty("cache.server"), not(is("undefined")));
+    }
+
+    protected static final int PORT_OFFSET_CACHE_1 = NumberUtils.toInt(System.getProperty("cache.server.port.offset"), 0);
+    protected static final int CACHE_HOTROD_PORT_CACHE_1 = 11222 + PORT_OFFSET_CACHE_1;
+    protected static final int PORT_OFFSET_CACHE_2 = NumberUtils.toInt(System.getProperty("cache.server.2.port.offset"), 0);
+    protected static final int CACHE_HOTROD_PORT_CACHE_2 = 11222 + PORT_OFFSET_CACHE_2;
+
+    private final int[] CACHE_HOTROD_PORTS = new int[] { CACHE_HOTROD_PORT_CACHE_1, CACHE_HOTROD_PORT_CACHE_2 };
+    private final int[] TCPPING_PORTS = new int[] { 7600 + PORT_OFFSET_NODE_1, 7600 + PORT_OFFSET_NODE_2 };
+
+    private static final String SESSION_CACHE_NAME = EmployeeServletDistributable.DEPLOYMENT_NAME + "-cache";
+    private static final String SSO_CACHE_NAME = SESSION_CACHE_NAME + ".ssoCache";
+
+    private static final Address SESSION_CACHE_ADDR = Address.subsystem("infinispan")
+      .and("cache-container", "web")
+      .and("replicated-cache", SESSION_CACHE_NAME);
+    private static final Address SSO_CACHE_ADDR = Address.subsystem("infinispan")
+      .and("cache-container", "web")
+      .and("replicated-cache", SSO_CACHE_NAME);
+
+    private static final String JBOSS_WEB_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+      + "<jboss-web>\n"
+      + "    <replication-config>\n"
+      + "        <replication-granularity>SESSION</replication-granularity>\n"
+      + "        <cache-name>" + "web." + SESSION_CACHE_NAME + "</cache-name>\n"
+      + "    </replication-config>\n"
+      + "</jboss-web>";
+
+    @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)
+          .addAsWebInfResource(new StringAsset(JBOSS_WEB_XML), "jboss-web.xml");
+    }
+
+    @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(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException {
+        log.infov("Preparing worker node ({0} @ {1})", nodeIndex, 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[" + TCPPING_PORTS[nodeIndex] + "]"));
+        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", "1"));
+        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());
+
+        op.add(Address.of("socket-binding-group", "standard-sockets").and("remote-destination-outbound-socket-binding", "cache-server"),
+          Values.of("host", "localhost")
+            .and("port", CACHE_HOTROD_PORTS[nodeIndex]));
+
+        op.add(SESSION_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC"));
+        op.add(SESSION_CACHE_ADDR.and("remote-store", "REMOTE_STORE"),
+          Values.of("remote-servers", ModelNode.fromString("[{\"outbound-socket-binding\"=>\"cache-server\"}]"))
+            .and("cache", SESSION_CACHE_NAME)
+            .and("passivation", false)
+            .and("purge", false)
+            .and("preload", false)
+            .and("shared", true)
+        );
+
+        op.add(SSO_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC"));
+        op.add(SSO_CACHE_ADDR.and("remote-store", "REMOTE_STORE"),
+          Values.of("remote-servers", ModelNode.fromString("[{\"outbound-socket-binding\"=>\"cache-server\"}]"))
+            .and("cache", SSO_CACHE_NAME)
+            .and("passivation", false)
+            .and("purge", false)
+            .and("preload", false)
+            .and("shared", true)
+        );
+
+        Assert.assertTrue(op.writeAttribute(Address.subsystem("jgroups"), "default-stack", "tcpping").isSuccess());
+        Assert.assertTrue(op.writeAttribute(Address.subsystem("web"), "instance-id", "${jboss.node.name}").isSuccess());
+        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/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml
index 061e94e..2fe7f5e 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml
@@ -36,13 +36,19 @@
             <groupId>org.wildfly.extras.creaper</groupId>
             <artifactId>creaper-core</artifactId>
             <scope>test</scope>
-            <version>1.5.0</version>
+            <version>1.6.1</version>
         </dependency>
         <dependency>
             <groupId>org.wildfly.core</groupId>
             <artifactId>wildfly-cli</artifactId>
             <scope>test</scope>
-            <version>3.0.0.Beta30</version>
+            <version>${wildfly.core.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.wildfly.core</groupId>
+            <artifactId>wildfly-controller-client</artifactId>
+            <scope>test</scope>
+            <version>${wildfly.core.version}</version>
         </dependency>
     </dependencies>    
 
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
index eb7973c..5735a6a 100644
--- 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
@@ -53,8 +53,8 @@ public class WildflySAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTes
     }
 
     @Override
-    protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException {
-        log.infov("Preparing worker node ({0})", managementPort);
+    protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException {
+        log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort);
 
         OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions
           .standalone()
@@ -71,8 +71,6 @@ public class WildflySAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTes
         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"));
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/crossdc/WildflySAMLAdapterCrossDCTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/crossdc/WildflySAMLAdapterCrossDCTest.java
new file mode 100644
index 0000000..9288f20
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/crossdc/WildflySAMLAdapterCrossDCTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.crossdc;
+
+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.apache.commons.lang3.math.NumberUtils;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.TargetsContainer;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.wildfly.extras.creaper.core.*;
+import org.wildfly.extras.creaper.core.online.*;
+import org.wildfly.extras.creaper.core.online.operations.*;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@AppServerContainer("app-server-wildfly")
+public class WildflySAMLAdapterCrossDCTest extends AbstractSAMLAdapterClusterTest {
+
+    @BeforeClass
+    public static void checkCrossDcTest() {
+        Assume.assumeThat("Seems not to be running cross-DC tests", System.getProperty("cache.server"), not(is("undefined")));
+    }
+
+    protected static final int PORT_OFFSET_CACHE_1 = NumberUtils.toInt(System.getProperty("cache.server.port.offset"), 0);
+    protected static final int CACHE_HOTROD_PORT_CACHE_1 = 11222 + PORT_OFFSET_CACHE_1;
+    protected static final int PORT_OFFSET_CACHE_2 = NumberUtils.toInt(System.getProperty("cache.server.2.port.offset"), 0);
+    protected static final int CACHE_HOTROD_PORT_CACHE_2 = 11222 + PORT_OFFSET_CACHE_2;
+
+    private final int[] CACHE_HOTROD_PORTS = new int[] { CACHE_HOTROD_PORT_CACHE_1, CACHE_HOTROD_PORT_CACHE_2 };
+    private final int[] TCPPING_PORTS = new int[] { 7600 + PORT_OFFSET_NODE_1, 7600 + PORT_OFFSET_NODE_2 };
+
+    private static final String SESSION_CACHE_NAME = EmployeeServletDistributable.DEPLOYMENT_NAME + "-cache";
+    private static final String SSO_CACHE_NAME = SESSION_CACHE_NAME + ".ssoCache";
+
+    private static final Address SESSION_CACHE_ADDR = Address.subsystem("infinispan")
+      .and("cache-container", "web")
+      .and("replicated-cache", SESSION_CACHE_NAME);
+    private static final Address SSO_CACHE_ADDR = Address.subsystem("infinispan")
+      .and("cache-container", "web")
+      .and("replicated-cache", SSO_CACHE_NAME);
+
+    private static final String JBOSS_WEB_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+      + "<jboss-web>\n"
+      + "    <replication-config>\n"
+      + "        <replication-granularity>SESSION</replication-granularity>\n"
+      + "        <cache-name>" + "web." + SESSION_CACHE_NAME + "</cache-name>\n"
+      + "    </replication-config>\n"
+      + "</jboss-web>";
+
+    @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)
+          .addAsWebInfResource(new StringAsset(JBOSS_WEB_XML), "jboss-web.xml");
+    }
+
+    @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(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException {
+        log.infov("Preparing worker node ({0} @ {1})", nodeIndex, 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[" + TCPPING_PORTS[nodeIndex] + "]"));
+        b.add(tcppingStack.and("protocol", "TCPPING").and("property", "port_range"), Values.of("value", "0"));
+        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.of("socket-binding-group", "standard-sockets").and("remote-destination-outbound-socket-binding", "cache-server"),
+          Values.of("host", "localhost")
+            .and("port", CACHE_HOTROD_PORTS[nodeIndex]));
+
+        op.add(SESSION_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC"));
+        op.writeAttribute(SESSION_CACHE_ADDR.and("component", "locking"), "isolation", "REPEATABLE_READ");
+        op.writeAttribute(SESSION_CACHE_ADDR.and("component", "transaction"), "mode", "BATCH");
+        op.add(SESSION_CACHE_ADDR.and("store", "remote"),
+          Values.ofList("remote-servers", "cache-server")
+            .and("cache", SESSION_CACHE_NAME)
+            .and("passivation", false)
+            .and("purge", false)
+            .and("preload", false)
+            .and("shared", true)
+        );
+
+        op.add(SSO_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC"));
+        op.add(SSO_CACHE_ADDR.and("store", "remote"),
+          Values.ofList("remote-servers", "cache-server")
+            .and("cache", SSO_CACHE_NAME)
+            .and("passivation", false)
+            .and("purge", false)
+            .and("preload", false)
+            .and("shared", true)
+        );
+
+        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/wildfly10/src/test/java/org/keycloak/testsuite/adapter/cluster/Wildfly10SAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/cluster/Wildfly10SAMLAdapterClusterTest.java
index 5a26448..d80fda5 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/cluster/Wildfly10SAMLAdapterClusterTest.java
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/cluster/Wildfly10SAMLAdapterClusterTest.java
@@ -53,8 +53,8 @@ public class Wildfly10SAMLAdapterClusterTest extends AbstractSAMLAdapterClusterT
     }
 
     @Override
-    protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException {
-        log.infov("Preparing worker node ({0})", managementPort);
+    protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException {
+        log.infov("Preparing worker node ({0} @ {1})", nodeIndex, managementPort);
 
         OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions
           .standalone()
@@ -71,8 +71,6 @@ public class Wildfly10SAMLAdapterClusterTest extends AbstractSAMLAdapterClusterT
         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"));
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/crossdc/Wildfly10SAMLAdapterCrossDCTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/crossdc/Wildfly10SAMLAdapterCrossDCTest.java
new file mode 100644
index 0000000..9c7d935
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/crossdc/Wildfly10SAMLAdapterCrossDCTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.crossdc;
+
+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.apache.commons.lang3.math.NumberUtils;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.TargetsContainer;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.wildfly.extras.creaper.core.*;
+import org.wildfly.extras.creaper.core.online.*;
+import org.wildfly.extras.creaper.core.online.operations.*;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@AppServerContainer("app-server-wildfly10")
+public class Wildfly10SAMLAdapterCrossDCTest extends AbstractSAMLAdapterClusterTest {
+
+    @BeforeClass
+    public static void checkCrossDcTest() {
+        Assume.assumeThat("Seems not to be running cross-DC tests", System.getProperty("cache.server"), not(is("undefined")));
+    }
+
+    protected static final int PORT_OFFSET_CACHE_1 = NumberUtils.toInt(System.getProperty("cache.server.port.offset"), 0);
+    protected static final int CACHE_HOTROD_PORT_CACHE_1 = 11222 + PORT_OFFSET_CACHE_1;
+    protected static final int PORT_OFFSET_CACHE_2 = NumberUtils.toInt(System.getProperty("cache.server.2.port.offset"), 0);
+    protected static final int CACHE_HOTROD_PORT_CACHE_2 = 11222 + PORT_OFFSET_CACHE_2;
+
+    private final int[] CACHE_HOTROD_PORTS = new int[] { CACHE_HOTROD_PORT_CACHE_1, CACHE_HOTROD_PORT_CACHE_2 };
+    private final int[] TCPPING_PORTS = new int[] { 7600 + PORT_OFFSET_NODE_1, 7600 + PORT_OFFSET_NODE_2 };
+
+    private static final String SESSION_CACHE_NAME = EmployeeServletDistributable.DEPLOYMENT_NAME + "-cache";
+    private static final String SSO_CACHE_NAME = SESSION_CACHE_NAME + ".ssoCache";
+
+    private static final Address SESSION_CACHE_ADDR = Address.subsystem("infinispan")
+      .and("cache-container", "web")
+      .and("replicated-cache", SESSION_CACHE_NAME);
+    private static final Address SSO_CACHE_ADDR = Address.subsystem("infinispan")
+      .and("cache-container", "web")
+      .and("replicated-cache", SSO_CACHE_NAME);
+
+    private static final String JBOSS_WEB_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+      + "<jboss-web>\n"
+      + "    <replication-config>\n"
+      + "        <replication-granularity>SESSION</replication-granularity>\n"
+      + "        <cache-name>" + "web." + SESSION_CACHE_NAME + "</cache-name>\n"
+      + "    </replication-config>\n"
+      + "</jboss-web>";
+
+    @TargetsContainer(value = "app-server-wildfly10-" + 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)
+          .addAsWebInfResource(new StringAsset(JBOSS_WEB_XML), "jboss-web.xml");
+    }
+
+    @TargetsContainer(value = "app-server-wildfly10-" + NODE_2_NAME)
+    @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME + "_2", managed = false)
+    protected static WebArchive employee2() {
+        return employee();
+    }
+
+    @Override
+    protected void prepareWorkerNode(int nodeIndex, Integer managementPort) throws IOException, CliException, NumberFormatException {
+        log.infov("Preparing worker node ({0} @ {1})", nodeIndex, 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[" + TCPPING_PORTS[nodeIndex] + "]"));
+        b.add(tcppingStack.and("protocol", "TCPPING").and("property", "port_range"), Values.of("value", "0"));
+        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.of("socket-binding-group", "standard-sockets").and("remote-destination-outbound-socket-binding", "cache-server"),
+          Values.of("host", "localhost")
+            .and("port", CACHE_HOTROD_PORTS[nodeIndex]));
+
+        op.add(SESSION_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC"));
+        op.writeAttribute(SESSION_CACHE_ADDR.and("component", "locking"), "isolation", "REPEATABLE_READ");
+        op.writeAttribute(SESSION_CACHE_ADDR.and("component", "transaction"), "mode", "BATCH");
+        op.add(SESSION_CACHE_ADDR.and("store", "remote"),
+          Values.ofList("remote-servers", "cache-server")
+            .and("cache", SESSION_CACHE_NAME)
+            .and("passivation", false)
+            .and("purge", false)
+            .and("preload", false)
+            .and("shared", true)
+        );
+
+        op.add(SSO_CACHE_ADDR, Values.of("statistics-enabled", "true").and("mode", "SYNC"));
+        op.add(SSO_CACHE_ADDR.and("store", "remote"),
+          Values.ofList("remote-servers", "cache-server")
+            .and("cache", SSO_CACHE_NAME)
+            .and("passivation", false)
+            .and("purge", false)
+            .and("preload", false)
+            .and("shared", true)
+        );
+
+        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);
+    }
+
+}