keycloak-uncached

Changes

Details

diff --git a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/InfinispanSessionCacheIdMapperUpdater.java b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/InfinispanSessionCacheIdMapperUpdater.java
new file mode 100644
index 0000000..b6f4c23
--- /dev/null
+++ b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/InfinispanSessionCacheIdMapperUpdater.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.saml.jbossweb.infinispan;
+
+import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
+
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.servlet.ServletContext;
+import org.apache.catalina.Context;
+import org.infinispan.Cache;
+import org.infinispan.configuration.cache.CacheMode;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class InfinispanSessionCacheIdMapperUpdater {
+
+    private static final Logger LOG = Logger.getLogger(InfinispanSessionCacheIdMapperUpdater.class);
+
+    public static final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container/web";
+
+    private static final String DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheContainerJndi";
+    private static final String DEPLOYMENT_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.deploymentCacheName";
+    private static final String SSO_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheName";
+
+    public static SessionIdMapperUpdater addTokenStoreUpdaters(Context context, SessionIdMapper mapper, SessionIdMapperUpdater previousIdMapperUpdater) {
+       boolean distributable = context.getDistributable();
+
+        if (! distributable) {
+            LOG.warnv("Deployment {0} does not use supported distributed session cache mechanism", context.getName());
+            return previousIdMapperUpdater;
+        }
+
+        ServletContext servletContext = context.getServletContext();
+        String cacheContainerLookup = (servletContext != null && servletContext.getInitParameter(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME) != null)
+          ? servletContext.getInitParameter(DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME)
+          : DEFAULT_CACHE_CONTAINER_JNDI_NAME;
+
+        // the following is based on https://github.com/jbossas/jboss-as/blob/7.2.0.Final/clustering/web-infinispan/src/main/java/org/jboss/as/clustering/web/infinispan/DistributedCacheManagerFactory.java#L116-L122
+        String host = context.getParent() == null ? "" : context.getParent().getName();
+        String contextPath = context.getPath();
+        if ("/".equals(contextPath)) {
+            contextPath = "/ROOT";
+        }
+
+        boolean deploymentSessionCacheNamePreset = servletContext != null && servletContext.getInitParameter(DEPLOYMENT_CACHE_NAME_PARAM_NAME) != null;
+        String deploymentSessionCacheName = deploymentSessionCacheNamePreset
+          ? servletContext.getInitParameter(DEPLOYMENT_CACHE_NAME_PARAM_NAME)
+          : host + contextPath;
+        boolean ssoCacheNamePreset = servletContext != null && servletContext.getInitParameter(SSO_CACHE_NAME_PARAM_NAME) != null;
+        String ssoCacheName = ssoCacheNamePreset
+          ? servletContext.getInitParameter(SSO_CACHE_NAME_PARAM_NAME)
+          : deploymentSessionCacheName + ".ssoCache";
+
+        try {
+            EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
+
+            Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(ssoCacheName);
+            if (ssoCacheConfiguration == null) {
+                Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName);
+                if (cacheConfiguration == null) {
+                    LOG.debugv("Using default cache container configuration for SSO cache. lookup={0}, looked up configuration of cache={1}", cacheContainerLookup, deploymentSessionCacheName);
+                    ssoCacheConfiguration = cacheManager.getDefaultCacheConfiguration();
+                } else {
+                    LOG.debugv("Using distributed HTTP session cache configuration for SSO cache. lookup={0}, configuration taken from cache={1}", cacheContainerLookup, deploymentSessionCacheName);
+                    ssoCacheConfiguration = cacheConfiguration;
+                    cacheManager.defineConfiguration(ssoCacheName, ssoCacheConfiguration);
+                }
+            } else {
+                LOG.debugv("Using custom configuration for SSO cache. lookup={0}, cache name={1}", cacheContainerLookup, ssoCacheName);
+            }
+
+            CacheMode ssoCacheMode = ssoCacheConfiguration.clustering().cacheMode();
+            if (ssoCacheMode != CacheMode.REPL_ASYNC && ssoCacheMode != CacheMode.REPL_SYNC) {
+                LOG.warnv("SSO cache mode is {0}, it is recommended to use replicated mode instead", ssoCacheConfiguration.clustering().cacheModeString());
+            }
+
+            Cache<String, String[]> ssoCache = cacheManager.getCache(ssoCacheName, true);
+            ssoCache.addListener(new SsoSessionCacheListener(mapper));
+
+            LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, deploymentSessionCacheName);
+
+            SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater);
+
+            return updater;
+        } catch (NamingException ex) {
+            LOG.warnv("Failed to obtain distributed session cache container, lookup={0}", cacheContainerLookup);
+            return previousIdMapperUpdater;
+        }
+    }
+}
diff --git a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoCacheSessionIdMapperUpdater.java b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoCacheSessionIdMapperUpdater.java
new file mode 100644
index 0000000..f60e802
--- /dev/null
+++ b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoCacheSessionIdMapperUpdater.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.saml.jbossweb.infinispan;
+
+import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
+
+import org.infinispan.Cache;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class SsoCacheSessionIdMapperUpdater implements SessionIdMapperUpdater {
+
+    private final SessionIdMapperUpdater delegate;
+    /**
+     * Cache where key is a HTTP session ID, and value is a pair (user session ID, principal name) of Strings.
+     */
+    private final Cache<String, String[]> httpSessionToSsoCache;
+
+    public SsoCacheSessionIdMapperUpdater(Cache<String, String[]> httpSessionToSsoCache, SessionIdMapperUpdater previousIdMapperUpdater) {
+        this.delegate = previousIdMapperUpdater;
+        this.httpSessionToSsoCache = httpSessionToSsoCache;
+    }
+
+    // SessionIdMapperUpdater methods
+
+    @Override
+    public void clear(SessionIdMapper idMapper) {
+        httpSessionToSsoCache.clear();
+        this.delegate.clear(idMapper);
+    }
+
+    @Override
+    public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) {
+        httpSessionToSsoCache.put(httpSessionId, new String[] {sso, principal});
+        this.delegate.map(idMapper, sso, principal, httpSessionId);
+    }
+
+    @Override
+    public void removeSession(SessionIdMapper idMapper, String httpSessionId) {
+        httpSessionToSsoCache.remove(httpSessionId);
+        this.delegate.removeSession(idMapper, httpSessionId);
+    }
+}
diff --git a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoSessionCacheListener.java b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoSessionCacheListener.java
new file mode 100644
index 0000000..ee100ad
--- /dev/null
+++ b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/infinispan/SsoSessionCacheListener.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.saml.jbossweb.infinispan;
+
+import org.keycloak.adapters.spi.SessionIdMapper;
+
+import java.util.*;
+import java.util.concurrent.*;
+import org.infinispan.notifications.Listener;
+import org.infinispan.notifications.cachelistener.annotation.*;
+import org.infinispan.notifications.cachelistener.event.*;
+import org.infinispan.notifications.cachemanagerlistener.annotation.CacheStarted;
+import org.infinispan.notifications.cachemanagerlistener.annotation.CacheStopped;
+import org.infinispan.notifications.cachemanagerlistener.event.CacheStartedEvent;
+import org.infinispan.notifications.cachemanagerlistener.event.CacheStoppedEvent;
+import org.infinispan.transaction.xa.GlobalTransaction;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@Listener
+public class SsoSessionCacheListener {
+
+    private static final Logger LOG = Logger.getLogger(SsoSessionCacheListener.class);
+
+    private final ConcurrentMap<GlobalTransaction, Queue<Event>> map = new ConcurrentHashMap<>();
+
+    private final SessionIdMapper idMapper;
+
+    private ExecutorService executor = Executors.newSingleThreadExecutor();
+
+    public SsoSessionCacheListener(SessionIdMapper idMapper) {
+        this.idMapper = idMapper;
+    }
+
+    @TransactionRegistered
+    public void startTransaction(TransactionRegisteredEvent event) {
+        map.put(event.getGlobalTransaction(), new ConcurrentLinkedQueue<Event>());
+    }
+
+    @CacheStarted
+    public void cacheStarted(CacheStartedEvent event) {
+        this.executor = Executors.newSingleThreadExecutor();
+    }
+
+    @CacheStopped
+    public void cacheStopped(CacheStoppedEvent event) {
+        this.executor.shutdownNow();
+    }
+
+    @CacheEntryCreated
+    @CacheEntryRemoved
+    @CacheEntryModified
+    public void addEvent(TransactionalEvent event) {
+        if (event.isPre() == false) {
+            map.get(event.getGlobalTransaction()).add(event);
+        }
+    }
+
+    @TransactionCompleted
+    public void endTransaction(TransactionCompletedEvent event) {
+        Queue<Event> events = map.remove(event.getGlobalTransaction());
+
+        if (events == null || ! event.isTransactionSuccessful()) {
+            return;
+        }
+
+        if (event.isOriginLocal()) {
+            // Local events are processed by local HTTP session listener
+            return;
+        }
+
+        for (final Event e : events) {
+            switch (e.getType()) {
+                case CACHE_ENTRY_CREATED:
+                    this.executor.submit(new Runnable() {
+                        @Override public void run() {
+                            cacheEntryCreated((CacheEntryCreatedEvent) e);
+                        }
+                    });
+                    break;
+
+                case CACHE_ENTRY_MODIFIED:
+                    this.executor.submit(new Runnable() {
+                        @Override public void run() {
+                            cacheEntryModified((CacheEntryModifiedEvent) e);
+                        }
+                    });
+                    break;
+
+                case CACHE_ENTRY_REMOVED:
+                    this.executor.submit(new Runnable() {
+                        @Override public void run() {
+                            cacheEntryRemoved((CacheEntryRemovedEvent) e);
+                        }
+                    });
+                    break;
+            }
+        }
+    }
+
+    private void cacheEntryCreated(CacheEntryCreatedEvent event) {
+        if (! (event.getKey() instanceof String) || ! (event.getValue() instanceof String[])) {
+            return;
+        }
+        String httpSessionId = (String) event.getKey();
+        String[] value = (String[]) event.getValue();
+        String ssoId = value[0];
+        String principal = value[1];
+
+        LOG.tracev("cacheEntryCreated {0}:{1}", httpSessionId, ssoId);
+
+        this.idMapper.map(ssoId, principal, httpSessionId);
+    }
+
+    private void cacheEntryModified(CacheEntryModifiedEvent event) {
+        if (! (event.getKey() instanceof String) || ! (event.getValue() instanceof String[])) {
+            return;
+        }
+        String httpSessionId = (String) event.getKey();
+        String[] value = (String[]) event.getValue();
+        String ssoId = value[0];
+        String principal = value[1];
+
+        LOG.tracev("cacheEntryModified {0}:{1}", httpSessionId, ssoId);
+
+        this.idMapper.removeSession(httpSessionId);
+        this.idMapper.map(ssoId, principal, httpSessionId);
+    }
+
+    private void cacheEntryRemoved(CacheEntryRemovedEvent event) {
+        if (! (event.getKey() instanceof String)) {
+            return;
+        }
+
+        LOG.tracev("cacheEntryRemoved {0}", event.getKey());
+
+        this.idMapper.removeSession((String) event.getKey());
+    }
+}
diff --git a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/SamlAuthenticatorValve.java b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/SamlAuthenticatorValve.java
index 3ec61a7..99d5393 100755
--- a/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/SamlAuthenticatorValve.java
+++ b/adapters/saml/as7-eap6/adapter/src/main/java/org/keycloak/adapters/saml/jbossweb/SamlAuthenticatorValve.java
@@ -22,9 +22,10 @@ import org.apache.catalina.connector.Request;
 import org.apache.catalina.connector.Response;
 import org.apache.catalina.core.StandardContext;
 import org.apache.catalina.deploy.LoginConfig;
