keycloak-aplcache

Changes

Details

diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
index 8192e71..1584c11 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
@@ -308,6 +308,16 @@ public class AdapterDeploymentContext {
         }
 
         @Override
+        public String getAdapterStateCookiePath() {
+            return delegate.getAdapterStateCookiePath();
+        }
+
+        @Override
+        public void setAdapterStateCookiePath(String adapterStateCookiePath) {
+            delegate.setAdapterStateCookiePath(adapterStateCookiePath);
+        }
+
+        @Override
         public String getStateCookieName() {
             return delegate.getStateCookieName();
         }
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java
index 1665c92..c13cbfa 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java
@@ -47,7 +47,7 @@ public class CookieTokenStore {
                 .append(idToken).append(DELIM)
                 .append(refreshToken).toString();
 
-        String cookiePath = getContextPath(facade);
+        String cookiePath = getCookiePath(deployment, facade);
         facade.getResponse().setCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, cookie, cookiePath, null, -1, deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr()), true);
     }
 
@@ -98,14 +98,29 @@ public class CookieTokenStore {
         }
     }
 
-    public static void removeCookie(HttpFacade facade) {
-        String cookiePath = getContextPath(facade);
+    public static void removeCookie(KeycloakDeployment deployment, HttpFacade facade) {
+        String cookiePath = getCookiePath(deployment, facade);
         facade.getResponse().resetCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, cookiePath);
     }
 
