keycloak-memoizeit

Changes

Details

diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronAccount.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronAccount.java
index d11f1a9..4e374f5 100644
--- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronAccount.java
+++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronAccount.java
@@ -24,9 +24,8 @@ import org.keycloak.adapters.AdapterTokenStore;
 import org.keycloak.adapters.KeycloakDeployment;
 import org.keycloak.adapters.OidcKeycloakAccount;
 import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
-import org.wildfly.security.auth.server.SecurityIdentity;
 
-import javax.security.auth.callback.CallbackHandler;
+import java.io.Serializable;
 import java.security.Principal;
 import java.util.HashSet;
 import java.util.Set;
@@ -34,8 +33,9 @@ import java.util.Set;
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
  */
-public class ElytronAccount implements OidcKeycloakAccount {
+public class ElytronAccount implements Serializable, OidcKeycloakAccount {
 
+    private static final long serialVersionUID = -6775274346765339292L;
     protected static Logger log = Logger.getLogger(ElytronAccount.class);
 
     private final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal;
diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronCookieTokenStore.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronCookieTokenStore.java
index f5ecd88..86b6539 100644
--- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronCookieTokenStore.java
+++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronCookieTokenStore.java
@@ -26,15 +26,17 @@ import org.keycloak.adapters.KeycloakDeployment;
 import org.keycloak.adapters.OidcKeycloakAccount;
 import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
 import org.keycloak.adapters.RequestAuthenticator;
+import org.keycloak.adapters.spi.UserSessionManagement;
 import org.wildfly.security.http.HttpScope;
 import org.wildfly.security.http.Scope;
 
 import javax.security.auth.callback.CallbackHandler;
+import java.util.List;
 
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
  */
-public class ElytronCookieTokenStore implements ElytronTokeStore {
+public class ElytronCookieTokenStore implements ElytronTokeStore, UserSessionManagement {
 
     protected static Logger log = Logger.getLogger(ElytronCookieTokenStore.class);
 
@@ -158,4 +160,14 @@ public class ElytronCookieTokenStore implements ElytronTokeStore {
             }
         }
     }
+
+    @Override
+    public void logoutAll() {
+        //no-op
+    }
+
+    @Override
+    public void logoutHttpSessions(List<String> ids) {
+        //no-op
+    }
 }
diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronSessionTokenStore.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronSessionTokenStore.java
index 1a81637..e1ec274 100644
--- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronSessionTokenStore.java
+++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronSessionTokenStore.java
@@ -18,19 +18,27 @@
 
 package org.keycloak.adapters.elytron;
 
-import java.util.function.Consumer;
+import static org.keycloak.adapters.elytron.ElytronHttpFacade.UNDERTOW_EXCHANGE;
 
 import javax.security.auth.callback.CallbackHandler;
-
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import io.undertow.server.HttpServerExchange;
+import io.undertow.server.session.Session;
+import io.undertow.server.session.SessionConfig;
+import io.undertow.server.session.SessionManager;
+import io.undertow.servlet.handlers.ServletRequestContext;
 import org.jboss.logging.Logger;
 import org.keycloak.KeycloakPrincipal;
 import org.keycloak.KeycloakSecurityContext;
-import org.keycloak.adapters.AdapterTokenStore;
 import org.keycloak.adapters.AdapterUtils;
 import org.keycloak.adapters.KeycloakDeployment;
 import org.keycloak.adapters.OidcKeycloakAccount;
 import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
 import org.keycloak.adapters.RequestAuthenticator;
+import org.keycloak.adapters.spi.UserSessionManagement;
 import org.wildfly.security.http.HttpScope;
 import org.wildfly.security.http.HttpScopeNotification;
 import org.wildfly.security.http.Scope;
@@ -38,7 +46,7 @@ import org.wildfly.security.http.Scope;
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
  */
-public class ElytronSessionTokenStore implements ElytronTokeStore {
+public class ElytronSessionTokenStore implements ElytronTokeStore, UserSessionManagement {
 
     private static Logger log = Logger.getLogger(ElytronSessionTokenStore.class);
 
@@ -131,17 +139,21 @@ public class ElytronSessionTokenStore implements ElytronTokeStore {
 
         if (!session.exists()) {
             session.create();
+            session.registerForNotification(httpScopeNotification -> {
+                if (!httpScopeNotification.isOfType(HttpScopeNotification.SessionNotificationType.UNDEPLOY)) {
+                    HttpScope invalidated = httpScopeNotification.getScope(Scope.SESSION);
+
+                    if (invalidated != null) {
+                        invalidated.setAttachment(ElytronAccount.class.getName(), null);
+                        invalidated.setAttachment(KeycloakSecurityContext.class.getName(), null);
+                    }
+                }
+            });
         }
 
         session.setAttachment(ElytronAccount.class.getName(), account);
         session.setAttachment(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
 
-        session.registerForNotification(httpScopeNotification -> {
-            if (!httpScopeNotification.isOfType(HttpScopeNotification.SessionNotificationType.UNDEPLOY)) {
-                logout();
-            }
-        });
-
         HttpScope scope = this.httpFacade.getScope(Scope.EXCHANGE);
 
         scope.setAttachment(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
@@ -176,27 +188,72 @@ public class ElytronSessionTokenStore implements ElytronTokeStore {
             return;
         }
 
-        try {
-            if (glo) {
-                KeycloakSecurityContext ksc = (KeycloakSecurityContext) session.getAttachment(KeycloakSecurityContext.class.getName());
-
-                if (ksc == null) {
-                    return;
-                }
+        KeycloakSecurityContext ksc = (KeycloakSecurityContext) session.getAttachment(KeycloakSecurityContext.class.getName());
 
+        try {
+            if (glo && ksc != null) {
                 KeycloakDeployment deployment = httpFacade.getDeployment();
 
+                session.invalidate();
+
                 if (!deployment.isBearerOnly() && ksc != null && ksc instanceof RefreshableKeycloakSecurityContext) {
                     ((RefreshableKeycloakSecurityContext) ksc).logout(deployment);
                 }
+            } else {
+                session.setAttachment(ElytronAccount.class.getName(), null);
+                session.setAttachment(KeycloakSecurityContext.class.getName(), null);
             }
-
-            session.setAttachment(KeycloakSecurityContext.class.getName(), null);
-            session.setAttachment(ElytronAccount.class.getName(), null);
-            session.invalidate();
         } catch (IllegalStateException ise) {
             // Session may be already logged-out in case that app has adminUrl
             log.debugf("Session %s logged-out already", session.getID());
         }
     }
+
+    @Override
+    public void logoutAll() {
+        Collection<String> sessions = httpFacade.getScopeIds(Scope.SESSION);
+        logoutHttpSessions(new ArrayList<>(sessions));
+    }
+
+    @Override
+    public void logoutHttpSessions(List<String> ids) {
+        HttpServerExchange exchange = ProtectedHttpServerExchange.class.cast(httpFacade.getScope(Scope.EXCHANGE).getAttachment(UNDERTOW_EXCHANGE)).getExchange();
+        ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
+        SessionManager sessionManager = servletRequestContext.getDeployment().getSessionManager();
+
+        for (String id : ids) {
+            // TODO: Workaround for WFLY-3345. Remove this once we fix KEYCLOAK-733. Same applies to legacy wildfly adapter.
+            Session session = sessionManager.getSession(null, new SessionConfig() {
+
+                @Override
+                public void setSessionId(HttpServerExchange exchange, String sessionId) {
+                }
+
+                @Override
+                public void clearSession(HttpServerExchange exchange, String sessionId) {
+                }
+
+                @Override
+                public String findSessionId(HttpServerExchange exchange) {
+                    return id;
+                }
+
+                @Override
+                public SessionCookieSource sessionCookieSource(HttpServerExchange exchange) {
+                    return null;
+                }
+
+                @Override
+                public String rewriteUrl(String originalUrl, String sessionId) {
+                    return null;
+                }
+
+            });
+
+            if (session != null) {
+                session.invalidate(exchange);
+            }
+        }
+
+    }
 }
diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java
index 6be7607..9250f33 100644
--- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java
+++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java
@@ -18,9 +18,6 @@
 
 package org.keycloak.adapters.elytron;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
 import java.util.Map;
 
 import javax.security.auth.callback.CallbackHandler;
@@ -36,7 +33,6 @@ import org.keycloak.adapters.spi.AuthChallenge;
 import org.keycloak.adapters.spi.AuthOutcome;
 import org.keycloak.adapters.spi.UserSessionManagement;
 import org.wildfly.security.http.HttpAuthenticationException;
-import org.wildfly.security.http.HttpScope;
 import org.wildfly.security.http.HttpServerAuthenticationMechanism;
 import org.wildfly.security.http.HttpServerRequest;
 import org.wildfly.security.http.Scope;
@@ -137,25 +133,7 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat
 
         nodesRegistrationManagement.tryRegister(httpFacade.getDeployment());
 
-        PreAuthActionsHandler preActions = new PreAuthActionsHandler(new UserSessionManagement() {
-            @Override
-            public void logoutAll() {
-                Collection<String> sessions = httpFacade.getScopeIds(Scope.SESSION);
-                logoutHttpSessions(new ArrayList<>(sessions));
-            }
-
-            @Override
-            public void logoutHttpSessions(List<String> ids) {
-                for (String id : ids) {
-                    HttpScope session = httpFacade.getScope(Scope.SESSION, id);
-
-                    if (session != null) {
-                        session.invalidate();
-                    }
-                }
-
-            }
-        }, deploymentContext, httpFacade);
+        PreAuthActionsHandler preActions = new PreAuthActionsHandler(UserSessionManagement.class.cast(httpFacade.getTokenStore()), deploymentContext, httpFacade);
 
         return preActions.handleRequest();
     }
diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SessionServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SessionServlet.java
index 7886872..7fa5d49 100644
--- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SessionServlet.java
+++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SessionServlet.java
@@ -34,6 +34,10 @@ public class SessionServlet extends HttpServlet {
 
     @Override
     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        if (req.getRequestURI().endsWith("/logout")) {
+            req.logout();
+            return;
+        }
         String counter = increaseAndGetCounter(req);
 
         resp.setContentType("text/html");
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SessionPortalDistributable.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SessionPortalDistributable.java
new file mode 100644
index 0000000..8662530
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SessionPortalDistributable.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018 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 java.net.URL;
+
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class SessionPortalDistributable extends AbstractPageWithInjectedUrl {
+
+    public static final String DEPLOYMENT_NAME = "session-portal-distributable";
+
+    @ArquillianResource
+    @OperateOnDeployment(DEPLOYMENT_NAME)
+    private URL url;
+
+    @Override
+    public URL getInjectedUrl() {
+        return url;
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractAdapterClusteredTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractAdapterClusteredTest.java
new file mode 100644
index 0000000..61827f4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractAdapterClusteredTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2018 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;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.APP_SERVER_CURRENT;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+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 org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.jboss.arquillian.container.test.api.ContainerController;
+import org.jboss.arquillian.container.test.api.Deployer;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.keycloak.testsuite.arquillian.ContainerInfo;
+import org.keycloak.testsuite.auth.page.login.LoginActions;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public abstract class AbstractAdapterClusteredTest extends AbstractServletsAdapterTest {
+
+    protected static final String NODE_1_NAME = "ha-node-1";
+    protected static final String NODE_2_NAME = "ha-node-2";
+
+    // target containers will be replaced in runtime in DeploymentTargetModifier by real app-server
+    public static final String TARGET_CONTAINER_NODE_1 = APP_SERVER_CURRENT + NODE_1_NAME;
+    public static final String TARGET_CONTAINER_NODE_2 = APP_SERVER_CURRENT + 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);
+
+    protected LoadBalancingProxyClient loadBalancerToNodes;
+    protected Undertow reverseProxyToNodes;
+
+    @ArquillianResource
+    protected ContainerController controller;
+
+    @ArquillianResource
+    protected Deployer deployer;
+
+    @Page
+    LoginActions loginActionsPage;
+
+    @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)));
+    }
+
+    @Before
+    public void prepareReverseProxy() throws Exception {
+        loadBalancerToNodes = new LoadBalancingProxyClient().addHost(NODE_1_URI, NODE_1_NAME).setConnectionsPerThread(10);
+        int maxTime = 3600000; // 1 hour for proxy request timeout, so we can debug the backend keycloak servers
+        reverseProxyToNodes = Undertow.builder()
+          .addHttpListener(HTTP_PORT_NODE_REVPROXY, "localhost")
+          .setIoThreads(2)
+          .setHandler(new ProxyHandler(loadBalancerToNodes, maxTime, ResponseCodeHandler.HANDLE_404)).build();
+        reverseProxyToNodes.start();
+    }
+
+    @Before
+    public void startServers() throws Exception {
+        prepareServerDirectories();
+        
+        for (ContainerInfo containerInfo : testContext.getAppServerBackendsInfo()) {
+            controller.start(containerInfo.getQualifier());
+        }
+        deploy();
+    }
+
+    protected abstract void deploy();
+
+    protected void prepareServerDirectories() throws Exception {
+        prepareServerDirectory("standalone-cluster", "standalone-" + NODE_1_NAME);
+        prepareServerDirectory("standalone-cluster", "standalone-" + NODE_2_NAME);
+    }
+
+    protected void prepareServerDirectory(String baseDir, String targetSubdirectory) throws IOException {
+        Path path = Paths.get(System.getProperty("app.server.home"), targetSubdirectory);
+        File targetSubdirFile = path.toFile();
+        FileUtils.deleteDirectory(targetSubdirFile);
+        FileUtils.forceMkdir(targetSubdirFile);
+        //workaround for WFARQ-44
+        FileUtils.copyDirectory(Paths.get(System.getProperty("app.server.home"), baseDir, "deployments").toFile(), new File(targetSubdirFile, "deployments"));
+        FileUtils.copyDirectory(Paths.get(System.getProperty("app.server.home"), baseDir, "configuration").toFile(), new File(targetSubdirFile, "configuration"));
+    }
+
+    @After
+    public void stopReverseProxy() {
+        reverseProxyToNodes.stop();
+    }
+
+    @After
+    public void stopServers() {
+        undeploy();
+        for (ContainerInfo containerInfo : testContext.getAppServerBackendsInfo()) {
+            controller.stop(containerInfo.getQualifier());
+        }
+    }
+
+    protected abstract void undeploy();
+
+    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 String getProxiedUrl(URL url) {
+        try {
+            return new URL(url.getProtocol(), url.getHost(), HTTP_PORT_NODE_REVPROXY, url.getFile()).toString();
+        } catch (MalformedURLException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java
index 94e9919..bde6152 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractSAMLAdapterClusteredTest.java
@@ -17,89 +17,34 @@
 package org.keycloak.testsuite.adapter;
 
 import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertThat;
 import static org.keycloak.testsuite.admin.Users.setPasswordFor;
-import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.APP_SERVER_CURRENT;
 import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
 import static org.keycloak.testsuite.utils.io.IOUtil.loadRealm;
-import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
 
-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.MalformedURLException;
-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.BiConsumer;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang3.math.NumberUtils;
 import org.apache.http.client.methods.HttpGet;
 import org.jboss.arquillian.container.test.api.*;
-import org.jboss.arquillian.graphene.page.Page;
 import org.jboss.arquillian.test.api.ArquillianResource;
-import org.jboss.logging.Logger;
 import org.junit.*;
 import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.representations.idm.*;
 import org.keycloak.common.util.Retry;
 import org.keycloak.testsuite.adapter.page.EmployeeServletDistributable;
-import org.keycloak.testsuite.adapter.page.SAMLServlet;
 import org.keycloak.testsuite.admin.ApiUtil;
-import org.keycloak.testsuite.arquillian.AppServerTestEnricher;
-import org.keycloak.testsuite.arquillian.ContainerInfo;
-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.Matchers;
 import org.keycloak.testsuite.util.SamlClient;
 import org.keycloak.testsuite.util.SamlClient.Binding;
 import org.keycloak.testsuite.util.SamlClientBuilder;
-import org.keycloak.testsuite.util.WaitUtils;
-import org.openqa.selenium.TimeoutException;
-import org.openqa.selenium.WebDriver;
-import org.openqa.selenium.support.ui.WebDriverWait;
 
 /**
  *
  * @author hmlnarik
  */
-public abstract class AbstractSAMLAdapterClusteredTest extends AbstractServletsAdapterTest {
-
-    protected static final String NODE_1_NAME = "ha-node-1";
-    protected static final String NODE_2_NAME = "ha-node-2";
-
-    // target containers will be replaced in runtime in DeploymentTargetModifier by real app-server
-    public static final String TARGET_CONTAINER_NODE_1 = APP_SERVER_CURRENT + NODE_1_NAME;
-    public static final String TARGET_CONTAINER_NODE_2 = APP_SERVER_CURRENT + 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);
-
-    protected LoadBalancingProxyClient loadBalancerToNodes;
-    protected Undertow reverseProxyToNodes;
-
-    @ArquillianResource
-    protected ContainerController controller;
-
-    @ArquillianResource
-    protected Deployer deployer;
-
-    @Page
-    LoginActions loginActionsPage;
+public abstract class AbstractSAMLAdapterClusteredTest extends AbstractAdapterClusteredTest {
 
     @Override
     public void addTestRealms(List<RealmRepresentation> testRealms) {
@@ -109,79 +54,27 @@ public abstract class AbstractSAMLAdapterClusteredTest extends AbstractServletsA
     @Override
     public void setDefaultPageUriParameters() {
         super.setDefaultPageUriParameters();
-
         testRealmSAMLPostLoginPage.setAuthRealm(DEMO);
         loginPage.setAuthRealm(DEMO);
         loginActionsPage.setAuthRealm(DEMO);
     }
 
-    @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)));
-    }
-
-    @Before
-    public void prepareReverseProxy() throws Exception {
-        loadBalancerToNodes = new LoadBalancingProxyClient().addHost(NODE_1_URI, NODE_1_NAME).setConnectionsPerThread(10);
-        int maxTime = 3600000; // 1 hour for proxy request timeout, so we can debug the backend keycloak servers
-        reverseProxyToNodes = Undertow.builder()
-          .addHttpListener(HTTP_PORT_NODE_REVPROXY, "localhost")
-          .setIoThreads(2)
-          .setHandler(new ProxyHandler(loadBalancerToNodes, maxTime, ResponseCodeHandler.HANDLE_404)).build();
-        reverseProxyToNodes.start();
-    }
-
-    @Before
-    public void startServers() throws Exception {
-        prepareServerDirectories();
-        
-        for (ContainerInfo containerInfo : testContext.getAppServerBackendsInfo()) {
-            controller.start(containerInfo.getQualifier());
-        }
+    @Override
+    protected void deploy() {
         deployer.deploy(EmployeeServletDistributable.DEPLOYMENT_NAME);
         deployer.deploy(EmployeeServletDistributable.DEPLOYMENT_NAME + "_2");
     }
 
-    protected abstract void prepareServerDirectories() throws Exception;
-
-    protected void prepareServerDirectory(String baseDir, String targetSubdirectory) throws IOException {
-        Path path = Paths.get(System.getProperty("app.server.home"), targetSubdirectory);
-        File targetSubdirFile = path.toFile();
-        FileUtils.deleteDirectory(targetSubdirFile);
-        FileUtils.forceMkdir(targetSubdirFile);
-        //workaround for WFARQ-44
-        FileUtils.copyDirectory(Paths.get(System.getProperty("app.server.home"), baseDir, "deployments").toFile(), new File(targetSubdirFile, "deployments"));
-        FileUtils.copyDirectory(Paths.get(System.getProperty("app.server.home"), baseDir, "configuration").toFile(), new File(targetSubdirFile, "configuration"));
-    }
-
-    @After
-    public void stopReverseProxy() {
-        reverseProxyToNodes.stop();
-    }
-
-    @After
-    public void stopServers() {
+    @Override
+    protected void undeploy() {
         deployer.undeploy(EmployeeServletDistributable.DEPLOYMENT_NAME);
         deployer.undeploy(EmployeeServletDistributable.DEPLOYMENT_NAME + "_2");
-
-        for (ContainerInfo containerInfo : testContext.getAppServerBackendsInfo()) {
-            controller.stop(containerInfo.getQualifier());
-        }
     }
 
     private void testLogoutViaSessionIndex(URL employeeUrl, boolean forceRefreshAtOtherNode, BiConsumer<SamlClientBuilder, String> logoutFunction) {
         setPasswordFor(bburkeUser, CredentialRepresentation.PASSWORD);
 
-        final String employeeUrlString;
-        try {
-            URL employeeUrlAtRevProxy = new URL(employeeUrl.getProtocol(), employeeUrl.getHost(), HTTP_PORT_NODE_REVPROXY, employeeUrl.getFile());
-            employeeUrlString = employeeUrlAtRevProxy.toString();
-        } catch (MalformedURLException ex) {
-            throw new RuntimeException(ex);
-        }
-
+        String employeeUrlString = getProxiedUrl(employeeUrl);
         SamlClientBuilder builder = new SamlClientBuilder()
           // Go to employee URL at reverse proxy which is set to forward to first node
           .navigateTo(employeeUrlString)
@@ -270,36 +163,4 @@ public abstract class AbstractSAMLAdapterClusteredTest extends AbstractServletsA
             ;
         });
     }
-
-    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();
-        assertCurrentUrlStartsWith(loginPage);
-    }
-} 
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/OIDCAdapterClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/OIDCAdapterClusterTest.java
new file mode 100644
index 0000000..1a4e66a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/OIDCAdapterClusterTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018 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 static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.container.test.api.TargetsContainer;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.adapter.AbstractAdapterClusteredTest;
+import org.keycloak.testsuite.adapter.page.SessionPortalDistributable;
+import org.keycloak.testsuite.adapter.servlet.SessionServlet;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+import org.keycloak.testsuite.arquillian.containers.ContainerConstants;
+import org.keycloak.testsuite.auth.page.AuthRealm;
+import org.keycloak.testsuite.auth.page.login.OIDCLogin;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_CLUSTER)
+@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED_CLUSTER)
+@AppServerContainer(ContainerConstants.APP_SERVER_EAP_CLUSTER)
+@AppServerContainer(ContainerConstants.APP_SERVER_EAP6_CLUSTER)
+public class OIDCAdapterClusterTest extends AbstractAdapterClusteredTest {
+
+    @TargetsContainer(value = TARGET_CONTAINER_NODE_1)
+    @Deployment(name = SessionPortalDistributable.DEPLOYMENT_NAME, managed = false)
+    protected static WebArchive sessionPortalNode1() {
+        return servletDeployment(SessionPortalDistributable.DEPLOYMENT_NAME, "keycloak.json", SessionServlet.class);
+    }
+
+    @TargetsContainer(value = TARGET_CONTAINER_NODE_2)
+    @Deployment(name = SessionPortalDistributable.DEPLOYMENT_NAME + "_2", managed = false)
+    protected static WebArchive sessionPortalNode2() {
+        return servletDeployment(SessionPortalDistributable.DEPLOYMENT_NAME, "keycloak.json", SessionServlet.class);
+    }
+
+    @Page
+    protected OIDCLogin loginPage;
+
+    @Page
+    protected SessionPortalDistributable sessionPortalPage;
+
+    @Override
+    public void setDefaultPageUriParameters() {
+        super.setDefaultPageUriParameters();
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        addAdapterTestRealms(testRealms);
+    }
+
+    @Override
+    protected void deploy() {
+        deployer.deploy(SessionPortalDistributable.DEPLOYMENT_NAME);
+        deployer.deploy(SessionPortalDistributable.DEPLOYMENT_NAME + "_2");
+    }
+
+    @Override
+    protected void undeploy() {
+        deployer.undeploy(SessionPortalDistributable.DEPLOYMENT_NAME);
+        deployer.undeploy(SessionPortalDistributable.DEPLOYMENT_NAME + "_2");
+    }
+
+    @Before
+    public void onBefore() {
+        loginPage.setAuthRealm(AuthRealm.DEMO);
+    }
+
+    @Test
+    public void testSuccessfulLoginAndBackchannelLogout(@ArquillianResource
+                                    @OperateOnDeployment(value = SessionPortalDistributable.DEPLOYMENT_NAME) URL appUrl) {
+        String proxiedUrl = getProxiedUrl(appUrl);
+        driver.navigate().to(proxiedUrl);
+        assertCurrentUrlStartsWith(loginPage);
+        loginPage.form().login("bburke@redhat.com", "password");
+        assertCurrentUrlEquals(proxiedUrl);
+        assertSessionCounter(NODE_2_NAME, NODE_2_URI, NODE_1_URI, proxiedUrl, 2);
+        assertSessionCounter(NODE_1_NAME, NODE_1_URI, NODE_2_URI, proxiedUrl, 3);
+        assertSessionCounter(NODE_2_NAME, NODE_2_URI, NODE_1_URI, proxiedUrl, 4);
+
+        String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
+                .queryParam(OAuth2Constants.REDIRECT_URI, proxiedUrl).build(AuthRealm.DEMO).toString();
+        driver.navigate().to(logoutUri);
+        driver.navigate().to(proxiedUrl);
+        assertCurrentUrlStartsWith(loginPage);
+    }
+
+    @Test
+    public void testSuccessfulLoginAndProgrammaticLogout(@ArquillianResource
+                                                        @OperateOnDeployment(value = SessionPortalDistributable.DEPLOYMENT_NAME) URL appUrl) {
+        String proxiedUrl = getProxiedUrl(appUrl);
+        driver.navigate().to(proxiedUrl);
+        assertCurrentUrlStartsWith(loginPage);
+        loginPage.form().login("bburke@redhat.com", "password");
+        assertCurrentUrlEquals(proxiedUrl);
+        assertSessionCounter(NODE_2_NAME, NODE_2_URI, NODE_1_URI, proxiedUrl, 2);
+        assertSessionCounter(NODE_1_NAME, NODE_1_URI, NODE_2_URI, proxiedUrl, 3);
+        assertSessionCounter(NODE_2_NAME, NODE_2_URI, NODE_1_URI, proxiedUrl, 4);
+
+        String logoutUri = proxiedUrl + "/logout";
+        driver.navigate().to(logoutUri);
+        driver.navigate().to(proxiedUrl);
+        assertCurrentUrlStartsWith(loginPage);
+    }
+
+    private void assertSessionCounter(String hostToPointToName, URI hostToPointToUri, URI hostToRemove, String appUrl, int expectedCount) {
+        updateProxy(hostToPointToName, hostToPointToUri, hostToRemove);
+        driver.navigate().to(appUrl);
+        assertThat(driver.getPageSource(), containsString("Counter=" + expectedCount));
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/SAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/SAMLAdapterClusterTest.java
index 84e80aa..7664984 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/SAMLAdapterClusterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/SAMLAdapterClusterTest.java
@@ -26,8 +26,6 @@ import org.keycloak.testsuite.adapter.servlet.SendUsernameServlet;
 import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
 import org.keycloak.testsuite.arquillian.containers.ContainerConstants;
 
-import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment;
-
 /**
  *
  * @author hmlnarik
@@ -52,10 +50,4 @@ public class SAMLAdapterClusterTest extends AbstractSAMLAdapterClusteredTest {
     protected static WebArchive employee2() {
         return employee();
     }
-
-    @Override
-    protected void prepareServerDirectories() throws Exception {
-        prepareServerDirectory("standalone-cluster", "standalone-" + NODE_1_NAME);
-        prepareServerDirectory("standalone-cluster", "standalone-" + NODE_2_NAME);
-    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
index 5afd248..2d4f615 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
@@ -271,6 +271,16 @@
             "secret": "password"
         },
         {
+            "clientId": "session-portal-distributable",
+            "enabled": true,
+            "adminUrl": "http://localhost:8580/session-portal-distributable",
+            "baseUrl": "http://localhost:8580/session-portal-distributable",
+            "redirectUris": [
+                "http://localhost:8580/session-portal-distributable/*"
+            ],
+            "secret": "password"
+        },
+        {
             "clientId": "input-portal",
             "enabled": true,
             "adminUrl": "/input-portal",
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/META-INF/context.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/META-INF/context.xml
new file mode 100644
index 0000000..067e33d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/META-INF/context.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ * Copyright 2018 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.
+  -->
+
+<Context path="/session-portal-distributable">
+    <Valve className="org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve"/>
+</Context>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/WEB-INF/jetty-web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/WEB-INF/jetty-web.xml
new file mode 100644
index 0000000..8c59313
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/WEB-INF/jetty-web.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+  ~ 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.
+  -->
+
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
+<Configure class="org.eclipse.jetty.webapp.WebAppContext">
+    <Get name="securityHandler">
+        <Set name="authenticator">
+            <New class="org.keycloak.adapters.jetty.KeycloakJettyAuthenticator">
+                <!--
+                <Set name="adapterConfig">
+                    <New class="org.keycloak.representations.adapters.config.AdapterConfig">
+                        <Set name="realm">tomcat</Set>
+                        <Set name="resource">customer-portal</Set>
+                        <Set name="authServerUrl">http://localhost:8180/auth</Set>
+                        <Set name="sslRequired">external</Set>
+                        <Set name="credentials">
+                            <Map>
+                                <Entry>
+                                    <Item>secret</Item>
+                                    <Item>password</Item>
+                                </Entry>
+                            </Map>
+                        </Set>
+                        <Set name="realmKey">MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</Set>
+                    </New>
+                </Set>
+                -->
+            </New>
+        </Set>
+    </Get>
+</Configure>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/WEB-INF/keycloak.json
new file mode 100644
index 0000000..e46549e
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/WEB-INF/keycloak.json
@@ -0,0 +1,10 @@
+{
+  "realm" : "demo",
+  "resource" : "session-portal-distributable",
+  "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+  "auth-server-url" : "http://localhost:8180/auth",
+  "ssl-required" : "external",
+  "credentials" : {
+      "secret": "password"
+   }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/WEB-INF/web.xml
new file mode 100644
index 0000000..8fbba24
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/session-portal-distributable/WEB-INF/web.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ * Copyright 2018 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">
+
+    <module-name>session-portal-distributable</module-name>
+
+    <distributable/>
+
+    <absolute-ordering/>
+
+    <servlet>
+        <servlet-name>Servlet</servlet-name>
+        <servlet-class>org.keycloak.testsuite.adapter.servlet.SessionServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>Servlet</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Users</web-resource-name>
+            <url-pattern>/*</url-pattern>
+        </web-resource-collection>
+        <auth-constraint>
+            <role-name>user</role-name>
+        </auth-constraint>
+    </security-constraint>
+
+    <login-config>
+        <auth-method>KEYCLOAK</auth-method>
+        <realm-name>demo</realm-name>
+    </login-config>
+
+    <security-role>
+        <role-name>admin</role-name>
+    </security-role>
+    <security-role>
+        <role-name>user</role-name>
+    </security-role>
+</web-app>