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 0000000000..d65d74a308
--- /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/KeycloakConfigurationServletListener.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java
index 41762870aa..5ece449657 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;
/**
* 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 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 4d4c830abd..02b63edb33 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 properties, CallbackHandler callbackHandler, SamlDeploymentContext deploymentContext, SessionIdMapper idMapper) {
+ public KeycloakHttpServerAuthenticationMechanism(Map 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 c1b69a4435..031a425f5a 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/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 0000000000..ba9f7b231d
--- /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 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 ssoCache, SsoSessionCacheListener listener) {
+ if (ssoCache.getCacheConfiguration().persistence() == null) {
+ return;
+ }
+
+ final Set 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 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 0000000000..23cbba0b58
--- /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 httpSessionToSsoCache;
+
+ public SsoCacheSessionIdMapperUpdater(Cache httpSessionToSsoCache, SessionIdMapperUpdater previousIdMapperUpdater) {
+ this.delegate = previousIdMapperUpdater;
+ this.httpSessionToSsoCache = httpSessionToSsoCache;
+ }
+
+ // SessionIdMapperUpdater methods
+
+ @Override
+ public void clear(SessionIdMapper idMapper) {
+ httpSessionToSsoCache.clear();
+ this.delegate.clear(idMapper);
+ }
+
+ @Override
+ public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) {
+ 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 0000000000..84422860a4
--- /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> map = new ConcurrentHashMap<>();
+
+ private final SessionIdMapper idMapper;
+
+ private final Cache ssoCache;
+
+ private ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ public SsoSessionCacheListener(Cache 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 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/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 6d53485aa5..fc2a1e013a 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/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 7ca8af6e70..dfbe846210 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 ssoToSession = new ConcurrentHashMap<>();
ConcurrentHashMap sessionToSso = new ConcurrentHashMap<>();
ConcurrentHashMap> 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 01e7ad958d..377ae72154 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 223e4190b5..4e39dfaabe 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 a098af1fe4..68971e9aba 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 576eadfcdb..94e99192e9 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("