keycloak-aplcache

Changes

Details

diff --git a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java
index 6d53485..fc2a1e0 100644
--- a/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java
+++ b/adapters/saml/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/saml/wildfly/infinispan/SsoSessionCacheListener.java
@@ -171,19 +171,25 @@ public class SsoSessionCacheListener {
             return;
         }
 
-        String[] value = ssoCache.get((String) httpSessionId);
+        this.executor.submit(new Runnable() {
 
-        if (value != null) {
-            String ssoId = value[0];
-            String principal = value[1];
+            @Override
+            public void run() {
+                String[] value = ssoCache.get((String) httpSessionId);
 
-            LOG.tracev("remoteCacheEntryCreated {0}:{1}", httpSessionId, ssoId);
+                if (value != null) {
+                    String ssoId = value[0];
+                    String principal = value[1];
 
-            this.idMapper.map(ssoId, principal, httpSessionId);
-        } else {
-            LOG.tracev("remoteCacheEntryCreated {0}", event.getKey());
+                    LOG.tracev("remoteCacheEntryCreated {0}:{1}", httpSessionId, ssoId);
 
-        }
+                    idMapper.map(ssoId, principal, httpSessionId);
+                } else {
+                    LOG.tracev("remoteCacheEntryCreated {0}", event.getKey());
+
+                }
+            }
+          });
     }
 
     @ClientCacheEntryRemoved
diff --git a/adapters/saml/wildfly-elytron/pom.xml b/adapters/saml/wildfly-elytron/pom.xml
index b1781f6..ecc8dbf 100755
--- a/adapters/saml/wildfly-elytron/pom.xml
+++ b/adapters/saml/wildfly-elytron/pom.xml
@@ -76,6 +76,14 @@
             <artifactId>wildfly-elytron</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.infinispan</groupId>
+            <artifactId>infinispan-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.infinispan</groupId>
+            <artifactId>infinispan-cachestore-remote</artifactId>
+        </dependency>
+        <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <scope>test</scope>
diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java
index 8b31a31..a111e1d 100644
--- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java
@@ -44,6 +44,7 @@ import org.keycloak.adapters.spi.AuthenticationError;
 import org.keycloak.adapters.spi.HttpFacade;
 import org.keycloak.adapters.spi.LogoutError;
 import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
 import org.wildfly.security.auth.callback.AnonymousAuthorizationCallback;
 import org.wildfly.security.auth.callback.AuthenticationCompleteCallback;
 import org.wildfly.security.auth.callback.SecurityIdentityCallback;