-    private static String getContextPath(HttpFacade facade) {
+    static String getCookiePath(KeycloakDeployment deployment, HttpFacade facade) {
+        if (deployment.getAdapterStateCookiePath().startsWith("/")) {
+            return deployment.getAdapterStateCookiePath();
+        }
+        String contextPath = getContextPath(facade);
+        StringBuilder cookiePath = new StringBuilder(contextPath);
+        if (!contextPath.endsWith("/") && !deployment.getAdapterStateCookiePath().isEmpty()) {
+            cookiePath.append("/");
+        }
+        return cookiePath.append(deployment.getAdapterStateCookiePath()).toString();
+    }
+    
+    static String getContextPath(HttpFacade facade) {
         String uri = facade.getRequest().getURI();
         String path = KeycloakUriBuilder.fromUri(uri).getPath();
+        if (path == null || path.isEmpty()) {
+            return "/";
+        }
         int index = path.indexOf("/", 1);
         return index == -1 ? path : path.substring(0, index);
     }
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
index 89f38cc..b2d1874 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
@@ -70,6 +70,7 @@ public class KeycloakDeployment {
     protected SslRequired sslRequired = SslRequired.ALL;
     protected int confidentialPort = -1;
     protected TokenStore tokenStore = TokenStore.SESSION;
+    protected String adapterStateCookiePath = "";
     protected String stateCookieName = "OAuth_Token_Request_State";
     protected boolean useResourceRoleMappings;
     protected boolean cors;
@@ -297,6 +298,14 @@ public class KeycloakDeployment {
         this.tokenStore = tokenStore;
     }
 
+    public String getAdapterStateCookiePath() {
+        return adapterStateCookiePath;
+    }
+
+    public void setAdapterStateCookiePath(String adapterStateCookiePath) {
+        this.adapterStateCookiePath = adapterStateCookiePath;
+    }
+
     public String getStateCookieName() {
         return stateCookieName;
     }
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
index 936c065..c05bcb6 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
@@ -88,6 +88,9 @@ public class KeycloakDeploymentBuilder {
         } else {
             deployment.setTokenStore(TokenStore.SESSION);
         }
+        if (adapterConfig.getTokenCookiePath() != null) {
+            deployment.setAdapterStateCookiePath(adapterConfig.getTokenCookiePath());
+        }
         if (adapterConfig.getPrincipalAttribute() != null) deployment.setPrincipalAttribute(adapterConfig.getPrincipalAttribute());
 
         deployment.setResourceCredentials(adapterConfig.getCredentials());
diff --git a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettyCookieTokenStore.java b/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettyCookieTokenStore.java
index 2aa973d..3222998 100755
--- a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettyCookieTokenStore.java
+++ b/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettyCookieTokenStore.java
@@ -85,7 +85,7 @@ public class JettyCookieTokenStore implements AdapterTokenStore {
 
     @Override
     public void logout() {
-        CookieTokenStore.removeCookie(facade);
+        CookieTokenStore.removeCookie(deployment, facade);
 
     }
 
@@ -113,7 +113,7 @@ public class JettyCookieTokenStore implements AdapterTokenStore {
         if (success && session.isActive()) return principal;
 
         log.debugf("Cleanup and expire cookie for user %s after failed refresh", principal.getName());
-        CookieTokenStore.removeCookie(facade);
+        CookieTokenStore.removeCookie(deployment, facade);
         return null;
     }
 
diff --git a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaCookieTokenStore.java b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaCookieTokenStore.java
index d2b6474..768a4dd 100755
--- a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaCookieTokenStore.java
+++ b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaCookieTokenStore.java
@@ -93,7 +93,7 @@ public class CatalinaCookieTokenStore implements AdapterTokenStore {
 
     @Override
     public void logout() {
-        CookieTokenStore.removeCookie(facade);
+        CookieTokenStore.removeCookie(deployment, facade);
     }
 
     @Override
@@ -132,7 +132,7 @@ public class CatalinaCookieTokenStore implements AdapterTokenStore {
         log.fine("Cleanup and expire cookie for user " + principal.getName() + " after failed refresh");
         request.setUserPrincipal(null);
         request.setAuthType(null);
-        CookieTokenStore.removeCookie(facade);
+        CookieTokenStore.removeCookie(deployment, facade);
         return null;
     }
 }
diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java
index d3556c8..a5287d5 100755
--- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java
+++ b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java
@@ -74,7 +74,7 @@ public class UndertowCookieTokenStore implements AdapterTokenStore {
             return true;
         } else {
             log.debug("Account was not active, removing cookie and returning false");
-            CookieTokenStore.removeCookie(facade);
+            CookieTokenStore.removeCookie(deployment, facade);
             return false;
         }
     }
@@ -90,7 +90,7 @@ public class UndertowCookieTokenStore implements AdapterTokenStore {
         KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this);
         if (principal == null) return;
 
-        CookieTokenStore.removeCookie(facade);
+        CookieTokenStore.removeCookie(deployment, facade);
     }
 
     @Override
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationDefinition.java
index bcfd399..55295c6 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationDefinition.java
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationDefinition.java
@@ -101,6 +101,12 @@ abstract class AbstractAdapterConfigurationDefinition extends SimpleResourceDefi
                     .setAllowExpression(true)
                     .setValidator(new IntRangeValidator(-1, true))
                     .build();
+    protected static final SimpleAttributeDefinition COOKIE_PATH =
+            new SimpleAttributeDefinitionBuilder("token-cookie-path", ModelType.STRING, true)
+                    .setXmlName("token-cookie-path")
+                    .setAllowExpression(true)
+                    .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true))
+                    .build();
 
     static final List<SimpleAttributeDefinition> DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();
 
@@ -115,6 +121,7 @@ abstract class AbstractAdapterConfigurationDefinition extends SimpleResourceDefi
         DEPLOYMENT_ONLY_ATTRIBUTES.add(TOKEN_MINIMUM_TIME_TO_LIVE);
         DEPLOYMENT_ONLY_ATTRIBUTES.add(MIN_TIME_BETWEEN_JWKS_REQUESTS);
         DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_KEY_CACHE_TTL);