+
 import org.keycloak.adapters.jbossweb.JBossWebPrincipalFactory;
-import org.keycloak.adapters.saml.AbstractSamlAuthenticatorValve;
-import org.keycloak.adapters.saml.SamlDeployment;
+import org.keycloak.adapters.saml.*;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
 import org.keycloak.adapters.tomcat.GenericPrincipalFactory;
 
 import javax.servlet.http.HttpServletResponse;
@@ -71,4 +72,11 @@ public class SamlAuthenticatorValve extends AbstractSamlAuthenticatorValve {
     protected GenericPrincipalFactory createPrincipalFactory() {
         return new JBossWebPrincipalFactory();
     }
+
+    @Override
+    protected void addTokenStoreUpdaters() {
+        context.addApplicationListenerInstance(new IdMapperUpdaterSessionListener(mapper));
+        setIdMapperUpdater(SessionIdMapperUpdater.EXTERNAL);
+        super.addTokenStoreUpdaters();
+    }
 }
diff --git a/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/Tomcat8SamlSessionStore.java b/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/Tomcat8SamlSessionStore.java
index a3d80fb..57fef3e 100755
--- a/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/Tomcat8SamlSessionStore.java
+++ b/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/Tomcat8SamlSessionStore.java
@@ -24,6 +24,7 @@ import org.keycloak.adapters.saml.CatalinaSamlSessionStore;
 import org.keycloak.adapters.saml.SamlDeployment;
 import org.keycloak.adapters.spi.HttpFacade;
 import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
 import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement;
 import org.keycloak.adapters.tomcat.GenericPrincipalFactory;
 