@@ -68,16 +69,16 @@ class ElytronHttpFacade implements HttpFacade {
     private boolean restored;
     private SamlSession samlSession;
 
-    public ElytronHttpFacade(HttpServerRequest request, SessionIdMapper idMapper, SamlDeploymentContext deploymentContext, CallbackHandler handler) {
+    public ElytronHttpFacade(HttpServerRequest request, SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater, SamlDeploymentContext deploymentContext, CallbackHandler handler) {
         this.request = request;
         this.deploymentContext = deploymentContext;
         this.callbackHandler = handler;
         this.responseConsumer = response -> {};
-        this.sessionStore = createTokenStore(idMapper);
+        this.sessionStore = createTokenStore(idMapper, idMapperUpdater);
     }
 
-    private SamlSessionStore createTokenStore(SessionIdMapper idMapper) {
-        return new ElytronSamlSessionStore(this, idMapper, getDeployment());
+    private SamlSessionStore createTokenStore(SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater) {
+        return new ElytronSamlSessionStore(this, idMapper, idMapperUpdater, getDeployment());
     }
 
     void authenticationComplete(SamlSession samlSession) {
diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java
index 2ce6292..ebe7376 100644
--- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java
@@ -18,13 +18,10 @@
 package org.keycloak.adapters.saml.elytron;
 
 import java.net.URI;
-import java.security.Principal;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpSession;
 
 import org.jboss.logging.Logger;
 import org.keycloak.adapters.saml.SamlDeployment;
@@ -32,6 +29,7 @@ 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.common.util.KeycloakUriBuilder;
 import org.wildfly.security.http.HttpScope;
 import org.wildfly.security.http.Scope;
@@ -45,13 +43,15 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto
     public static final String SAML_REDIRECT_URI = "SAML_REDIRECT_URI";
 
     private final SessionIdMapper idMapper;
+    private final SessionIdMapperUpdater idMapperUpdater;
     protected final SamlDeployment deployment;
     private final ElytronHttpFacade exchange;
 
 
-    public ElytronSamlSessionStore(ElytronHttpFacade exchange, SessionIdMapper idMapper, SamlDeployment deployment) {
+    public ElytronSamlSessionStore(ElytronHttpFacade exchange, SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater, SamlDeployment deployment) {
         this.exchange = exchange;
         this.idMapper = idMapper;
+        this.idMapperUpdater = idMapperUpdater;
         this.deployment = deployment;
     }
 
@@ -81,10 +81,11 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto
     public void logoutAccount() {
         HttpScope session = getSession(false);
         if (session.exists()) {
+            log.debug("Logging out - current account");
             SamlSession samlSession = (SamlSession)session.getAttachment(SamlSession.class.getName());
             if (samlSession != null) {
                 if (samlSession.getSessionIndex() != null) {
-                    idMapper.removeSession(session.getID());
+                    idMapperUpdater.removeSession(idMapper, session.getID());
                 }
                 session.setAttachment(SamlSession.class.getName(), null);
             }
@@ -96,11 +97,12 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto
     public void logoutByPrincipal(String principal) {
         Set<String> sessions = idMapper.getUserSessions(principal);
         if (sessions != null) {
+            log.debugf("Logging out - by principal: %s", sessions);
             List<String> ids = new LinkedList<>();
             ids.addAll(sessions);
             logoutSessionIds(ids);
             for (String id : ids) {
-                idMapper.removeSession(id);
+                idMapperUpdater.removeSession(idMapper, id);
             }
         }
 
@@ -109,12 +111,13 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto
     @Override
     public void logoutBySsoId(List<String> ssoIds) {
         if (ssoIds == null) return;
+        log.debugf("Logging out - by session IDs: %s", ssoIds);
         List<String> sessionIds = new LinkedList<>();
         for (String id : ssoIds) {
              String sessionId = idMapper.getSessionFromSSO(id);
              if (sessionId != null) {
                  sessionIds.add(sessionId);
-                 idMapper.removeSession(sessionId);
+                 idMapperUpdater.removeSession(idMapper, sessionId);
              }
 
         }
@@ -126,6 +129,8 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto
             HttpScope scope = exchange.getScope(Scope.SESSION, id);
 
             if (scope.exists()) {
+                log.debugf("Invalidating session %s", id);
+                scope.setAttachment(SamlSession.class.getName(), null);
                 scope.invalidate();
             }
         });
@@ -138,6 +143,13 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto
             log.debug("session was null, returning null");
             return false;
         }
+
+        if (! idMapper.hasSession(session.getID())) {
+            log.debugf("Session %s has expired on some other node", session.getID());
+            session.setAttachment(SamlSession.class.getName(), null);
+            return false;
+        }
+
         final SamlSession samlSession = (SamlSession)session.getAttachment(SamlSession.class.getName());
         if (samlSession == null) {
             log.debug("SamlSession was not in session, returning null");
@@ -154,7 +166,7 @@ public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeSto
         HttpScope session = getSession(true);
         session.setAttachment(SamlSession.class.getName(), account);
         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-elytron/src/main/java/org/keycloak/adapters/saml/elytron/IdMapperUpdaterSessionListener.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/IdMapperUpdaterSessionListener.java
new file mode 100644
index 0000000..d65d74a
--- /dev/null
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/IdMapperUpdaterSessionListener.java
@@ -0,0 +1,106 @@
+/*
+ * 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.elytron;
+
+import org.keycloak.adapters.saml.SamlSession;
+import org.keycloak.adapters.spi.SessionIdMapper;
+
+import java.util.Objects;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionListener;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class IdMapperUpdaterSessionListener implements HttpSessionListener, HttpSessionAttributeListener {
+
+    private static final Logger LOG = Logger.getLogger(IdMapperUpdaterSessionListener.class);
+
+    private final SessionIdMapper idMapper;
+
+    public IdMapperUpdaterSessionListener(SessionIdMapper idMapper) {
+        this.idMapper = idMapper;
+    }
+
+    @Override
+    public void sessionCreated(HttpSessionEvent hse) {
+        LOG.debugf("Session created");
+        HttpSession session = hse.getSession();
+        Object value = session.getAttribute(SamlSession.class.getName());
+        map(session.getId(), value);
+    }
+
+    @Override
+    public void sessionDestroyed(HttpSessionEvent hse) {
+        LOG.debugf("Session destroyed");
+        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())) {
+            LOG.debugf("Attribute added");
+            map(session.getId(), hsbe.getValue());
+        }
+    }
+
+    @Override
+    public void attributeRemoved(HttpSessionBindingEvent hsbe) {
+        HttpSession session = hsbe.getSession();
+        if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) {
+            LOG.debugf("Attribute removed");
+            unmap(session.getId(), hsbe.getValue());
+        }
+    }
+
+    @Override
+    public void attributeReplaced(HttpSessionBindingEvent hsbe) {
+        HttpSession session = hsbe.getSession();
+        if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) {
+            LOG.debugf("Attribute replaced");
+            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/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/InfinispanSessionCacheIdMapperUpdater.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/InfinispanSessionCacheIdMapperUpdater.java
new file mode 100644
index 0000000..ba9f7b2
--- /dev/null
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/InfinispanSessionCacheIdMapperUpdater.java
@@ -0,0 +1,130 @@
+/*
+ * 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.elytron.infinispan;
+
+import org.keycloak.adapters.saml.AdapterConstants;
+import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
+
+import java.util.*;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.servlet.ServletContext;
+import org.infinispan.Cache;
+import org.infinispan.configuration.cache.CacheMode;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * @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";
+
+    public static SessionIdMapperUpdater addTokenStoreUpdaters(ServletContext servletContext, SessionIdMapper mapper, SessionIdMapperUpdater previousIdMapperUpdater) {
+        String containerName = servletContext.getInitParameter(AdapterConstants.REPLICATION_CONFIG_CONTAINER_PARAM_NAME);
+        String cacheName = servletContext.getInitParameter(AdapterConstants.REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME);
+
+        // the following is based on https://github.com/jbossas/jboss-as/blob/7.2.0.Final/clustering/web-infinispan/src/main/java/org/jboss/as/clustering/web/infinispan/DistributedCacheManagerFactory.java#L116-L122
+        String contextPath = servletContext.getContextPath();
+        if (contextPath == null || contextPath.isEmpty() || "/".equals(contextPath)) {
+            contextPath = "/ROOT";
+        }
+        String deploymentSessionCacheName = contextPath;
+
+        if (containerName == null || cacheName == null) {
+            LOG.warnv("Cannot determine parameters of SSO cache for deployment {0}.", contextPath);
+
+            return previousIdMapperUpdater;
+        }
+
+        String cacheContainerLookup = DEFAULT_CACHE_CONTAINER_JNDI_NAME + "/" + containerName;
+
+        try {
+            EmbeddedCacheManager cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
+
+            Configuration ssoCacheConfiguration = cacheManager.getCacheConfiguration(cacheName);
+            if (ssoCacheConfiguration == null) {
+                Configuration cacheConfiguration = cacheManager.getCacheConfiguration(deploymentSessionCacheName);
+                if (cacheConfiguration == null) {
+                    LOG.debugv("Using default configuration for SSO cache {0}.{1}.", containerName, cacheName);
+                    ssoCacheConfiguration = cacheManager.getDefaultCacheConfiguration();
+                } else {
+                    LOG.debugv("Using distributed HTTP session cache configuration for SSO cache {0}.{1}, configuration taken from cache {2}",
+                      containerName, cacheName, deploymentSessionCacheName);
+                    ssoCacheConfiguration = cacheConfiguration;
+                    cacheManager.defineConfiguration(cacheName, ssoCacheConfiguration);
+                }
+            } else {
+                LOG.debugv("Using custom configuration of SSO cache {0}.{1}.", containerName, cacheName);
+            }
+
+            CacheMode ssoCacheMode = ssoCacheConfiguration.clustering().cacheMode();
+            if (ssoCacheMode != CacheMode.REPL_ASYNC && ssoCacheMode != CacheMode.REPL_SYNC) {
+                LOG.warnv("SSO cache mode is {0}, it is recommended to use replicated mode instead.", ssoCacheConfiguration.clustering().cacheModeString());
+            }
+
+            Cache<String, String[]> ssoCache = cacheManager.getCache(cacheName, true);
+            final SsoSessionCacheListener listener = new SsoSessionCacheListener(ssoCache, mapper);
+            ssoCache.addListener(listener);
+
+            addSsoCacheCrossDcListener(ssoCache, listener);
+
+            LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, cacheName);
+
+            SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater) {
+                @Override
+                public void close() throws Exception {
+                    ssoCache.stop();
+                }
+            };
+
+            return updater;
+        } catch (NamingException ex) {
+            LOG.warnv("Failed to obtain distributed session cache container, lookup={0}", cacheContainerLookup);
+            return previousIdMapperUpdater;
+        }
+    }
+
+    private static void addSsoCacheCrossDcListener(Cache<String, String[]> ssoCache, SsoSessionCacheListener listener) {
+        if (ssoCache.getCacheConfiguration().persistence() == null) {
+            return;
+        }
+
+        final Set<RemoteStore> stores = getRemoteStores(ssoCache);
+        if (stores == null || stores.isEmpty()) {
+            return;
+        }
+
+        LOG.infov("Listening for events on remote stores configured for cache {0}", ssoCache.getName());
+
+        for (RemoteStore store : stores) {
+            store.getRemoteCache().addClientListener(listener);
+        }
+    }
+
+    public static Set<RemoteStore> getRemoteStores(Cache ispnCache) {
+        return ispnCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class);
+    }
+}
diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoCacheSessionIdMapperUpdater.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoCacheSessionIdMapperUpdater.java
new file mode 100644
index 0000000..23cbba0
--- /dev/null
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoCacheSessionIdMapperUpdater.java
@@ -0,0 +1,67 @@
+/*
+ * 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.elytron.infinispan;
+
+import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
+
+import org.infinispan.Cache;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public abstract class SsoCacheSessionIdMapperUpdater implements SessionIdMapperUpdater, AutoCloseable {
+
+    private static final Logger LOG = Logger.getLogger(SsoCacheSessionIdMapperUpdater.class.getName());
+
+    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) {
+        LOG.debugf("Adding mapping (%s, %s, %s)", sso, principal, httpSessionId);
+
+        httpSessionToSsoCache.put(httpSessionId, new String[] {sso, principal});
+        this.delegate.map(idMapper, sso, principal, httpSessionId);
+    }
+
+    @Override
+    public void removeSession(SessionIdMapper idMapper, String httpSessionId) {
+        LOG.debugf("Removing session %s", httpSessionId);
+
+        httpSessionToSsoCache.remove(httpSessionId);
+        this.delegate.removeSession(idMapper, httpSessionId);
+    }
+}
diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoSessionCacheListener.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoSessionCacheListener.java
new file mode 100644
index 0000000..8442286
--- /dev/null
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/SsoSessionCacheListener.java
@@ -0,0 +1,212 @@
+/*
+ * 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.elytron.infinispan;
+
+import org.keycloak.adapters.spi.SessionIdMapper;
+
+import java.util.*;
+import java.util.concurrent.*;
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved;
+import org.infinispan.client.hotrod.annotation.ClientListener;
+import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent;
+import org.infinispan.notifications.Listener;
+import org.infinispan.notifications.cachelistener.annotation.*;
+import org.infinispan.notifications.cachelistener.event.*;
+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(sync = false)
+@ClientListener()
+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 final Cache<String, String[]> ssoCache;
+
+    private ExecutorService executor = Executors.newSingleThreadExecutor();
+
+    public SsoSessionCacheListener(Cache<String, String[]> ssoCache, SessionIdMapper idMapper) {
+        this.ssoCache = ssoCache;
+        this.idMapper = idMapper;
+    }
+
+    @TransactionRegistered
+    public void startTransaction(TransactionRegisteredEvent event) {
+        if (event.getGlobalTransaction() == null) {
+            return;
+        }
+
+        map.put(event.getGlobalTransaction().globalId(), new ConcurrentLinkedQueue<>());
+    }
+
+    @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.isOriginLocal()) {
+            // Local events are processed by local HTTP session listener
+            return;
+        }
+
+        if (event.isPre()) {    // only handle post events
+            return;
+        }
+
+        if (event.getGlobalTransaction() != null) {
+            map.get(event.getGlobalTransaction().globalId()).add(event);
+        } else {
+            processEvent(event);
+        }
+    }
+
+    @TransactionCompleted
+    public void endTransaction(TransactionCompletedEvent event) {
+        if (event.getGlobalTransaction() == null) {
+            return;
+        }
+
+        Queue<Event> events = map.remove(event.getGlobalTransaction().globalId());
+
+        if (events == null || ! event.isTransactionSuccessful()) {
+            return;
+        }
+
+        for (final Event e : events) {
+            processEvent(e);
+        }
+    }
+
+    private void processEvent(final Event e) {
+        switch (e.getType()) {
+            case CACHE_ENTRY_CREATED:
+                this.executor.submit(new Runnable() {
+                    @Override public void run() {
+                        cacheEntryCreated((CacheEntryCreatedEvent) e);
+                    }
+                });
+                break;
+
+            case CACHE_ENTRY_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());
+    }
+
+    @ClientCacheEntryCreated
+    public void remoteCacheEntryCreated(ClientCacheEntryCreatedEvent event) {
+        if (! (event.getKey() instanceof String)) {
+            return;
+        }
+
+        String httpSessionId = (String) event.getKey();
+
+        if (idMapper.hasSession(httpSessionId)) {
+            // Ignore local events generated by remote store
+            LOG.tracev("IGNORING remoteCacheEntryCreated {0}", httpSessionId);
+            return;
+        }
+
+        this.executor.submit(new Runnable() {
+
+            @Override
+            public void run() {
+                String[] value;
+                try {
+                    value = ssoCache.get((String) httpSessionId);
+
+                    if (value != null) {
+                        String ssoId = value[0];
+                        String principal = value[1];
+
+                        LOG.tracev("remoteCacheEntryCreated {0}:{1}", httpSessionId, ssoId);
+
+                        idMapper.map(ssoId, principal, httpSessionId);
+                    } else {
+                        LOG.tracev("remoteCacheEntryCreated {0}", event.getKey());
+
+                    }
+                } catch (Exception ex) {
+                    LOG.debugf(ex, "Cannot get remote cache entry %s", httpSessionId);
+                }
+            }
+        });
+    }
+
+    @ClientCacheEntryRemoved
+    public void remoteCacheEntryRemoved(ClientCacheEntryRemovedEvent event) {
+        LOG.tracev("remoteCacheEntryRemoved {0}", event.getKey());
+
+        this.executor.submit(new Runnable() {
+
+            @Override
+            public void run() {
+                idMapper.removeSession((String) event.getKey());
+            }
+        });
+    }
+}
diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java
index 4176287..5ece449 100644
--- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java
@@ -27,7 +27,6 @@ import javax.servlet.ServletContextEvent;
 import javax.servlet.ServletContextListener;
 
 import org.jboss.logging.Logger;
-import org.keycloak.adapters.AdapterDeploymentContext;
 import org.keycloak.adapters.saml.AdapterConstants;
 import org.keycloak.adapters.saml.DefaultSamlDeployment;
 import org.keycloak.adapters.saml.SamlConfigResolver;
@@ -35,7 +34,17 @@ import org.keycloak.adapters.saml.SamlDeployment;
 import org.keycloak.adapters.saml.SamlDeploymentContext;
 import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
 import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
+import org.keycloak.adapters.saml.elytron.infinispan.InfinispanSessionCacheIdMapperUpdater;
+import org.keycloak.adapters.spi.InMemorySessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
 import org.keycloak.saml.common.exceptions.ParsingException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Objects;
 
 /**
  * <p>A {@link ServletContextListener} that parses the keycloak adapter configuration and set the same configuration
@@ -50,8 +59,14 @@ public class KeycloakConfigurationServletListener implements ServletContextListe
 
     protected static Logger log = Logger.getLogger(KeycloakConfigurationServletListener.class);
 
-    static final String ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE = SamlDeploymentContext.class.getName();
-    static final String ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE_ELYTRON = SamlDeploymentContext.class.getName() + ".elytron";
+    public static final String ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE = SamlDeploymentContext.class.getName();
+    public static final String ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE_ELYTRON = SamlDeploymentContext.class.getName() + ".elytron";
+    public static final String ADAPTER_SESSION_ID_MAPPER_ATTRIBUTE_ELYTRON = SessionIdMapper.class.getName() + ".elytron";
+    public static final String ADAPTER_SESSION_ID_MAPPER_UPDATER_ATTRIBUTE_ELYTRON = SessionIdMapperUpdater.class.getName() + ".elytron";
+
+    private final SessionIdMapper idMapper = new InMemorySessionIdMapper();
+    private SessionIdMapperUpdater idMapperUpdater = SessionIdMapperUpdater.DIRECT;
+    private Collection<AutoCloseable> toClose = new LinkedList<>();
 
     @Override
     public void contextInitialized(ServletContextEvent sce) {
@@ -93,13 +108,23 @@ public class KeycloakConfigurationServletListener implements ServletContextListe
             }
         }
 
+        addTokenStoreUpdaters(servletContext);
+
         servletContext.setAttribute(ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE, deploymentContext);
         servletContext.setAttribute(ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE_ELYTRON, deploymentContext);
+        servletContext.setAttribute(ADAPTER_SESSION_ID_MAPPER_ATTRIBUTE_ELYTRON, idMapper);
+        servletContext.setAttribute(ADAPTER_SESSION_ID_MAPPER_UPDATER_ATTRIBUTE_ELYTRON, idMapperUpdater);
     }
 
     @Override
     public void contextDestroyed(ServletContextEvent sce) {
-
+        for (AutoCloseable c : toClose) {
+            try {
+                c.close();
+            } catch (Exception e) {
+                log.warnf(e, "Exception while destroying servlet context");
+            }
+        }
     }
 
     private static InputStream getConfigInputStream(ServletContext context) {
@@ -127,4 +152,64 @@ public class KeycloakConfigurationServletListener implements ServletContextListe
         }
         return new ByteArrayInputStream(json.getBytes());
     }
+
+    public void addTokenStoreUpdaters(ServletContext servletContext) {
+        SessionIdMapperUpdater updater = this.idMapperUpdater;
+
+        try {
+            String idMapperSessionUpdaterClasses = servletContext.getInitParameter("keycloak.sessionIdMapperUpdater.classes");
+            if (idMapperSessionUpdaterClasses == null) {
+                return;
+            }
+
+            servletContext.addListener(new IdMapperUpdaterSessionListener(idMapper));    // This takes care of HTTP sessions manipulated locally
+
+            updater = SessionIdMapperUpdater.DIRECT;
+
+            for (String clazz : idMapperSessionUpdaterClasses.split("\\s*,\\s*")) {
+                if (! clazz.isEmpty()) {
+                    if (Objects.equals("org.keycloak.adapters.saml.wildfly.infinispan.InfinispanSessionCacheIdMapperUpdater", clazz)) {
+                        clazz = InfinispanSessionCacheIdMapperUpdater.class.getName();  // exchange wildfly/undertow for elytron one
+                    }
+                    updater = invokeAddTokenStoreUpdaterMethod(clazz, servletContext, updater);
+                    if (updater instanceof AutoCloseable) {
+                        toClose.add((AutoCloseable) updater);
+                    }
+                }
+            }
+        } finally {
+            setIdMapperUpdater(updater);
+        }
+    }
+
+    private SessionIdMapperUpdater invokeAddTokenStoreUpdaterMethod(String idMapperSessionUpdaterClass, ServletContext servletContext,
+      SessionIdMapperUpdater previousIdMapperUpdater) {
+        try {
+            Class<?> clazz = servletContext.getClassLoader().loadClass(idMapperSessionUpdaterClass);
+            Method addTokenStoreUpdatersMethod = clazz.getMethod("addTokenStoreUpdaters", ServletContext.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, servletContext, 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;
+        }
+    }
+
+    public SessionIdMapperUpdater getIdMapperUpdater() {
+        return idMapperUpdater;
+    }
+
+    protected void setIdMapperUpdater(SessionIdMapperUpdater idMapperUpdater) {
+        this.idMapperUpdater = idMapperUpdater;
+    }
 }
diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java
index 4d4c830..02b63ed 100644
--- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java
@@ -31,7 +31,9 @@ import org.keycloak.adapters.saml.SamlDeploymentContext;
 import org.keycloak.adapters.spi.AuthChallenge;
 import org.keycloak.adapters.spi.AuthOutcome;
 import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
 import org.wildfly.security.http.HttpAuthenticationException;
+import org.wildfly.security.http.HttpScope;
 import org.wildfly.security.http.HttpServerAuthenticationMechanism;
 import org.wildfly.security.http.HttpServerRequest;
 import org.wildfly.security.http.Scope;
@@ -48,12 +50,14 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat
     private final CallbackHandler callbackHandler;
     private final SamlDeploymentContext deploymentContext;
     private final SessionIdMapper idMapper;
+    private final SessionIdMapperUpdater idMapperUpdater;
 
-    public KeycloakHttpServerAuthenticationMechanism(Map<String, ?> properties, CallbackHandler callbackHandler, SamlDeploymentContext deploymentContext, SessionIdMapper idMapper) {
+    public KeycloakHttpServerAuthenticationMechanism(Map<String, ?> properties, CallbackHandler callbackHandler, SamlDeploymentContext deploymentContext, SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater) {
         this.properties = properties;
         this.callbackHandler = callbackHandler;
         this.deploymentContext = deploymentContext;
         this.idMapper = idMapper;
+        this.idMapperUpdater = idMapperUpdater;
     }
 
     @Override
@@ -72,7 +76,7 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat
             return;
         }
 
-        ElytronHttpFacade httpFacade = new ElytronHttpFacade(request, idMapper, deploymentContext, callbackHandler);
+        ElytronHttpFacade httpFacade = new ElytronHttpFacade(request, getSessionIdMapper(request), getSessionIdMapperUpdater(request), deploymentContext, callbackHandler);
         SamlDeployment deployment = httpFacade.getDeployment();
 
         if (!deployment.isConfigured()) {
@@ -138,6 +142,18 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat
         return this.deploymentContext;
     }
 
+    private SessionIdMapper getSessionIdMapper(HttpServerRequest request) {
+        HttpScope scope = request.getScope(Scope.APPLICATION);
+        SessionIdMapper res = scope == null ? null : (SessionIdMapper) scope.getAttachment(KeycloakConfigurationServletListener.ADAPTER_SESSION_ID_MAPPER_ATTRIBUTE_ELYTRON);
+        return res == null ? this.idMapper : res;
+    }
+
+    private SessionIdMapperUpdater getSessionIdMapperUpdater(HttpServerRequest request) {
+        HttpScope scope = request.getScope(Scope.APPLICATION);
+        SessionIdMapperUpdater res = scope == null ? null : (SessionIdMapperUpdater) scope.getAttachment(KeycloakConfigurationServletListener.ADAPTER_SESSION_ID_MAPPER_UPDATER_ATTRIBUTE_ELYTRON);
+        return res == null ? this.idMapperUpdater : res;
+    }
+
     protected void redirectLogout(SamlDeployment deployment, ElytronHttpFacade exchange) {
         sendRedirect(exchange, deployment.getLogoutPage());
     }
diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java
index c1b69a4..031a425 100644
--- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java
@@ -25,6 +25,8 @@ import javax.security.auth.callback.CallbackHandler;
 import org.keycloak.adapters.saml.SamlDeploymentContext;
 import org.keycloak.adapters.spi.InMemorySessionIdMapper;
 import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapperUpdater;
+import org.jboss.logging.Logger;
 import org.wildfly.security.http.HttpAuthenticationException;
 import org.wildfly.security.http.HttpServerAuthenticationMechanism;
 import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory;
@@ -34,7 +36,7 @@ import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory;
  */
 public class KeycloakHttpServerAuthenticationMechanismFactory implements HttpServerAuthenticationMechanismFactory {
 
-    private SessionIdMapper idMapper = new InMemorySessionIdMapper();
+    private final SessionIdMapper idMapper = new InMemorySessionIdMapper();
     private final SamlDeploymentContext deploymentContext;
 
     /**
@@ -62,7 +64,8 @@ public class KeycloakHttpServerAuthenticationMechanismFactory implements HttpSer
         mechanismProperties.putAll(properties);
 
         if (KeycloakHttpServerAuthenticationMechanism.NAME.equals(mechanismName)) {
-            return new KeycloakHttpServerAuthenticationMechanism(properties, callbackHandler, this.deploymentContext, idMapper);
+            KeycloakHttpServerAuthenticationMechanism mech = new KeycloakHttpServerAuthenticationMechanism(properties, callbackHandler, this.deploymentContext, idMapper, SessionIdMapperUpdater.DIRECT);
+            return mech;
         }
 
         return null;
diff --git a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java
index 7ca8af6..dfbe846 100755
--- a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java
+++ b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/InMemorySessionIdMapper.java
@@ -21,6 +21,7 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import org.jboss.logging.Logger;
 
 /**
  * Maps external principal and SSO id to internal local http session id
@@ -29,6 +30,9 @@ import java.util.concurrent.ConcurrentHashMap;
  * @version $Revision: 1 $
  */
 public class InMemorySessionIdMapper implements SessionIdMapper {
+
+    private static final Logger LOG = Logger.getLogger(InMemorySessionIdMapper.class.getName());
+
     ConcurrentHashMap<String, String> ssoToSession = new ConcurrentHashMap<>();
     ConcurrentHashMap<String, String> sessionToSso = new ConcurrentHashMap<>();
     ConcurrentHashMap<String, Set<String>> principalToSession = new ConcurrentHashMap<>();
@@ -63,6 +67,8 @@ public class InMemorySessionIdMapper implements SessionIdMapper {
 
     @Override
     public void map(String sso, String principal, String session) {
+        LOG.debugf("Adding mapping (%s, %s, %s)", sso, principal, session);
+
         if (sso != null) {
             ssoToSession.put(sso, session);
             sessionToSso.put(session, sso);
@@ -86,6 +92,8 @@ public class InMemorySessionIdMapper implements SessionIdMapper {
 
     @Override
     public void removeSession(String session) {
+        LOG.debugf("Removing session %s", session);
+
         String sso = sessionToSso.remove(session);
         if (sso != null) {
             ssoToSession.remove(sso);
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/add-adapter-log-level.cli b/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/add-adapter-log-level.cli
index 01e7ad9..377ae72 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/add-adapter-log-level.cli
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/add-adapter-log-level.cli
@@ -1,4 +1,5 @@
 embed-server --server-config=${server.config:standalone.xml}
 
 /subsystem=logging/logger=org.keycloak.adapters:add(level=DEBUG)
+/subsystem=logging/logger=org.keycloak.subsystem.adapter:add(level=DEBUG)
 /subsystem=logging/console-handler=CONSOLE:change-log-level(level=DEBUG)
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/configure-crossdc-config.cli b/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/configure-crossdc-config.cli
index 223e419..4e39dfa 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/configure-crossdc-config.cli
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/common/cli/configure-crossdc-config.cli
@@ -25,4 +25,16 @@ embed-server --server-config=standalone-ha.xml
 /subsystem=infinispan/cache-container=web/replicated-cache=employee-distributable-cache/store=remote:add(remote-servers=[cache-server],cache=employee-distributable-cache,passivation=false,purge=false,preload=false,shared=true)
 
 /subsystem=infinispan/cache-container=web/replicated-cache=employee-distributable-cache.ssoCache:add(statistics-enabled=true,mode=SYNC)
-/subsystem=infinispan/cache-container=web/replicated-cache=employee-distributable-cache.ssoCache/store=remote:add(remote-servers=[cache-server],cache=employee-distributable-cache.ssoCache,passivation=false,purge=false,preload=false,shared=true)
+/subsystem=infinispan/cache-container=web/replicated-cache=employee-distributable-cache.ssoCache/store=remote:add( \
+    remote-servers=["cache-server"], \
+    cache=employee-distributable-cache.ssoCache, \
+    passivation=false, \
+    purge=false, \
+    preload=false, \
+    shared=true, \
+    fetch-state=false, \
+    properties={ \
+        rawValues=true, \
+        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion:2.6} \
+    } \
+)
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CrossDCTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CrossDCTestEnricher.java
index a098af1..68971e9 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CrossDCTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CrossDCTestEnricher.java
@@ -76,7 +76,7 @@ public class CrossDCTestEnricher {
         if (annotation == null) {
             Class<?> annotatedClass = getNearestSuperclassWithAnnotation(event.getTestClass().getJavaClass(), InitialDcState.class);
 
-            annotation = annotatedClass.getAnnotation(InitialDcState.class);
+            annotation = annotatedClass == null ? null : annotatedClass.getAnnotation(InitialDcState.class);
         }
 
         if (annotation == null) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java
index 576eadf..94e9919 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java
@@ -120,13 +120,16 @@ public abstract class AbstractSAMLAdapterClusteredTest extends AbstractServletsA
         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)));
-        assumeNotElytronAdapter();
     }
 
     @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();
+        int maxTime = 3600000; // 1 hour for proxy request timeout, so we can debug the backend keycloak servers
+        reverseProxyToNodes = Undertow.builder()
+          .addHttpListener(HTTP_PORT_NODE_REVPROXY, "localhost")
+          .setIoThreads(2)
+          .setHandler(new ProxyHandler(loadBalancerToNodes, maxTime, ResponseCodeHandler.HANDLE_404)).build();
         reverseProxyToNodes.start();
     }
 
@@ -232,20 +235,6 @@ public abstract class AbstractSAMLAdapterClusteredTest extends AbstractServletsA
         log.infov("Logged out via admin console");
     }
     
-    private static void assumeNotElytronAdapter() {
-        if (!AppServerTestEnricher.isUndertowAppServer()) {
-            try {
-                boolean contains = FileUtils.readFileToString(Paths.get(System.getProperty("app.server.home"), "standalone", "configuration", "standalone.xml").toFile(), "UTF-8").contains("<security-domain name=\"KeycloakDomain\"");
-                if (contains) {
-                    Logger.getLogger(AbstractSAMLAdapterClusteredTest.class).debug("Elytron adapter installed: skipping");
-                }
-                Assume.assumeFalse(contains);
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
-        }
-    }
-
     @Test
     public void testAdminInitiatedBackchannelLogout(@ArquillianResource
       @OperateOnDeployment(value = EmployeeServletDistributable.DEPLOYMENT_NAME) URL employeeUrl) throws Exception {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/crossdc/SAMLAdapterCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/crossdc/SAMLAdapterCrossDCTest.java
index affe466..5466922 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/crossdc/SAMLAdapterCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/crossdc/SAMLAdapterCrossDCTest.java
@@ -26,8 +26,10 @@ import org.keycloak.testsuite.adapter.page.EmployeeServletDistributable;
 import org.keycloak.testsuite.adapter.AbstractSAMLAdapterClusteredTest;
 import org.keycloak.testsuite.adapter.servlet.SendUsernameServlet;
 import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+import org.keycloak.testsuite.arquillian.annotation.InitialDcState;
 import org.keycloak.testsuite.arquillian.containers.ContainerConstants;
 
+import org.keycloak.testsuite.crossdc.ServerSetup;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
 import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment;
@@ -39,6 +41,7 @@ import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlSer
 @AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_CLUSTER)
 @AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED_CLUSTER)
 @AppServerContainer(ContainerConstants.APP_SERVER_EAP_CLUSTER)
+@InitialDcState(authServers = ServerSetup.FIRST_NODE_IN_EVERY_DC, cacheServers = ServerSetup.FIRST_NODE_IN_EVERY_DC)
 public class SAMLAdapterCrossDCTest extends AbstractSAMLAdapterClusteredTest {
 
     @BeforeClass