+        DEPLOYMENT_ONLY_ATTRIBUTES.add(COOKIE_PATH);
     }
 
     static final List<SimpleAttributeDefinition> ALL_ATTRIBUTES = new ArrayList();
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties
index 8678ae5..4bff019 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties
@@ -98,6 +98,7 @@ keycloak.secure-deployment.public-key-cache-ttl=Maximum time the downloaded publ
 keycloak.secure-deployment.ignore-oauth-query-parameter=disable query parameter parsing for access_token
 keycloak.secure-deployment.proxy-url=The URL for the HTTP proxy if one is used.
 keycloak.secure-deployment.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience
+keycloak.secure-deployment.token-cookie-path=If set, defines the path used in cookies set by the adapter. Useful when deploying the application in the root context path.
 
 keycloak.secure-server=A deployment secured by Keycloak
 keycloak.secure-server.add=Add a deployment to be secured by Keycloak
@@ -142,6 +143,7 @@ keycloak.secure-server.public-key-cache-ttl=Maximum time the downloaded public k
 keycloak.secure-server.ignore-oauth-query-parameter=disable query parameter parsing for access_token
 keycloak.secure-server.proxy-url=The URL for the HTTP proxy if one is used.
 keycloak.secure-server.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience
+keycloak.secure-server.token-cookie-path=If set, defines the path used in cookies set by the adapter. Useful when deploying the application in the root context path.
 
 keycloak.secure-deployment.credential=Credential value
 keycloak.secure-server.credential=Credential value
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd
index 62ce35d..61a5204 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd
@@ -122,6 +122,7 @@
             <xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
             <xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/>
             <xs:element name="verify-token-audience" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
+            <xs:element name="token-cookie-path" type="xs:string" minOccurs="0" maxOccurs="1"/>
         </xs:all>
         <xs:attribute name="name" type="xs:string" use="required">
             <xs:annotation>
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml
index 367aec1..047bc3e 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml
@@ -69,6 +69,7 @@
         <realm>master</realm>
         <resource>http-endpoint</resource>
         <use-resource-role-mappings>true</use-resource-role-mappings>
+        <token-cookie-path>/</token-cookie-path>
         <realm-public-key>
             MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB
         </realm-public-key>
@@ -90,6 +91,7 @@
         <realm>jboss-infra</realm>
         <resource>wildfly-console</resource>
         <public-client>true</public-client>
+        <token-cookie-path>/</token-cookie-path>
         <ssl-required>EXTERNAL</ssl-required>
         <confidential-port>443</confidential-port>
         <proxy-url>http://localhost:9000</proxy-url>
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 bbe1046..f5ecd88 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
@@ -18,12 +18,9 @@
 
 package org.keycloak.adapters.elytron;
 
-import java.security.Principal;
-
 import org.jboss.logging.Logger;
 import org.keycloak.KeycloakPrincipal;
 import org.keycloak.KeycloakSecurityContext;
-import org.keycloak.adapters.AdapterTokenStore;
 import org.keycloak.adapters.CookieTokenStore;
 import org.keycloak.adapters.KeycloakDeployment;
 import org.keycloak.adapters.OidcKeycloakAccount;
@@ -98,7 +95,7 @@ public class ElytronCookieTokenStore implements ElytronTokeStore {
             return true;
         } else {
             log.debug("Account was not active, removing cookie and returning false");
-            CookieTokenStore.removeCookie(httpFacade);
+            CookieTokenStore.removeCookie(deployment, httpFacade);
             return false;
         }
     }
@@ -145,7 +142,7 @@ public class ElytronCookieTokenStore implements ElytronTokeStore {
             return;
         }
 
-        CookieTokenStore.removeCookie(this.httpFacade);
+        CookieTokenStore.removeCookie(this.httpFacade.getDeployment(), this.httpFacade);
 
         if (glo) {
             KeycloakSecurityContext ksc = (KeycloakSecurityContext) principal.getKeycloakSecurityContext();
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
index 095a613..a4f0d6b 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
@@ -37,7 +37,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
         "allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
         "client-keystore", "client-keystore-password", "client-key-password",
         "always-refresh-token",
-        "register-node-at-startup", "register-node-period", "token-store", "principal-attribute",
+        "register-node-at-startup", "register-node-period", "token-store", "token-cookie-path", "principal-attribute",
         "proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live",
         "min-time-between-jwks-requests", "public-key-cache-ttl",
         "policy-enforcer", "ignore-oauth-query-parameter", "verify-token-audience"
@@ -68,6 +68,8 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
     protected int registerNodePeriod = -1;
     @JsonProperty("token-store")
     protected String tokenStore;
+    @JsonProperty("token-cookie-path")
+    protected String tokenCookiePath;
     @JsonProperty("principal-attribute")
     protected String principalAttribute;
     @JsonProperty("turn-off-change-session-id-on-login")
@@ -197,6 +199,14 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
         this.tokenStore = tokenStore;
     }
 
+    public String getTokenCookiePath() {
+        return tokenCookiePath;
+    }
+
+    public void setTokenCookiePath(String tokenCookiePath) {
+        this.tokenCookiePath = tokenCookiePath;
+    }
+
     public String getPrincipalAttribute() {
         return principalAttribute;
     }
diff --git a/testsuite/integration-arquillian/servers/app-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/UndertowAppServer.java b/testsuite/integration-arquillian/servers/app-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/UndertowAppServer.java
index d9b4ce5..81f9980 100644
--- a/testsuite/integration-arquillian/servers/app-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/UndertowAppServer.java
+++ b/testsuite/integration-arquillian/servers/app-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/UndertowAppServer.java
@@ -120,6 +120,10 @@ public class UndertowAppServer implements DeployableContainer<UndertowAppServerC
             throw new IllegalArgumentException("UndertowContainer only supports UndertowWebArchive or WebArchive.");
         }
 
+        if ("ROOT.war".equals(archive.getName())) {
+            di.setContextPath("/");
+        }
+
         ClassLoader parentCl = Thread.currentThread().getContextClassLoader();
         UndertowWarClassLoader classLoader = new UndertowWarClassLoader(parentCl, archive);
         Thread.currentThread().setContextClassLoader(classLoader);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/CustomerCookiePortalRoot.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/CustomerCookiePortalRoot.java
new file mode 100644
index 0000000..8370c1c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/CustomerCookiePortalRoot.java
@@ -0,0 +1,57 @@
+/*
+ * 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.MalformedURLException;
+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;
+import org.keycloak.testsuite.util.URLUtils;
+
+public class CustomerCookiePortalRoot extends AbstractPageWithInjectedUrl {
+
+    public static final String DEPLOYMENT_NAME = "customer-cookie-portal-root";
+
+    @ArquillianResource
+    @OperateOnDeployment(DEPLOYMENT_NAME)
+    private URL url;
+
+    @Override
+    public URL getInjectedUrl() {
+        try {
+            return new URL(url.toString() + "/");
+        } catch (MalformedURLException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    
+    public String logoutURL() {
+        return  getInjectedUrl() + "/logout";
+    }
+
+    public void navigateTo(String relative) {
+        URLUtils.navigateToUri(getInjectedUrl() + relative);
+    }
+
+    @Override
+    public String toString() {
+        return getInjectedUrl().toString();
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/CookieStoreRootContextTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/CookieStoreRootContextTest.java
new file mode 100644
index 0000000..a8b23f7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/CookieStoreRootContextTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.constants.AdapterConstants;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
+import org.keycloak.testsuite.adapter.page.CustomerCookiePortalRoot;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+import org.keycloak.testsuite.arquillian.containers.ContainerConstants;
+import org.keycloak.testsuite.auth.page.login.OIDCLogin;
+import org.keycloak.testsuite.util.JavascriptBrowser;
+import org.keycloak.testsuite.util.WaitUtils;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Cookie;
+import org.openqa.selenium.WebDriver;
+
+/**
+ *
+ * @author tkyjovsk
+ */
+@AppServerContainer(ContainerConstants.APP_SERVER_UNDERTOW)
+@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY)
+@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED)
+@AppServerContainer(ContainerConstants.APP_SERVER_EAP)
+@AppServerContainer(ContainerConstants.APP_SERVER_EAP6)
+public class CookieStoreRootContextTest extends DemoServletsAdapterTest {
+
+    // Javascript browser needed KEYCLOAK-4703
+    @Drone
+    @JavascriptBrowser
+    protected WebDriver jsDriver;
+
+    @Page
+    @JavascriptBrowser
+    protected OIDCLogin jsDriverTestRealmLoginPage;
+
+    @Page
+    private CustomerCookiePortalRoot customerCookiePortalRoot;
+
+    @Rule
+    public AssertEvents assertEvents = new AssertEvents(this);
+
+    @Deployment(name = CustomerCookiePortalRoot.DEPLOYMENT_NAME)
+    protected static WebArchive customerCookiePortalRoot() {
+        WebArchive original = servletDeployment(CustomerCookiePortalRoot.DEPLOYMENT_NAME, AdapterActionsFilter.class, CustomerServlet.class, ErrorServlet.class, ServletTestUtils.class);
+
+        WebArchive archive = ShrinkWrap.create(WebArchive.class, "ROOT.war");
+
+        archive.merge(original);
+
+        return archive;
+    }
+
+    @Override
+    public void setDefaultPageUriParameters() {
+        super.setDefaultPageUriParameters();
+        configPage.setConsoleRealm(DEMO);
+        loginEventsPage.setConsoleRealm(DEMO);
+        applicationsPage.setAuthRealm(DEMO);
+        loginEventsPage.setConsoleRealm(DEMO);
+    }
+    
+    @Test
+    public void testTokenInCookieSSORoot() {
+        // Login
+        String tokenCookie = loginToCustomerCookiePortalRoot();
+        Cookie cookie = driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE);
+        assertEquals("/", cookie.getPath());
+
+        // SSO to second app
+        customerPortal.navigateTo();
+        assertLogged();
+
+        customerCookiePortalRoot.navigateTo();
+        assertLogged();
+        cookie = driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE);
+        String tokenCookie2 = cookie.getValue();
+        assertEquals(tokenCookie, tokenCookie2);
+        assertEquals("/", cookie.getPath());
+
+        // Logout with httpServletRequest
+        logoutFromCustomerCookiePortalRoot();
+
+        // Also should be logged-out from the second app
+        customerPortal.navigateTo();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+    }
+
+    private String loginToCustomerCookiePortalRoot() {
+        customerCookiePortalRoot.navigateTo("relative");
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+        testRealmLoginPage.form().login("bburke@redhat.com", "password");
+        assertCurrentUrlEquals(customerCookiePortalRoot.getInjectedUrl().toString() + "relative");
+        assertLogged();
+
+        // Assert no JSESSIONID cookie
+        Assert.assertNull(driver.manage().getCookieNamed("JSESSIONID"));
+
+        return driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue();
+    }
+    
+    private void logoutFromCustomerCookiePortalRoot() {
+        String logout = customerCookiePortalRoot.logoutURL();
+        driver.navigate().to(logout);
+        WaitUtils.waitUntilElement(By.id("customer_portal_logout")).is().present();
+        assertNull(driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE));
+        customerCookiePortalRoot.navigateTo();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java
index 2847b2c..da8df41 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java
@@ -145,7 +145,7 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
     protected OIDCLogin jsDriverTestRealmLoginPage;
 
     @Page
-    private CustomerPortal customerPortal;
+    protected CustomerPortal customerPortal;
     @Page
     private CustomerPortalNoConf customerPortalNoConf;
     @Page
@@ -169,13 +169,13 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
     @Page
     private OAuthGrant oAuthGrantPage;
     @Page
-    private Applications applicationsPage;
+    protected Applications applicationsPage;
     @Page
-    private LoginEvents loginEventsPage;
+    protected LoginEvents loginEventsPage;
     @Page
     private BasicAuth basicAuthPage;
     @Page
-    private Config configPage;
+    protected Config configPage;
     @Page
     private ClientSecretJwtSecurePortal clientSecretJwtSecurePortal;
     @Page
@@ -393,7 +393,7 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
         assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
     }
     
-    private void assertLogged() {
+    protected void assertLogged() {
         assertPageContains("Bill Burke");
         assertPageContains("Stian Thorgersen");
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-cookie-portal-root/META-INF/context.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-cookie-portal-root/META-INF/context.xml
new file mode 100644
index 0000000..abff3d2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-cookie-portal-root/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="/customer-portal">
+    <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/customer-cookie-portal-root/WEB-INF/jetty-web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-cookie-portal-root/WEB-INF/jetty-web.xml
new file mode 100644
index 0000000..8df5936
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-cookie-portal-root/WEB-INF/jetty-web.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+  ~ 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.
+  -->
+
+<!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/customer-cookie-portal-root/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-cookie-portal-root/WEB-INF/keycloak.json
new file mode 100644
index 0000000..e699fe6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-cookie-portal-root/WEB-INF/keycloak.json
@@ -0,0 +1,13 @@
+{
+    "realm": "demo",
+    "resource": "customer-cookie-portal-root",
+    "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+    "auth-server-url": "http://localhost:8180/auth",
+    "ssl-required" : "external",
+    "expose-token": true,
+    "token-store": "cookie",
+    "token-cookie-path": "/",
+    "credentials": {
+        "secret": "password"
+    }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-cookie-portal-root/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-cookie-portal-root/WEB-INF/web.xml
new file mode 100644
index 0000000..955cb1f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-cookie-portal-root/WEB-INF/web.xml
@@ -0,0 +1,84 @@
+<?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">
+
+    <servlet>
+        <servlet-name>Servlet</servlet-name>
+        <servlet-class>org.keycloak.testsuite.adapter.servlet.CustomerServlet</servlet-class>
+    </servlet>
+    <servlet>
+        <servlet-name>Error Servlet</servlet-name>
+        <servlet-class>org.keycloak.testsuite.adapter.servlet.ErrorServlet</servlet-class>
+    </servlet>
+
+    <filter>
+        <filter-name>AdapterActionsFilter</filter-name>
+        <filter-class>org.keycloak.testsuite.adapter.filter.AdapterActionsFilter</filter-class>
+    </filter>
+
+    <servlet-mapping>
+        <servlet-name>Servlet</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+
+    <servlet-mapping>
+        <servlet-name>Error Servlet</servlet-name>
+        <url-pattern>/error.html</url-pattern>
+    </servlet-mapping>
+
+    <filter-mapping>
+        <filter-name>AdapterActionsFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-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>
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Errors</web-resource-name>
+            <url-pattern>/error.html</url-pattern>
+        </web-resource-collection>
+    </security-constraint>
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Unsecured</web-resource-name>
+            <url-pattern>/unsecured/*</url-pattern>
+        </web-resource-collection>
+    </security-constraint>
+
+
+    <login-config>
+        <auth-method>KEYCLOAK</auth-method>
+        <realm-name>demo</realm-name>
+    </login-config>
+
+    <security-role>
+        <role-name>user</role-name>
+    </security-role>
+</web-app>
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 a45098c..5afd248 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
@@ -171,6 +171,15 @@
             "secret": "password"
         },
         {
+            "clientId": "customer-cookie-portal-root",
+            "enabled": true,
+            "baseUrl": "/",
+            "redirectUris": [
+                "http://localhost:8280/*"
+            ],
+            "secret": "password"
+        },
+        {
             "clientId": "customer-portal-js",
             "enabled": true,
             "publicClient": true,