KEYCLOAK-7421 Support SAML cluster logout for Elytron SAML adapter
This commit is contained in:
parent
cd96d6cc35
commit
7703d81389
17 changed files with 706 additions and 47 deletions
|
@ -75,6 +75,14 @@
|
|||
<groupId>org.wildfly.security</groupId>
|
||||
<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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -171,6 +171,10 @@ public class SsoSessionCacheListener {
|
|||
return;
|
||||
}
|
||||
|
||||
this.executor.submit(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
String[] value = ssoCache.get((String) httpSessionId);
|
||||
|
||||
if (value != null) {
|
||||
|
@ -179,12 +183,14 @@ public class SsoSessionCacheListener {
|
|||
|
||||
LOG.tracev("remoteCacheEntryCreated {0}:{1}", httpSessionId, ssoId);
|
||||
|
||||
this.idMapper.map(ssoId, principal, httpSessionId);
|
||||
idMapper.map(ssoId, principal, httpSessionId);
|
||||
} else {
|
||||
LOG.tracev("remoteCacheEntryCreated {0}", event.getKey());
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ClientCacheEntryRemoved
|
||||
public void remoteCacheEntryRemoved(ClientCacheEntryRemovedEvent event) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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} \
|
||||
} \
|
||||
)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue