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