@@ -33,7 +34,7 @@ import org.keycloak.adapters.tomcat.GenericPrincipalFactory;
  */
 public class Tomcat8SamlSessionStore extends CatalinaSamlSessionStore {
     public Tomcat8SamlSessionStore(CatalinaUserSessionManagement sessionManagement, GenericPrincipalFactory principalFactory, SessionIdMapper idMapper, Request request, AbstractSamlAuthenticatorValve valve, HttpFacade facade, SamlDeployment deployment) {
-        super(sessionManagement, principalFactory, idMapper, request, valve, facade, deployment);
+        super(sessionManagement, principalFactory, idMapper, SessionIdMapperUpdater.DIRECT, request, valve, facade, deployment);
     }
 
     @Override
diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java
index bc23f17..c356391 100755
--- a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java
+++ b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java
@@ -25,13 +25,10 @@ import org.apache.catalina.authenticator.FormAuthenticator;
 import org.apache.catalina.connector.Request;
 import org.apache.catalina.connector.Response;
 import org.jboss.logging.Logger;
+
 import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
 import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
-import org.keycloak.adapters.spi.AuthChallenge;
-import org.keycloak.adapters.spi.AuthOutcome;
-import org.keycloak.adapters.spi.HttpFacade;
-import org.keycloak.adapters.spi.InMemorySessionIdMapper;
-import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.*;
 import org.keycloak.adapters.tomcat.CatalinaHttpFacade;
 import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement;
 import org.keycloak.adapters.tomcat.GenericPrincipalFactory;
@@ -46,6 +43,8 @@ import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.reflect.*;
+import java.util.Map;
 
 /**
  * Keycloak authentication valve
@@ -62,6 +61,7 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
 	protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
     protected SamlDeploymentContext deploymentContext;
     protected SessionIdMapper mapper = new InMemorySessionIdMapper();
+    protected SessionIdMapperUpdater idMapperUpdater = SessionIdMapperUpdater.DIRECT;
 
     @Override
     public void lifecycleEvent(LifecycleEvent event) {
@@ -69,7 +69,7 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
             cache = false;
         } else if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) {
         	keycloakInit();
-        } else if (event.getType() == Lifecycle.BEFORE_STOP_EVENT) {
+        } else if (Lifecycle.BEFORE_STOP_EVENT.equals(event.getType())) {
             beforeStop();
         }
     }
@@ -129,6 +129,8 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
         }
 
         context.getServletContext().setAttribute(SamlDeploymentContext.class.getName(), deploymentContext);
+
+        addTokenStoreUpdaters();
     }
 
     protected void beforeStop() {
@@ -273,8 +275,68 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
 
     protected SamlSessionStore createSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) {
         SamlSessionStore store;
-        store = new CatalinaSamlSessionStore(userSessionManagement, createPrincipalFactory(), mapper, request, this, facade, resolvedDeployment);
+        store = new CatalinaSamlSessionStore(userSessionManagement, createPrincipalFactory(), mapper, idMapperUpdater, request, this, facade, resolvedDeployment);
         return store;
     }
 
+    protected void addTokenStoreUpdaters() {
+        SessionIdMapperUpdater updater = getIdMapperUpdater();
+
+        try {
+            String idMapperSessionUpdaterClasses = context.getServletContext().getInitParameter("keycloak.sessionIdMapperUpdater.classes");
+            if (idMapperSessionUpdaterClasses == null) {
+                return;
+            }
+
+            for (String clazz : idMapperSessionUpdaterClasses.split("\\s*,\\s*")) {
+                if (! clazz.isEmpty()) {
+                    updater = invokeAddTokenStoreUpdaterMethod(clazz, updater);
+                }
+            }
+        } finally {
+            setIdMapperUpdater(updater);
+        }
+    }
+
+    private SessionIdMapperUpdater invokeAddTokenStoreUpdaterMethod(String idMapperSessionUpdaterClass, SessionIdMapperUpdater previousIdMapperUpdater) {
+        try {
+            Class<?> clazz = context.getLoader().getClassLoader().loadClass(idMapperSessionUpdaterClass);
+            Method addTokenStoreUpdatersMethod = clazz.getMethod("addTokenStoreUpdaters", Context.class, SessionIdMapper.class, SessionIdMapperUpdater.class);
+            if (! Modifier.isStatic(addTokenStoreUpdatersMethod.getModifiers())
+              || ! Modifier.isPublic(addTokenStoreUpdatersMethod.getModifiers())
+              || ! SessionIdMapperUpdater.class.isAssignableFrom(addTokenStoreUpdatersMethod.getReturnType())) {
+                log.errorv("addTokenStoreUpdaters method in class {0} has to be public static. Ignoring class.", idMapperSessionUpdaterClass);
+                return previousIdMapperUpdater;
+            }
+
+            log.debugv("Initializing sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
+            return (SessionIdMapperUpdater) addTokenStoreUpdatersMethod.invoke(null, context, mapper, previousIdMapperUpdater);
+        } catch (ClassNotFoundException ex) {
+            log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
+            return previousIdMapperUpdater;
+        } catch (NoSuchMethodException ex) {
+            log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
+            return previousIdMapperUpdater;
+        } catch (SecurityException ex) {
+            log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
+            return previousIdMapperUpdater;
+        } catch (IllegalAccessException ex) {
+            log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass);
+            return previousIdMapperUpdater;
+        } catch (IllegalArgumentException ex) {
+            log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass);
+            return previousIdMapperUpdater;
+        } catch (InvocationTargetException ex) {
+            log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass);
+            return previousIdMapperUpdater;
+        }
+    }
+
+    public SessionIdMapperUpdater getIdMapperUpdater() {
+        return idMapperUpdater;
+    }
+
+    public void setIdMapperUpdater(SessionIdMapperUpdater idMapperUpdater) {
+        this.idMapperUpdater = idMapperUpdater;
+    }
 }
diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java
index 281dd59..a74966b 100755
--- a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java
+++ b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java
@@ -24,6 +24,7 @@ import org.apache.catalina.realm.GenericPrincipal;
 import org.jboss.logging.Logger;
 import org.keycloak.adapters.spi.HttpFacade;
 import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
 import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement;
 import org.keycloak.adapters.tomcat.GenericPrincipalFactory;
 import org.keycloak.common.util.KeycloakUriBuilder;
@@ -45,17 +46,20 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
     private final CatalinaUserSessionManagement sessionManagement;
     protected final GenericPrincipalFactory principalFactory;
     private final SessionIdMapper idMapper;
+    private final SessionIdMapperUpdater idMapperUpdater;
     protected final Request request;
     protected final AbstractSamlAuthenticatorValve valve;
     protected final HttpFacade facade;
     protected final SamlDeployment deployment;
 
     public CatalinaSamlSessionStore(CatalinaUserSessionManagement sessionManagement, GenericPrincipalFactory principalFactory,
-                                    SessionIdMapper idMapper, Request request, AbstractSamlAuthenticatorValve valve, HttpFacade facade,
+                                    SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater,
+                                    Request request, AbstractSamlAuthenticatorValve valve, HttpFacade facade,
                                     SamlDeployment deployment) {
         this.sessionManagement = sessionManagement;
         this.principalFactory = principalFactory;
         this.idMapper = idMapper;
+        this.idMapperUpdater = idMapperUpdater;
         this.request = request;
         this.valve = valve;
         this.facade = facade;
@@ -89,11 +93,13 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
         Session sessionInternal = request.getSessionInternal(false);
         if (sessionInternal == null) return;
         HttpSession session = sessionInternal.getSession();
+        List<String> ids = new LinkedList<String>();
         if (session != null) {
             SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
             if (samlSession != null) {
                 if (samlSession.getSessionIndex() != null) {
-                    idMapper.removeSession(session.getId());
+                    ids.add(session.getId());
+                    idMapperUpdater.removeSession(idMapper, session.getId());
                 }
                 session.removeAttribute(SamlSession.class.getName());
             }
@@ -101,6 +107,7 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
         }
         sessionInternal.setPrincipal(null);
         sessionInternal.setAuthType(null);
+        logoutSessionIds(ids);
     }
 
     @Override
@@ -111,7 +118,7 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
             ids.addAll(sessions);
             logoutSessionIds(ids);
             for (String id : ids) {
-                idMapper.removeSession(id);
+                idMapperUpdater.removeSession(idMapper, id);
             }
         }
 
@@ -125,7 +132,7 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
              String sessionId = idMapper.getSessionFromSSO(id);
              if (sessionId != null) {
                  sessionIds.add(sessionId);
-                 idMapper.removeSession(sessionId);
+                 idMapperUpdater.removeSession(idMapper, sessionId);
              }
 
         }
@@ -141,7 +148,6 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
     @Override
     public boolean isLoggedIn() {
         Session session = request.getSessionInternal(false);
-        if (session == null) return false;
         if (session == null) {
             log.debug("session was null, returning null");
             return false;
@@ -193,7 +199,7 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
         request.setUserPrincipal(principal);
         request.setAuthType("KEYCLOAK-SAML");
         String newId = changeSessionId(session);
-        idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), newId);
+        idMapperUpdater.map(idMapper, account.getSessionIndex(), account.getPrincipal().getSamlSubject(), newId);
 
     }
 
diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/IdMapperUpdaterSessionListener.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/IdMapperUpdaterSessionListener.java
new file mode 100644
index 0000000..4fc7814
--- /dev/null
+++ b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/IdMapperUpdaterSessionListener.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.saml;
+
+import org.keycloak.adapters.spi.SessionIdMapper;
+
+import java.util.Objects;
+import javax.servlet.http.*;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class IdMapperUpdaterSessionListener implements HttpSessionListener, HttpSessionAttributeListener {
+
+    private final SessionIdMapper idMapper;
+
+    public IdMapperUpdaterSessionListener(SessionIdMapper idMapper) {
+        this.idMapper = idMapper;
+    }
+
+    @Override
+    public void sessionCreated(HttpSessionEvent hse) {
+        HttpSession session = hse.getSession();
+        Object value = session.getAttribute(SamlSession.class.getName());
+        map(session.getId(), value);
+    }
+
+    @Override
+    public void sessionDestroyed(HttpSessionEvent hse) {
+        HttpSession session = hse.getSession();
+        unmap(session.getId(), session.getAttribute(SamlSession.class.getName()));
+    }
+
+    @Override
+    public void attributeAdded(HttpSessionBindingEvent hsbe) {
+        HttpSession session = hsbe.getSession();
+        if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) {
+            map(session.getId(), hsbe.getValue());
+        }
+    }
+
+    @Override
+    public void attributeRemoved(HttpSessionBindingEvent hsbe) {
+        HttpSession session = hsbe.getSession();
+        if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) {
+            unmap(session.getId(), hsbe.getValue());
+        }
+    }
+
+    @Override
+    public void attributeReplaced(HttpSessionBindingEvent hsbe) {
+        HttpSession session = hsbe.getSession();
+        if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) {
+            unmap(session.getId(), hsbe.getValue());
+            map(session.getId(), session.getAttribute(SamlSession.class.getName()));
+        }
+    }
+
+    private void map(String sessionId, Object value) {
+        if (! (value instanceof SamlSession) || sessionId == null) {
+            return;
+        }
+        SamlSession account = (SamlSession) value;
+
+        idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId);
+    }
+
+    private void unmap(String sessionId, Object value) {
+        if (! (value instanceof SamlSession) || sessionId == null) {
+            return;
+        }
+
+        SamlSession samlSession = (SamlSession) value;
+        if (samlSession.getSessionIndex() != null) {
+            idMapper.removeSession(sessionId);
+        }
+    }
+}
diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/IdMapperUpdaterSessionListener.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/IdMapperUpdaterSessionListener.java
new file mode 100644
index 0000000..692413e
--- /dev/null
+++ b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/IdMapperUpdaterSessionListener.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.saml.undertow;
+
+import org.keycloak.adapters.saml.SamlSession;
+import org.keycloak.adapters.spi.SessionIdMapper;
+
+import io.undertow.server.HttpServerExchange;
+import io.undertow.server.session.Session;
+import io.undertow.server.session.SessionListener;
+import java.util.Objects;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class IdMapperUpdaterSessionListener implements SessionListener {
+
+    private final SessionIdMapper idMapper;
+
+    public IdMapperUpdaterSessionListener(SessionIdMapper idMapper) {
+        this.idMapper = idMapper;
+    }
+
+    @Override
+    public void sessionCreated(Session session, HttpServerExchange exchange) {
+        Object value = session.getAttribute(SamlSession.class.getName());
+        map(session.getId(), value);
+    }
+
+    @Override
+    public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) {
+        if (reason != SessionDestroyedReason.UNDEPLOY) {
+            unmap(session.getId(), session.getAttribute(SamlSession.class.getName()));
+        }
+    }
+
+    @Override
+    public void attributeAdded(Session session, String name, Object value) {
+        if (Objects.equals(name, SamlSession.class.getName())) {
+            map(session.getId(), value);
+        }
+    }
+
+    @Override
+    public void attributeUpdated(Session session, String name, Object newValue, Object oldValue) {
+        if (Objects.equals(name, SamlSession.class.getName())) {
+            unmap(session.getId(), oldValue);
+            map(session.getId(), newValue);
+        }
+    }
+
+    @Override
+    public void attributeRemoved(Session session, String name, Object oldValue) {
+        if (Objects.equals(name, SamlSession.class.getName())) {
+            unmap(session.getId(), oldValue);
+        }
+    }
+
+    @Override
+    public void sessionIdChanged(Session session, String oldSessionId) {
+        Object value = session.getAttribute(SamlSession.class.getName());
+        if (value != null) {
+            unmap(oldSessionId, value);
+            map(session.getId(), value);
+        }
+    }
+
+    private void map(String sessionId, Object value) {
+        if (! (value instanceof SamlSession) || sessionId == null) {
+            return;
+        }
+        SamlSession account = (SamlSession) value;
+
+        idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId);
+    }
+
+    private void unmap(String sessionId, Object value) {
+        if (! (value instanceof SamlSession) || sessionId == null) {
+            return;
+        }
+
+        SamlSession samlSession = (SamlSession) value;
+        if (samlSession.getSessionIndex() != null) {
+            idMapper.removeSession(sessionId);
+        }
+    }
+
+}
diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java
index 0e6a1a1..4985dfb 100755
--- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java
+++ b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java
@@ -154,7 +154,7 @@ public class SamlServletExtension implements ServletExtension {
         servletContext.setAttribute(SamlDeploymentContext.class.getName(), deploymentContext);
         UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement();
         final ServletSamlAuthMech mech = createAuthMech(deploymentInfo, deploymentContext, userSessionManagement);
-
+        mech.addTokenStoreUpdaters(deploymentInfo);
 
         // setup handlers
 
diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlAuthMech.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlAuthMech.java
index 8818171..b0a7339 100755
--- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlAuthMech.java
+++ b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlAuthMech.java
@@ -21,35 +21,89 @@ import io.undertow.security.api.SecurityContext;
 import io.undertow.server.HttpServerExchange;
 import io.undertow.servlet.handlers.ServletRequestContext;
 import io.undertow.util.Headers;
+
 import org.keycloak.adapters.saml.SamlDeployment;
 import org.keycloak.adapters.saml.SamlDeploymentContext;
 import org.keycloak.adapters.saml.SamlSessionStore;
-import org.keycloak.adapters.spi.HttpFacade;
-import org.keycloak.adapters.spi.InMemorySessionIdMapper;
-import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.*;
 import org.keycloak.adapters.undertow.ServletHttpFacade;
 import org.keycloak.adapters.undertow.UndertowHttpFacade;
 import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
 
+import io.undertow.servlet.api.DeploymentInfo;
 import javax.servlet.RequestDispatcher;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import java.io.IOException;
+import java.lang.reflect.*;
+import java.util.Map;
+import org.jboss.logging.Logger;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
 public class ServletSamlAuthMech extends AbstractSamlAuthMech {
+
+    private static final Logger LOG = Logger.getLogger(ServletSamlAuthMech.class);
+
     protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
+    protected SessionIdMapperUpdater idMapperUpdater = SessionIdMapperUpdater.DIRECT;
+
     public ServletSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) {
         super(deploymentContext, sessionManagement, errorPage);
     }
 
+    public void addTokenStoreUpdaters(DeploymentInfo deploymentInfo) {
+        deploymentInfo.addSessionListener(new IdMapperUpdaterSessionListener(idMapper));    // This takes care of HTTP sessions manipulated locally
+        SessionIdMapperUpdater updater = SessionIdMapperUpdater.EXTERNAL;
+
+        try {
+            Map<String, String> initParameters = deploymentInfo.getInitParameters();
+            String idMapperSessionUpdaterClasses = initParameters == null
+              ? null
+              : initParameters.get("keycloak.sessionIdMapperUpdater.classes");
+            if (idMapperSessionUpdaterClasses == null) {
+                return;
+            }
+
+            for (String clazz : idMapperSessionUpdaterClasses.split("\\s*,\\s*")) {
+                if (! clazz.isEmpty()) {
+                    updater = invokeAddTokenStoreUpdaterMethod(clazz, deploymentInfo, updater);
+                }
+            }
+        } finally {
+            setIdMapperUpdater(updater);
+        }
+    }
+
+    private SessionIdMapperUpdater invokeAddTokenStoreUpdaterMethod(String idMapperSessionUpdaterClass, DeploymentInfo deploymentInfo,
+      SessionIdMapperUpdater previousIdMapperUpdater) {
+        try {
+            Class<?> clazz = deploymentInfo.getClassLoader().loadClass(idMapperSessionUpdaterClass);
+            Method addTokenStoreUpdatersMethod = clazz.getMethod("addTokenStoreUpdaters", DeploymentInfo.class, SessionIdMapper.class, SessionIdMapperUpdater.class);
+            if (! Modifier.isStatic(addTokenStoreUpdatersMethod.getModifiers())
+              || ! Modifier.isPublic(addTokenStoreUpdatersMethod.getModifiers())
+              || ! SessionIdMapperUpdater.class.isAssignableFrom(addTokenStoreUpdatersMethod.getReturnType())) {
+                LOG.errorv("addTokenStoreUpdaters method in class {0} has to be public static. Ignoring class.", idMapperSessionUpdaterClass);
+                return previousIdMapperUpdater;
+            }
+
+            LOG.debugv("Initializing sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
+            return (SessionIdMapperUpdater) addTokenStoreUpdatersMethod.invoke(null, deploymentInfo, idMapper, previousIdMapperUpdater);
+        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException ex) {
+            LOG.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
+            return previousIdMapperUpdater;
+        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
+            LOG.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass);
+            return previousIdMapperUpdater;
+        }
+    }
+
     @Override
     protected SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext) {
-        return new ServletSamlSessionStore(exchange, sessionManagement, securityContext, idMapper, deployment);
+        return new ServletSamlSessionStore(exchange, sessionManagement, securityContext, idMapper, idMapperUpdater, deployment);
     }
 
     @Override
@@ -84,5 +138,11 @@ public class ServletSamlAuthMech extends AbstractSamlAuthMech {
         return null;
     }
 
+    public SessionIdMapperUpdater getIdMapperUpdater() {
+        return idMapperUpdater;
+    }
 
+    protected void setIdMapperUpdater(SessionIdMapperUpdater idMapperUpdater) {
+        this.idMapperUpdater = idMapperUpdater;
+    }
 }
diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java
index 72acda5..2bf2369 100755
--- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java
+++ b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java
@@ -24,11 +24,13 @@ import io.undertow.server.session.SessionManager;
 import io.undertow.servlet.handlers.ServletRequestContext;
 import io.undertow.servlet.spec.HttpSessionImpl;
 import org.jboss.logging.Logger;
+
 import org.keycloak.adapters.saml.SamlDeployment;
 import org.keycloak.adapters.saml.SamlSession;
 import org.keycloak.adapters.saml.SamlSessionStore;
 import org.keycloak.adapters.saml.SamlUtil;
 import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
 import org.keycloak.adapters.undertow.ChangeSessionId;
 import org.keycloak.adapters.undertow.SavedRequest;
 import org.keycloak.adapters.undertow.ServletHttpFacade;
@@ -44,6 +46,8 @@ import java.util.List;
 import java.util.Set;
 
 /**
+ * Session store manipulation methods per single HTTP exchange.
+ *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
@@ -55,17 +59,20 @@ public class ServletSamlSessionStore implements SamlSessionStore {
     private final UndertowUserSessionManagement sessionManagement;
     private final SecurityContext securityContext;
     private final SessionIdMapper idMapper;
+    private final SessionIdMapperUpdater idMapperUpdater;
     protected final SamlDeployment deployment;
 
 
     public ServletSamlSessionStore(HttpServerExchange exchange, UndertowUserSessionManagement sessionManagement,
                                    SecurityContext securityContext,
-                                   SessionIdMapper idMapper, SamlDeployment deployment) {
+                                   SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater,
+                                   SamlDeployment deployment) {
         this.exchange = exchange;
         this.sessionManagement = sessionManagement;
         this.securityContext = securityContext;
         this.idMapper = idMapper;
         this.deployment = deployment;
+        this.idMapperUpdater = idMapperUpdater;
     }
 
     @Override
@@ -97,7 +104,7 @@ public class ServletSamlSessionStore implements SamlSessionStore {
             SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
             if (samlSession != null) {
                 if (samlSession.getSessionIndex() != null) {
-                    idMapper.removeSession(session.getId());
+                    idMapperUpdater.removeSession(idMapper, session.getId());
                 }
                 session.removeAttribute(SamlSession.class.getName());
             }
@@ -113,7 +120,7 @@ public class ServletSamlSessionStore implements SamlSessionStore {
             ids.addAll(sessions);
             logoutSessionIds(ids);
             for (String id : ids) {
-                idMapper.removeSession(id);
+                idMapperUpdater.removeSession(idMapper, id);
             }
         }
 
@@ -127,7 +134,7 @@ public class ServletSamlSessionStore implements SamlSessionStore {
              String sessionId = idMapper.getSessionFromSSO(id);
              if (sessionId != null) {
                  sessionIds.add(sessionId);
-                 idMapper.removeSession(sessionId);
+                 idMapperUpdater.removeSession(idMapper, sessionId);
              }
 
         }
@@ -177,7 +184,7 @@ public class ServletSamlSessionStore implements SamlSessionStore {
         session.setAttribute(SamlSession.class.getName(), account);
         sessionManagement.login(servletRequestContext.getDeployment().getSessionManager());
         String sessionId = changeSessionId(session);
-        idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId);
+        idMapperUpdater.map(idMapper, account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId);
 
     }
 
diff --git a/adapters/saml/wildfly/wildfly-adapter/pom.xml b/adapters/saml/wildfly/wildfly-adapter/pom.xml
index 463886d..d798cab 100755
--- a/adapters/saml/wildfly/wildfly-adapter/pom.xml
+++ b/adapters/saml/wildfly/wildfly-adapter/pom.xml
@@ -67,6 +67,10 @@
             <artifactId>keycloak-jboss-adapter-core</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.infinispan</groupId>
+            <artifactId>infinispan-core</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
new file mode 100644
index 0000000..489d1d5
--- /dev/null
+++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/InfinispanSessionCacheIdMapperUpdater.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.saml.wildfly.infinispan;
+
+import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
+
+import io.undertow.servlet.api.DeploymentInfo;
+import java.util.*;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import org.infinispan.Cache;
+import org.infinispan.configuration.cache.CacheMode;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class InfinispanSessionCacheIdMapperUpdater {
+
+    private static final Logger LOG = Logger.getLogger(InfinispanSessionCacheIdMapperUpdater.class);
+
+    public static final String DEFAULT_CACHE_CONTAINER_JNDI_NAME = "java:jboss/infinispan/container/web";
+
+    private static final String DEPLOYMENT_CACHE_CONTAINER_JNDI_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheContainerJndi";
+    private static final String DEPLOYMENT_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.deploymentCacheName";
+    private static final String SSO_CACHE_NAME_PARAM_NAME = "keycloak.sessionIdMapperUpdater.infinispan.cacheName";
+
+    public static SessionIdMapperUpdater addTokenStoreUpdaters(DeploymentInfo deploymentInfo, SessionIdMapper mapper, SessionIdMapperUpdater previousIdMapperUpdater) {
+       boolean distributable = Objects.equals(
+          deploymentInfo.getSessionManagerFactory().getClass().getName(),
+          "org.wildfly.clustering.web.undertow.session.DistributableSessionManagerFactory"
+        );
+
+        if (! distributable) {
+            LOG.warnv("Deployment {0} does not use supported distributed session cache mechanism", deploymentInfo.getDeploymentName());
+            return previousIdMapperUpdater;
+        }
+
+        Map<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";
+
+        try {
+            EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
+
+            Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(ssoCacheName);
+            if (ssoCacheConfiguration == null) {
+                Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName);
+                if (cacheConfiguration == null) {
+                    LOG.debugv("Using default cache container configuration for SSO cache. lookup={0}, looked up configuration of cache={1}", cacheContainerLookup, deploymentSessionCacheName);
+                    ssoCacheConfiguration = cacheManager.getDefaultCacheConfiguration();
+                } else {
+                    LOG.debugv("Using distributed HTTP session cache configuration for SSO cache. lookup={0}, configuration taken from cache={1}", cacheContainerLookup, deploymentSessionCacheName);
+                    ssoCacheConfiguration = cacheConfiguration;
+                    cacheManager.defineConfiguration(ssoCacheName, ssoCacheConfiguration);
+                }
+            } else {
+                LOG.debugv("Using custom configuration of SSO cache. lookup={0}, cache name={1}", cacheContainerLookup, ssoCacheName);
+            }
+
+            CacheMode ssoCacheMode = ssoCacheConfiguration.clustering().cacheMode();
+            if (ssoCacheMode != CacheMode.REPL_ASYNC && ssoCacheMode != CacheMode.REPL_SYNC) {
+                LOG.warnv("SSO cache mode is {0}, it is recommended to use replicated mode instead", ssoCacheConfiguration.clustering().cacheModeString());
+            }
+
+            Cache<String, String[]> ssoCache = cacheManager.getCache(ssoCacheName, true);
+            ssoCache.addListener(new SsoSessionCacheListener(mapper));
+
+            LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, deploymentSessionCacheName);
+
+            SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater);
+            deploymentInfo.addSessionListener(updater);
+
+            return updater;
+        } catch (NamingException ex) {
+            LOG.warnv("Failed to obtain distributed session cache container, lookup={0}", cacheContainerLookup);
+            return previousIdMapperUpdater;
+        }
+    }
+}
diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoCacheSessionIdMapperUpdater.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoCacheSessionIdMapperUpdater.java
new file mode 100644
index 0000000..e2b8ba2
--- /dev/null
+++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoCacheSessionIdMapperUpdater.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.saml.wildfly.infinispan;
+
+import org.keycloak.adapters.saml.SamlSession;
+import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
+
+import io.undertow.server.HttpServerExchange;
+import io.undertow.server.session.Session;
+import io.undertow.server.session.SessionListener;
+import org.infinispan.Cache;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class SsoCacheSessionIdMapperUpdater implements SessionIdMapperUpdater, SessionListener {
+
+    private final SessionIdMapperUpdater delegate;
+    /**
+     * Cache where key is a HTTP session ID, and value is a pair (user session ID, principal name) of Strings.
+     */
+    private final Cache<String, String[]> httpSessionToSsoCache;
+
+    public SsoCacheSessionIdMapperUpdater(Cache<String, String[]> httpSessionToSsoCache, SessionIdMapperUpdater previousIdMapperUpdater) {
+        this.delegate = previousIdMapperUpdater;
+        this.httpSessionToSsoCache = httpSessionToSsoCache;
+    }
+
+    // SessionIdMapperUpdater methods
+
+    @Override
+    public void clear(SessionIdMapper idMapper) {
+        httpSessionToSsoCache.clear();
+        this.delegate.clear(idMapper);
+    }
+
+    @Override
+    public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) {
+        httpSessionToSsoCache.put(httpSessionId, new String[] {sso, principal});
+        this.delegate.map(idMapper, sso, principal, httpSessionId);
+    }
+
+    @Override
+    public void removeSession(SessionIdMapper idMapper, String httpSessionId) {
+        httpSessionToSsoCache.remove(httpSessionId);
+        this.delegate.removeSession(idMapper, httpSessionId);
+    }
+
+    // Undertow HTTP session listener methods
+
+    @Override
+    public void sessionCreated(Session session, HttpServerExchange exchange) {
+    }
+
+    @Override
+    public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) {
+    }
+
+    @Override
+    public void attributeAdded(Session session, String name, Object value) {
+    }
+
+    @Override
+    public void attributeUpdated(Session session, String name, Object newValue, Object oldValue) {
+    }
+
+    @Override
+    public void attributeRemoved(Session session, String name, Object oldValue) {
+    }
+
+    @Override
+    public void sessionIdChanged(Session session, String oldSessionId) {
+        this.httpSessionToSsoCache.remove(oldSessionId);
+        Object value = session.getAttribute(SamlSession.class.getName());
+        if (value instanceof SamlSession) {
+            SamlSession sess = (SamlSession) value;
+            httpSessionToSsoCache.put(session.getId(), new String[] {sess.getSessionIndex(), sess.getPrincipal().getSamlSubject()});
+        }
+    }
+}
diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java
new file mode 100644
index 0000000..ccd102e
--- /dev/null
+++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.saml.wildfly.infinispan;
+
+import org.keycloak.adapters.spi.SessionIdMapper;
+
+import java.util.*;
+import java.util.concurrent.*;
+import org.infinispan.notifications.Listener;
+import org.infinispan.notifications.cachelistener.annotation.*;
+import org.infinispan.notifications.cachelistener.event.*;
+import org.infinispan.notifications.cachemanagerlistener.annotation.CacheStarted;
+import org.infinispan.notifications.cachemanagerlistener.annotation.CacheStopped;
+import org.infinispan.notifications.cachemanagerlistener.event.CacheStartedEvent;
+import org.infinispan.notifications.cachemanagerlistener.event.CacheStoppedEvent;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@Listener
+public class SsoSessionCacheListener {
+
+    private static final Logger LOG = Logger.getLogger(SsoSessionCacheListener.class);
+
+    private final ConcurrentMap<String, Queue<Event>> map = new ConcurrentHashMap<>();
+
+    private final SessionIdMapper idMapper;
+
+    private ExecutorService executor = Executors.newSingleThreadExecutor();
+
+    public SsoSessionCacheListener(SessionIdMapper idMapper) {
+        this.idMapper = idMapper;
+    }
+
+    @TransactionRegistered
+    public void startTransaction(TransactionRegisteredEvent event) {
+        map.put(event.getGlobalTransaction().globalId(), new ConcurrentLinkedQueue<Event>());
+    }
+
+    @CacheStarted
+    public void cacheStarted(CacheStartedEvent event) {
+        this.executor = Executors.newSingleThreadExecutor();
+    }
+
+    @CacheStopped
+    public void cacheStopped(CacheStoppedEvent event) {
+        this.executor.shutdownNow();
+    }
+
+    @CacheEntryCreated
+    @CacheEntryRemoved
+    public void addEvent(TransactionalEvent event) {
+        if (event.isPre() == false) {
+            map.get(event.getGlobalTransaction().globalId()).add(event);
+        }
+    }
+
+    @TransactionCompleted
+    public void endTransaction(TransactionCompletedEvent event) {
+        Queue<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) {
+            switch (e.getType()) {
+                case CACHE_ENTRY_CREATED:
+                    this.executor.submit(new Runnable() {
+                        @Override public void run() {
+                            cacheEntryCreated((CacheEntryCreatedEvent) e);
+                        }
+                    });
+                    break;
+
+                case CACHE_ENTRY_REMOVED:
+                    this.executor.submit(new Runnable() {
+                        @Override public void run() {
+                            cacheEntryRemoved((CacheEntryRemovedEvent) e);
+                        }
+                    });
+                    break;
+            }
+        }
+    }
+
+    private void cacheEntryCreated(CacheEntryCreatedEvent event) {
+        if (! (event.getKey() instanceof String) || ! (event.getValue() instanceof String[])) {
+            return;
+        }
+        String httpSessionId = (String) event.getKey();
+        String[] value = (String[]) event.getValue();
+        String ssoId = value[0];
+        String principal = value[1];
+
+        LOG.tracev("cacheEntryCreated {0}:{1}", httpSessionId, ssoId);
+
+        this.idMapper.map(ssoId, principal, httpSessionId);
+    }
+
+    private void cacheEntryRemoved(CacheEntryRemovedEvent event) {
+        if (! (event.getKey() instanceof String)) {
+            return;
+        }
+
+        LOG.tracev("cacheEntryRemoved {0}", event.getKey());
+
+        this.idMapper.removeSession((String) event.getKey());
+    }
+}
diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlAuthMech.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlAuthMech.java
index bdf0606..ae4e242 100755
--- a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlAuthMech.java
+++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlAuthMech.java
@@ -37,6 +37,6 @@ public class WildflySamlAuthMech extends ServletSamlAuthMech {
 
     @Override
     protected SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext) {
-        return new WildflySamlSessionStore(exchange, sessionManagement, securityContext, idMapper, deployment);
+        return new WildflySamlSessionStore(exchange, sessionManagement, securityContext, idMapper, getIdMapperUpdater(), deployment);
     }
 }
diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlSessionStore.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlSessionStore.java
index b4c213a..5f8d717 100755
--- a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlSessionStore.java
+++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/WildflySamlSessionStore.java
@@ -23,6 +23,7 @@ import org.keycloak.adapters.saml.SamlDeployment;
 import org.keycloak.adapters.saml.SamlSession;
 import org.keycloak.adapters.saml.undertow.ServletSamlSessionStore;
 import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
 import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
 
 /**
@@ -31,8 +32,10 @@ import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
  */
 public class WildflySamlSessionStore extends ServletSamlSessionStore {
     public WildflySamlSessionStore(HttpServerExchange exchange, UndertowUserSessionManagement sessionManagement,
-                                   SecurityContext securityContext, SessionIdMapper idMapper, SamlDeployment resolvedDeployment) {
-        super(exchange, sessionManagement, securityContext, idMapper, resolvedDeployment);
+                                   SecurityContext securityContext,
+                                   SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater,
+                                   SamlDeployment resolvedDeployment) {
+        super(exchange, sessionManagement, securityContext, idMapper, idMapperUpdater, resolvedDeployment);
     }
 
     @Override
diff --git a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapper.java b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapper.java
index 3b467d7..b50e37b 100755
--- a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapper.java
+++ b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapper.java
@@ -24,15 +24,43 @@ import java.util.Set;
  * @version $Revision: 1 $
  */
 public interface SessionIdMapper {
+    /**
+     * Returns {@code true} if the mapper contains mapping for the given HTTP session ID.
+     * @param id
+     * @return
+     */
     boolean hasSession(String id);
 
+    /**
+     * Clears all mappings from this mapper.
+     */
     void clear();
 
+    /**
+     * Returns set of HTTP session IDs for the given principal.
+     * @param principal Principal
+     * @return
+     */
     Set<String> getUserSessions(String principal);
 
+    /**
+     * Returns HTTP session ID from the given user session ID.
+     * @param sso User session ID
+     * @return
+     */
     String getSessionFromSSO(String sso);
 
+    /**
+     * Establishes mapping between user session ID, principal and HTTP session ID.
+     * @param sso User session ID
+     * @param principal Principal
+     * @param session HTTP session ID
+     */
     void map(String sso, String principal, String session);
 
+    /**
+     * Removes mappings for the given HTTP session ID.
+     * @param session HTTP session ID.
+     */
     void removeSession(String session);
 }
diff --git a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapperUpdater.java b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapperUpdater.java
new file mode 100644
index 0000000..a0ea2ff
--- /dev/null
+++ b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/SessionIdMapperUpdater.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.spi;
+
+/**
+ * Classes implementing this interface represent a mechanism for updating {@link SessionIdMapper} entries.
+ * @author hmlnarik
+ */
+public interface SessionIdMapperUpdater {
+    /**
+     * {@link SessionIdMapper} entries are updated directly.
+     */
+    public static final SessionIdMapperUpdater DIRECT = new SessionIdMapperUpdater() {
+        @Override public void clear(SessionIdMapper idMapper) {
+            idMapper.clear();
+        }
+
+        @Override public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) {
+            idMapper.map(sso, principal, httpSessionId);
+        }
+
+        @Override public void removeSession(SessionIdMapper idMapper, String httpSessionId) {
+            idMapper.removeSession(httpSessionId);
+        }
+    };
+
+    /**
+     * Only HTTP session is manipulated with, {@link SessionIdMapper} entries are not updated by this updater and
+     * they have to be updated by some other means, e.g. by some listener of HTTP session changes.
+     */
+    public static final SessionIdMapperUpdater EXTERNAL = new SessionIdMapperUpdater() {
+        @Override public void clear(SessionIdMapper idMapper) { }
+
+        @Override public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) { }
+
+        @Override public void removeSession(SessionIdMapper idMapper, String httpSessionId) { }
+    };
+
+    /**
+     * Delegates to {@link SessionIdMapper#clear} method..
+     */
+    public abstract void clear(SessionIdMapper idMapper);
+
+    /**
+     * Delegates to {@link SessionIdMapper#map} method.
+     * @param idMapper Mapper
+     * @param sso User session ID
+     * @param principal Principal
+     * @param session HTTP session ID
+     */
+    public abstract void map(SessionIdMapper idMapper, String sso, String principal, String session);
+
+    /**
+     * Delegates to {@link SessionIdMapper#removeSession} method.
+     * @param idMapper Mapper
+     * @param session HTTP session ID.
+     */
+    public abstract void removeSession(SessionIdMapper idMapper, String session);
+}
diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml
index b85e56f..fd9d2e4 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml
@@ -42,6 +42,9 @@
         <module name="org.keycloak.keycloak-saml-adapter-core"/>
         <module name="org.keycloak.keycloak-common"/>
         <module name="org.apache.httpcomponents"/>
