keycloak-uncached

KEYCLOAK-4288 AS 7 / EAP 6

2/19/2017 6:02:34 PM

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 55348b4..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;
@@ -95,7 +99,7 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
             if (samlSession != null) {
                 if (samlSession.getSessionIndex() != null) {
                     ids.add(session.getId());
-                    idMapper.removeSession(session.getId());
+                    idMapperUpdater.removeSession(idMapper, session.getId());
                 }
                 session.removeAttribute(SamlSession.class.getName());
             }
@@ -114,7 +118,7 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
             ids.addAll(sessions);
             logoutSessionIds(ids);
             for (String id : ids) {
-                idMapper.removeSession(id);
+                idMapperUpdater.removeSession(idMapper, id);
             }
         }
 
@@ -128,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);
              }
 
         }
@@ -144,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;
@@ -196,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/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>