+        <module name="org.infinispan"/>
+        <module name="org.infinispan.cachestore.remote"/>
+        <module name="org.infinispan.client.hotrod"/>
     </dependencies>
 
 </module>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml
index ee00fcc..6cb3c73 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml
@@ -42,6 +42,10 @@
         <module name="org.keycloak.keycloak-saml-adapter-core"/>
         <module name="org.keycloak.keycloak-common"/>
         <module name="org.apache.httpcomponents"/>
+        <module name="org.infinispan"/>
+        <module name="org.infinispan.commons"/>
+        <module name="org.infinispan.cachestore.remote"/>
+        <module name="org.infinispan.client.hotrod"/>
     </dependencies>
 
 </module>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeServletDistributable.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeServletDistributable.java
new file mode 100644
index 0000000..ed547c0
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeServletDistributable.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.adapter.page;
+
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.test.api.ArquillianResource;
+
+import java.net.URL;
+import org.openqa.selenium.WebDriver;
+
+/**
+ * @author mhajas
+ */
+public class EmployeeServletDistributable extends SAMLServlet {
+    public static final String DEPLOYMENT_NAME = "employee-distributable";
+
+    @ArquillianResource
+    @OperateOnDeployment(DEPLOYMENT_NAME)
+    private URL url;
+
+    public EmployeeServletDistributable(WebDriver driver) {
+        super();
+        this.driver = driver;
+    }
+
+    @Override
+    public URL getInjectedUrl() {
+        return url;
+    }
+
+    public void setUrl(URL url) {
+        this.url = url;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java
index cc4a419..2f226b9 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java
@@ -28,7 +28,7 @@ public abstract class SAMLServlet extends AbstractPageWithInjectedUrl {
 
     public void logout() {
         driver.navigate().to(getUriBuilder().queryParam("GLO", "true").build().toASCIIString());
-        getUriBuilder().replaceQueryParam("GLO", null);
+        getUriBuilder().replaceQueryParam("GLO");
         pause(300);
     }
 
@@ -36,7 +36,7 @@ public abstract class SAMLServlet extends AbstractPageWithInjectedUrl {
         if (check) {
             getUriBuilder().queryParam("checkRoles", true);
         } else {
-            getUriBuilder().replaceQueryParam("checkRoles", null);
+            getUriBuilder().replaceQueryParam("checkRoles");
         }
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java
index bd6e5a0..e3950d3 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java
@@ -51,7 +51,7 @@ public class DeploymentTargetModifier extends AnnotationDeploymentScenarioGenera
 
         if (appServerQualifier != null && !appServerQualifier.isEmpty()) {
             for (DeploymentDescription deployment : deployments) {
-                if (deployment.getTarget() == null || !deployment.getTarget().getName().equals(appServerQualifier)) {
+                if (deployment.getTarget() == null || !deployment.getTarget().getName().startsWith(appServerQualifier)) {
                     log.debug("Setting target container for " + deployment.getName() + ": " + appServerQualifier);
                     deployment.setTarget(new TargetDescription(appServerQualifier));
                 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java
index cc018d2..235d15f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java
@@ -65,11 +65,11 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest {
         return deployment;
     }
 
-    protected static WebArchive samlServletDeployment(String name, Class... servletClasses) {
+    public static WebArchive samlServletDeployment(String name, Class... servletClasses) {
         return samlServletDeployment(name, "web.xml", servletClasses);
     }
 
-    protected static WebArchive samlServletDeployment(String name, String webXMLPath, Class... servletClasses) {
+    public static WebArchive samlServletDeployment(String name, String webXMLPath, Class... servletClasses) {
         String baseSAMLPath = "/adapter-test/keycloak-saml/";
         String webInfPath = baseSAMLPath + name + "/WEB-INF/";
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java
new file mode 100644
index 0000000..3868fb5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.servlet.cluster;
+
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.representations.idm.*;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.adapter.page.EmployeeServletDistributable;
+import org.keycloak.testsuite.adapter.page.SAMLServlet;
+import org.keycloak.testsuite.auth.page.AuthRealm;
+import org.keycloak.testsuite.auth.page.login.*;
+import org.keycloak.testsuite.page.AbstractPage;
+import org.keycloak.testsuite.util.WaitUtils;
+
+import io.undertow.Undertow;
+import io.undertow.server.handlers.ResponseCodeHandler;
+import io.undertow.server.handlers.proxy.LoadBalancingProxyClient;
+import io.undertow.server.handlers.proxy.ProxyHandler;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.function.Consumer;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.jboss.arquillian.container.test.api.*;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.junit.*;
+
+import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+import org.openqa.selenium.TimeoutException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.support.PageFactory;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.AbstractAuthTest.createUserRepresentation;
+import static org.keycloak.testsuite.admin.Users.setPasswordFor;
+import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getNearestSuperclassWithAnnotation;
+import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public abstract class AbstractSAMLAdapterClusterTest extends AbstractServletsAdapterTest {
+
+    protected static final String NODE_1_NAME = "ha-node-1";
+    protected static final String NODE_2_NAME = "ha-node-2";
+
+    protected final String NODE_1_SERVER_NAME = getAppServerId() + "-" + NODE_1_NAME;
+    protected final String NODE_2_SERVER_NAME = getAppServerId() + "-" + NODE_2_NAME;
+
+    protected static final int PORT_OFFSET_NODE_REVPROXY = NumberUtils.toInt(System.getProperty("app.server.reverse-proxy.port.offset"), -1);
+    protected static final int HTTP_PORT_NODE_REVPROXY = 8080 + PORT_OFFSET_NODE_REVPROXY;
+    protected static final int PORT_OFFSET_NODE_1 = NumberUtils.toInt(System.getProperty("app.server.1.port.offset"), -1);
+    protected static final int HTTP_PORT_NODE_1 = 8080 + PORT_OFFSET_NODE_1;
+    protected static final int PORT_OFFSET_NODE_2 = NumberUtils.toInt(System.getProperty("app.server.2.port.offset"), -1);
+    protected static final int HTTP_PORT_NODE_2 = 8080 + PORT_OFFSET_NODE_2;
+    protected static final URI NODE_1_URI = URI.create("http://localhost:" + HTTP_PORT_NODE_1);
+    protected static final URI NODE_2_URI = URI.create("http://localhost:" + HTTP_PORT_NODE_2);
+
+    @BeforeClass
+    public static void checkPropertiesSet() {
+        Assume.assumeThat(PORT_OFFSET_NODE_1, not(is(-1)));
+        Assume.assumeThat(PORT_OFFSET_NODE_2, not(is(-1)));
+        Assume.assumeThat(PORT_OFFSET_NODE_REVPROXY, not(is(-1)));
+    }
+
+    protected static void prepareServerDirectory(String targetSubdirectory) throws IOException {
+        Path path = Paths.get(System.getProperty("app.server.home"), targetSubdirectory);
+        File targetSubdirFile = path.toFile();
+        FileUtils.deleteDirectory(targetSubdirFile);
+        FileUtils.forceMkdir(targetSubdirFile);
+        FileUtils.copyDirectoryToDirectory(Paths.get(System.getProperty("app.server.home"), "standalone", "deployments").toFile(), targetSubdirFile);
+    }
+    protected LoadBalancingProxyClient loadBalancerToNodes;
+    protected Undertow reverseProxyToNodes;
+
+    @ArquillianResource
+    protected ContainerController controller;
+
+    @ArquillianResource
+    protected Deployer deployer;
+
+    @Page
+    LoginActions loginActionsPage;
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml-behind-lb.json"));
+    }
+
+    @Before
+    public void prepareReverseProxy() throws Exception {
+        loadBalancerToNodes = new LoadBalancingProxyClient().addHost(NODE_1_URI, NODE_1_NAME).setConnectionsPerThread(10);
+        reverseProxyToNodes = Undertow.builder().addHttpListener(HTTP_PORT_NODE_REVPROXY, "localhost").setIoThreads(2).setHandler(new ProxyHandler(loadBalancerToNodes, 5000, ResponseCodeHandler.HANDLE_404)).build();
+        reverseProxyToNodes.start();
+    }
+
+    @After
+    public void stopReverseProxy() {
+        reverseProxyToNodes.stop();
+    }
+
+    @Before
+    public void startServer() throws Exception {
+        prepareServerDirectory("standalone-" + NODE_1_NAME);
+        controller.start(NODE_1_SERVER_NAME);
+        prepareWorkerNode(Integer.valueOf(System.getProperty("app.server.1.management.port")));
+        prepareServerDirectory("standalone-" + NODE_2_NAME);
+        controller.start(NODE_2_SERVER_NAME);
+        prepareWorkerNode(Integer.valueOf(System.getProperty("app.server.2.management.port")));
+        deployer.deploy(EmployeeServletDistributable.DEPLOYMENT_NAME);
+        deployer.deploy(EmployeeServletDistributable.DEPLOYMENT_NAME + "_2");
+    }
+
+    protected abstract void prepareWorkerNode(Integer managementPort) throws Exception;
+
+    @After
+    public void stopServer() {
+        controller.stop(NODE_1_SERVER_NAME);
+        controller.stop(NODE_2_SERVER_NAME);
+    }
+
+    @Override
+    public void setDefaultPageUriParameters() {
+        super.setDefaultPageUriParameters();
+
+        testRealmSAMLPostLoginPage.setAuthRealm(DEMO);
+        loginPage.setAuthRealm(DEMO);
+        loginActionsPage.setAuthRealm(DEMO);
+    }
+
+    protected void testLogoutViaSessionIndex(URL employeeUrl, Consumer<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);
+        setPasswordFor(bburkeUser, CredentialRepresentation.PASSWORD);
+
+        assertSuccessfulLogin(page, bburkeUser, testRealmSAMLPostLoginPage, "principal=bburke");
+
+        updateProxy(NODE_2_NAME, NODE_2_URI, NODE_1_URI);
+        logoutFunction.accept(page);
+        delayedCheckLoggedOut(page, loginActionsPage);
+
+        updateProxy(NODE_1_NAME, NODE_1_URI, NODE_2_URI);
+        delayedCheckLoggedOut(page, loginActionsPage);
+    }
+
+    @Test
+    public void testBackchannelLogout(@ArquillianResource
+      @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception {
+        testLogoutViaSessionIndex(employeeUrl, (EmployeeServletDistributable page) -> {
+            RealmResource demoRealm = adminClient.realm(DEMO);
+            String bburkeId = ApiUtil.findUserByUsername(demoRealm, "bburke").getId();
+            demoRealm.users().get(bburkeId).logout();
+            log.infov("Logged out via admin console");
+        });
+    }
+
+    @Test
+    public void testFrontchannelLogout(@ArquillianResource
+      @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception {
+        testLogoutViaSessionIndex(employeeUrl, (EmployeeServletDistributable page) -> {
+            page.logout();
+            log.infov("Logged out via application");
+        });
+    }
+
+    protected void updateProxy(String hostToPointToName, URI hostToPointToUri, URI hostToRemove) {
+        loadBalancerToNodes.removeHost(hostToRemove);
+        loadBalancerToNodes.addHost(hostToPointToUri, hostToPointToName);
+        log.infov("Reverse proxy will direct requests to {0}", hostToPointToUri);
+    }
+
+    protected void assertSuccessfulLogin(SAMLServlet page, UserRepresentation user, Login loginPage, String expectedString) {
+        page.navigateTo();
+        assertCurrentUrlStartsWith(loginPage);
+        loginPage.form().login(user);
+        WebDriverWait wait = new WebDriverWait(driver, WaitUtils.PAGELOAD_TIMEOUT_MILLIS / 1000);
+        wait.until((WebDriver d) -> d.getPageSource().contains(expectedString));
+    }
+
+    protected void delayedCheckLoggedOut(AbstractPage page, AuthRealm loginPage) {
+        Retry.execute(() -> {
+            try {
+                checkLoggedOut(page, loginPage);
+            } catch (AssertionError | TimeoutException ex) {
+                driver.navigate().refresh();
+                log.debug("[Retriable] Timed out waiting for login page");
+                throw new RuntimeException(ex);
+            }
+        }, 10, 100);
+    }
+
+    protected void checkLoggedOut(AbstractPage page, AuthRealm loginPage) {
+        page.navigateTo();
+        WaitUtils.waitForPageToLoad(driver);
+        assertCurrentUrlStartsWith(loginPage);
+    }
+
+    private String getAppServerId() {
+        Class<?> annotatedClass = getNearestSuperclassWithAnnotation(this.getClass(), AppServerContainer.class);
+
+        return (annotatedClass == null ? "<cannot-find-@AppServerContainer>"
+                : annotatedClass.getAnnotation(AppServerContainer.class).value());
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/keycloak-saml.xml
new file mode 100644
index 0000000..60f48de
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/keycloak-saml.xml
@@ -0,0 +1,44 @@
+<!--
+  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter"
+                       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                       xsi:schemaLocation="urn:keycloak:saml:adapter http://www.keycloak.org/schema/keycloak_saml_adapter_1_7.xsd">
+    <SP entityID="http://localhost:8580/employee-distributable/"
+        sslPolicy="EXTERNAL"
+        nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+        logoutPage="/logout.jsp"
+        forceAuthentication="false">
+        <PrincipalNameMapping policy="FROM_NAME_ID"/>
+        <RoleIdentifiers>
+            <Attribute name="memberOf"/>
+            <Attribute name="Role"/>
+        </RoleIdentifiers>
+        <IDP entityID="idp">
+            <SingleSignOnService requestBinding="POST"
+                                 bindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+                    />
+
+            <SingleLogoutService
+                    requestBinding="POST"
+                    responseBinding="POST"
+                    postBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+                    redirectBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+                    />
+        </IDP>
+     </SP>
+</keycloak-saml-adapter>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml-behind-lb.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml-behind-lb.json
new file mode 100644
index 0000000..ae10da2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml-behind-lb.json
@@ -0,0 +1,164 @@
+{
+    "id": "demo",
+    "realm": "demo",
+    "enabled": true,
+    "sslRequired": "external",
+    "registrationAllowed": true,
+    "resetPasswordAllowed": true,
+    "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+    "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+    "requiredCredentials": [ "password" ],
+    "defaultRoles": [ "user" ],
+    "smtpServer": {
+        "from": "auto@keycloak.org",
+        "host": "localhost",
+        "port":"3025"
+    },
+    "users" : [
+        {
+            "username" : "bburke",
+            "enabled": true,
+            "email" : "bburke@redhat.com",
+            "credentials" : [
+                { "type" : "password",
+                    "value" : "password" }
+            ],
+            "attributes" : {
+                "phone": "617"
+            },
+            "realmRoles": ["manager", "user"],
+            "applicationRoles": {
+                "http://localhost:8580/employee-distributable/": [ "employee" ]
+            }
+        },
+        {
+            "username" : "unauthorized",
+            "enabled": true,
+            "email" : "unauthorized@redhat.com",
+            "credentials" : [
+                { "type" : "password",
+                    "value" : "password" }
+            ]
+        },
+        {
+            "username" : "topGroupUser",
+            "enabled": true,
+            "email" : "top@redhat.com",
+            "credentials" : [
+                { "type" : "password",
+                    "value" : "password" }
+            ],
+            "groups": [
+                "/top"
+            ]
+        },
+        {
+            "username" : "level2GroupUser",
+            "enabled": true,
+            "email" : "level2@redhat.com",
+            "credentials" : [
+                { "type" : "password",
+                    "value" : "password" }
+            ],
+            "groups": [
+                "/top/level2"
+            ]
+        }
+    ],
+    "clients": [
+        {
+            "clientId": "http://localhost:8580/employee-distributable/",
+            "enabled": true,
+            "protocol": "saml",
+            "fullScopeAllowed": true,
+            "baseUrl": "http://localhost:8580/employee-distributable",
+            "redirectUris": [
+                "http://localhost:8580/employee-distributable/*"
+            ],
+            "attributes": {
+                "saml_assertion_consumer_url_post": "http://localhost:8580/employee-distributable/saml",
+                "saml_assertion_consumer_url_redirect": "http://localhost:8580/employee-distributable/saml",
+                "saml_single_logout_service_url_post": "http://localhost:8580/employee-distributable/saml",
+                "saml_single_logout_service_url_redirect": "http://localhost:8580/employee-distributable/saml",
+                "saml.authnstatement": "true"
+            },
+            "protocolMappers": [
+                {
+                    "name": "email",
+                    "protocol": "saml",
+                    "protocolMapper": "saml-user-property-mapper",
+                    "consentRequired": false,
+                    "config": {
+                        "user.attribute": "email",
+                        "friendly.name": "email",
+                        "attribute.name": "urn:oid:1.2.840.113549.1.9.1",
+                        "attribute.nameformat": "URI Reference"
+                    }
+                },
+                {
+                    "name": "phone",
+                    "protocol": "saml",
+                    "protocolMapper": "saml-user-attribute-mapper",
+                    "consentRequired": false,
+                    "config": {
+                        "user.attribute": "phone",
+                        "attribute.name": "phone",
+                        "attribute.nameformat": "Basic"
+                    }
+                },
+                {
+                    "name": "role-list",
+                    "protocol": "saml",
+                    "protocolMapper": "saml-role-list-mapper",
+                    "consentRequired": false,
+                    "config": {
+                        "attribute.name": "Role",
+                        "attribute.nameformat": "Basic",
+                        "single": "false"
+                    }
+                }
+            ]
+        }
+    ],
+    "groups" : [
+        {
+            "name": "top",
+            "attributes": {
+                "topAttribute": ["true"]
+
+            },
+            "realmRoles": ["manager"],
+            "subGroups": [
+                {
+                    "name": "level2",
+                    "realmRoles": ["user"],
+                    "attributes": {
+                        "level2Attribute": ["true"]
+
+                    }
+                }
+            ]
+        }
+    ],
+
+    "roles" : {
+        "realm" : [
+            {
+                "name": "manager",
+                "description": "Have Manager privileges"
+            },
+            {
+                "name": "user",
+                "description": "Have User privileges"
+            }
+        ],
+        "application" : {
+            "http://localhost:8580/employee-distributable/" : [
+                {
+                    "name": "employee",
+                    "description": "Have Employee privileges"
+                }
+            ]
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl b/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl
index 74541a0..30e7dba 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl
@@ -48,6 +48,53 @@
                 </configuration>
             </container>
             
+            <container qualifier="app-server-${{app.server}}-ha-node-1" mode="manual" >
+                <configuration>
+                    <property name="enabled">true</property>
+                    <property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
+                    <property name="jbossHome">${app.server.home}</property>
+                    <property name="javaHome">${app.server.java.home}</property>
+                    <property name="cleanServerBaseDir">${app.server.home}/standalone-ha-node-1</property>
+                    <property name="serverConfig">standalone-ha.xml</property>
+                    <property name="jbossArguments">
+                        -Djboss.socket.binding.port-offset=${app.server.1.port.offset} 
+                        -Djboss.node.name=ha-node-1
+                        ${adapter.test.props}
+                    </property>
+                    <property name="javaVmArguments">
+                        -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=7901
+                        ${app.server.memory.settings}
+                        -Djava.net.preferIPv4Stack=true
+                    </property>
+                    <property name="managementProtocol">${app.server.management.protocol}</property>
+                    <property name="managementPort">${app.server.1.management.port}</property>
+                    <property name="startupTimeoutInSeconds">${app.server.startup.timeout}</property>
+                </configuration>
+            </container>            
+            
+            <container qualifier="app-server-${{app.server}}-ha-node-2" mode="manual" >
+                <configuration>
+                    <property name="enabled">true</property>
+                    <property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
+                    <property name="jbossHome">${app.server.home}</property>
+                    <property name="javaHome">${app.server.java.home}</property>
+                    <property name="cleanServerBaseDir">${app.server.home}/standalone-ha-node-2</property>
+                    <property name="serverConfig">standalone-ha.xml</property>
+                    <property name="jbossArguments">
+                        -Djboss.socket.binding.port-offset=${app.server.2.port.offset} 
+                        -Djboss.node.name=ha-node-2
+                        ${adapter.test.props}
+                    </property>
+                    <property name="javaVmArguments">
+                        -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=7902
+                        ${app.server.memory.settings}
+                        -Djava.net.preferIPv4Stack=true
+                    </property>
+                    <property name="managementProtocol">${app.server.management.protocol}</property>
+                    <property name="managementPort">${app.server.2.management.port}</property>
+                    <property name="startupTimeoutInSeconds">${app.server.startup.timeout}</property>
+                </configuration>
+            </container>
         </xsl:copy>
     </xsl:template>
 
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml
index 7c56a48..35f9501 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml
@@ -30,12 +30,29 @@
     <artifactId>integration-arquillian-tests-adapters-eap6</artifactId>
 
     <name>Adapter Tests - JBoss - EAP 6</name>
-    
+
+    <dependencies>
+        <dependency>
+            <groupId>org.wildfly.extras.creaper</groupId>
+            <artifactId>creaper-core</artifactId>
+            <scope>test</scope>
+            <version>1.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.wildfly.core</groupId>
+            <artifactId>wildfly-cli</artifactId>
+            <scope>test</scope>
+            <version>2.2.0.Final</version>
+        </dependency>
+    </dependencies>
+            
     <properties>
         <app.server>eap6</app.server>
 
         <app.server.management.protocol>remote</app.server.management.protocol>
         <app.server.management.port>${app.server.management.port.jmx}</app.server.management.port>
+        <app.server.1.management.port>${app.server.1.management.port.jmx}</app.server.1.management.port>
+        <app.server.2.management.port>${app.server.2.management.port.jmx}</app.server.2.management.port>
     </properties>
     
 </project>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/cluster/EAP6SAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/cluster/EAP6SAMLAdapterClusterTest.java
new file mode 100644
index 0000000..f0a166b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/cluster/EAP6SAMLAdapterClusterTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.cluster;
+
+import org.keycloak.testsuite.adapter.page.EmployeeServletDistributable;
+import org.keycloak.testsuite.arquillian.annotation.*;
+
+import java.io.*;
+
+import org.keycloak.testsuite.adapter.servlet.cluster.AbstractSAMLAdapterClusterTest;
+import org.keycloak.testsuite.adapter.servlet.SendUsernameServlet;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.TargetsContainer;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.wildfly.extras.creaper.core.*;
+import org.wildfly.extras.creaper.core.online.*;
+import org.wildfly.extras.creaper.core.online.operations.*;
+
+import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@AppServerContainer("app-server-eap6")
+public class EAP6SAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTest {
+
+    @TargetsContainer(value = "app-server-eap6-" + NODE_1_NAME)
+    @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME, managed = false)
+    protected static WebArchive employee() {
+        return samlServletDeployment(EmployeeServletDistributable.DEPLOYMENT_NAME, EmployeeServletDistributable.DEPLOYMENT_NAME + "/WEB-INF/web.xml", SendUsernameServlet.class);
+    }
+
+    @TargetsContainer(value = "app-server-eap6-" + NODE_2_NAME)
+    @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME + "_2", managed = false)
+    protected static WebArchive employee2() {
+        return employee();
+    }
+
+    @Override
+    protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException {
+        log.infov("Preparing worker node ({0})", managementPort);
+
+        OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions
+          .standalone()
+          .hostAndPort("localhost", managementPort)
+          .protocol(ManagementProtocol.REMOTE)
+          .build());
+        Operations op = new Operations(clientWorkerNodeClient);
+
+        Batch b = new Batch();
+        Address tcppingStack = Address
+          .subsystem("jgroups")
+          .and("stack", "tcpping");
+        b.add(tcppingStack);
+        b.add(tcppingStack.and("transport", "TRANSPORT"), Values.of("socket-binding", "jgroups-tcp").and("type", "TCP"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "TCPPING"));
+        b.add(tcppingStack.and("protocol", "TCPPING").and("property", "initial_hosts"), Values.of("value", "localhost[" + (7600 + PORT_OFFSET_NODE_1) + "],localhost[" + (7600 + PORT_OFFSET_NODE_2) + "]"));
+        b.add(tcppingStack.and("protocol", "TCPPING").and("property", "port_range"), Values.of("value", "0"));
+        b.add(tcppingStack.and("protocol", "TCPPING").and("property", "num_initial_members"), Values.of("value", "2"));
+        b.add(tcppingStack.and("protocol", "TCPPING").and("property", "timeout"), Values.of("value", "3000"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "MERGE2"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "FD_SOCK").and("socket-binding", "jgroups-tcp-fd"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "FD"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "VERIFY_SUSPECT"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "pbcast.NAKACK"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "UNICAST2"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "pbcast.STABLE"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "pbcast.GMS"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "UFC"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "MFC"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "FRAG2"));
+        b.invoke("add-protocol", tcppingStack, Values.of("type", "RSVP"));
+        Assert.assertTrue("Could not add TCPPING JGroups stack", op.batch(b).isSuccess());
+
+        Assert.assertTrue(op.writeAttribute(Address.subsystem("jgroups"), "default-stack", "tcpping").isSuccess());
+        Assert.assertTrue(op.writeAttribute(Address.subsystem("web"), "instance-id", "${jboss.node.name}").isSuccess());
+        Assert.assertTrue(op.add(Address.extension("org.keycloak.keycloak-saml-adapter-subsystem"), Values.of("module", "org.keycloak.keycloak-saml-adapter-subsystem")).isSuccess());
+        Assert.assertTrue(op.add(Address.subsystem("keycloak-saml")).isSuccess());
+
+        clientWorkerNodeClient.execute("reload");
+
+        log.infov("Worker node ({0}) Prepared", managementPort);
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml
new file mode 100644
index 0000000..767eb6c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+         version="3.0">
+
+    <distributable/>
+    
+    <absolute-ordering/>
+    
+    <module-name>%CONTEXT_PATH%</module-name>
+
+    <servlet-mapping>
+        <servlet-name>javax.ws.rs.core.Application</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+
+    <error-page>
+        <location>/error.html</location>
+    </error-page>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Application</web-resource-name>
+            <url-pattern>/*</url-pattern>
+        </web-resource-collection>
+        <auth-constraint>
+            <role-name>manager</role-name>
+        </auth-constraint>
+    </security-constraint>
+
+    <login-config>
+        <auth-method>KEYCLOAK-SAML</auth-method>
+        <realm-name>demo</realm-name>
+    </login-config>
+
+    <security-role>
+        <role-name>manager</role-name>
+    </security-role>
+
+    <context-param>
+        <param-name>keycloak.sessionIdMapperUpdater.classes</param-name>
+        <param-value>org.keycloak.adapters.saml.jbossweb.infinispan.InfinispanSessionCacheIdMapperUpdater</param-value>
+    </context-param>
+</web-app>
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 885a052..912947f 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml
@@ -30,7 +30,22 @@
     <artifactId>integration-arquillian-tests-adapters-wildfly</artifactId>
 
     <name>Adapter Tests - JBoss - Wildfly</name>
-    
+
+    <dependencies>
+        <dependency>
+            <groupId>org.wildfly.extras.creaper</groupId>
+            <artifactId>creaper-core</artifactId>
+            <scope>test</scope>
+            <version>1.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.wildfly.core</groupId>
+            <artifactId>wildfly-cli</artifactId>
+            <scope>test</scope>
+            <version>2.2.0.Final</version>
+        </dependency>
+    </dependencies>    
+
     <properties>
         <app.server>wildfly</app.server>
     </properties>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/cluster/WildflySAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/cluster/WildflySAMLAdapterClusterTest.java
new file mode 100644
index 0000000..eb7973c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/cluster/WildflySAMLAdapterClusterTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.cluster;
+
+import org.keycloak.testsuite.adapter.page.EmployeeServletDistributable;
+import org.keycloak.testsuite.arquillian.annotation.*;
+
+import java.io.*;
+
+import org.keycloak.testsuite.adapter.servlet.cluster.AbstractSAMLAdapterClusterTest;
+import org.keycloak.testsuite.adapter.servlet.SendUsernameServlet;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.TargetsContainer;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.wildfly.extras.creaper.core.*;
+import org.wildfly.extras.creaper.core.online.*;
+import org.wildfly.extras.creaper.core.online.operations.*;
+
+import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@AppServerContainer("app-server-wildfly")
+public class WildflySAMLAdapterClusterTest extends AbstractSAMLAdapterClusterTest {
+
+    @TargetsContainer(value = "app-server-wildfly-" + NODE_1_NAME)
+    @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME, managed = false)
+    protected static WebArchive employee() {
+        return samlServletDeployment(EmployeeServletDistributable.DEPLOYMENT_NAME, EmployeeServletDistributable.DEPLOYMENT_NAME + "/WEB-INF/web.xml", SendUsernameServlet.class);
+    }
+
+    @TargetsContainer(value = "app-server-wildfly-" + NODE_2_NAME)
+    @Deployment(name = EmployeeServletDistributable.DEPLOYMENT_NAME + "_2", managed = false)
+    protected static WebArchive employee2() {
+        return employee();
+    }
+
+    @Override
+    protected void prepareWorkerNode(Integer managementPort) throws IOException, CliException, NumberFormatException {
+        log.infov("Preparing worker node ({0})", managementPort);
+
+        OnlineManagementClient clientWorkerNodeClient = ManagementClient.online(OnlineOptions
+          .standalone()
+          .hostAndPort("localhost", managementPort)
+          .build());
+        Operations op = new Operations(clientWorkerNodeClient);
+
+        Batch b = new Batch();
+        Address tcppingStack = Address
+          .subsystem("jgroups")
+          .and("stack", "tcpping");
+        b.add(tcppingStack);
+        b.add(tcppingStack.and("transport", "TCP"), Values.of("socket-binding", "jgroups-tcp"));
+        b.add(tcppingStack.and("protocol", "TCPPING"));
+        b.add(tcppingStack.and("protocol", "TCPPING").and("property", "initial_hosts"), Values.of("value", "localhost[" + (7600 + PORT_OFFSET_NODE_1) + "],localhost[" + (7600 + PORT_OFFSET_NODE_2) + "]"));
+        b.add(tcppingStack.and("protocol", "TCPPING").and("property", "port_range"), Values.of("value", "0"));
+        b.add(tcppingStack.and("protocol", "TCPPING").and("property", "num_initial_members"), Values.of("value", "2"));
+        b.add(tcppingStack.and("protocol", "TCPPING").and("property", "timeout"), Values.of("value", "3000"));
+        b.add(tcppingStack.and("protocol", "MERGE3"));
+        b.add(tcppingStack.and("protocol", "FD_SOCK"), Values.of("socket-binding", "jgroups-tcp-fd"));
+        b.add(tcppingStack.and("protocol", "FD"));
+        b.add(tcppingStack.and("protocol", "VERIFY_SUSPECT"));
+        b.add(tcppingStack.and("protocol", "pbcast.NAKACK2"));
+        b.add(tcppingStack.and("protocol", "UNICAST3"));
+        b.add(tcppingStack.and("protocol", "pbcast.STABLE"));
+        b.add(tcppingStack.and("protocol", "pbcast.GMS"));
+        b.add(tcppingStack.and("protocol", "MFC"));
+        b.add(tcppingStack.and("protocol", "FRAG2"));
+        b.writeAttribute(Address.subsystem("jgroups").and("channel", "ee"), "stack", "tcpping");
+        op.batch(b);
+
+        op.add(Address.extension("org.keycloak.keycloak-saml-adapter-subsystem"), Values.of("module", "org.keycloak.keycloak-saml-adapter-subsystem"));
+        op.add(Address.subsystem("keycloak-saml"));
+
+        clientWorkerNodeClient.execute("reload");
+
+        log.infov("Worker node ({0}) Prepared", managementPort);
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml
new file mode 100644
index 0000000..b57928f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/resources/adapter-test/keycloak-saml/employee-distributable/WEB-INF/web.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+         version="3.0">
+
+    <distributable/>
+    
+    <absolute-ordering/>
+    
+    <module-name>%CONTEXT_PATH%</module-name>
+
+    <servlet-mapping>
+        <servlet-name>javax.ws.rs.core.Application</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+
+    <error-page>
+        <location>/error.html</location>
+    </error-page>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Application</web-resource-name>
+            <url-pattern>/*</url-pattern>
+        </web-resource-collection>
+        <auth-constraint>
+            <role-name>manager</role-name>
+        </auth-constraint>
+    </security-constraint>
+
+    <login-config>
+        <auth-method>KEYCLOAK-SAML</auth-method>
+        <realm-name>demo</realm-name>
+    </login-config>
+
+    <security-role>
+        <role-name>manager</role-name>
+    </security-role>
+
+    <context-param>
+        <param-name>keycloak.sessionIdMapperUpdater.classes</param-name>
+        <param-value>org.keycloak.adapters.saml.wildfly.infinispan.InfinispanSessionCacheIdMapperUpdater</param-value>
+    </context-param>
+</web-app>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
index 6b63997..624839c 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
@@ -54,6 +54,14 @@
         
         <app.server.ssl.required>false</app.server.ssl.required>
 
+        <app.server.reverse-proxy.port.offset>500</app.server.reverse-proxy.port.offset>
+        <app.server.1.port.offset>300</app.server.1.port.offset>
+        <app.server.1.management.port>10290</app.server.1.management.port>
+        <app.server.1.management.port.jmx>10299</app.server.1.management.port.jmx>
+        <app.server.2.port.offset>400</app.server.2.port.offset>
+        <app.server.2.management.port>10390</app.server.2.management.port>
+        <app.server.2.management.port.jmx>10399</app.server.2.management.port.jmx>
+
         <settings.path></settings.path>
         <repo.url></repo.url>
 
@@ -193,6 +201,14 @@
                                 <app.server.startup.timeout>${app.server.startup.timeout}</app.server.startup.timeout>
                                 <app.server.memory.settings>${app.server.memory.settings}</app.server.memory.settings>
 
+                                <app.server.reverse-proxy.port.offset>${app.server.reverse-proxy.port.offset}</app.server.reverse-proxy.port.offset>
+
+                                <app.server.1.port.offset>${app.server.1.port.offset}</app.server.1.port.offset>
+                                <app.server.1.management.port>${app.server.1.management.port}</app.server.1.management.port>
+
+                                <app.server.2.port.offset>${app.server.2.port.offset}</app.server.2.port.offset>
+                                <app.server.2.management.port>${app.server.2.management.port}</app.server.2.management.port>
+
                                 <adapter.test.props>${adapter.test.props}</adapter.test.props>
 
                                 <adapter.config.bundled>${adapter.config.bundled}</adapter.config.bundled>