keycloak-aplcache

Merge pull request #754 from mposolda/master KEYCLOAK748

10/9/2014 4:36:13 PM

Details

diff --git a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java
index 988bfba..a4f7f51 100755
--- a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java
+++ b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java
@@ -19,6 +19,9 @@ public interface AdapterConstants {
     // two places to avoid dependency between Keycloak Subsystem and Keyclaok Undertow Integration.
     String AUTH_DATA_PARAM_NAME = "org.keycloak.json.adapterConfig";
 
-    // Attribute passed in codeToToken request from adapter to Keycloak and saved in ClientSession.
+    // Attribute passed in codeToToken request from adapter to Keycloak and saved in ClientSession. Contains ID of HttpSession on adapter
     public static final String HTTP_SESSION_ID = "http_session_id";
+
+    // Attribute passed in codeToToken request from adapter to Keycloak and saved in ClientSession. Contains hostname of adapter where HttpSession is served
+    public static final String HTTP_SESSION_HOST = "http_session_host";
 }
diff --git a/core/src/main/java/org/keycloak/util/HostUtils.java b/core/src/main/java/org/keycloak/util/HostUtils.java
new file mode 100644
index 0000000..fb1b29f
--- /dev/null
+++ b/core/src/main/java/org/keycloak/util/HostUtils.java
@@ -0,0 +1,36 @@
+package org.keycloak.util;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class HostUtils {
+
+    public static String getHostName() {
+        String jbossHostName = System.getProperty("jboss.host.name");
+        if (jbossHostName != null) {
+            return jbossHostName;
+        } else {
+            try {
+                return InetAddress.getLocalHost().getHostName();
+            } catch (UnknownHostException uhe) {
+                throw new IllegalStateException(uhe);
+            }
+        }
+    }
+
+    public static String getIpAddress() {
+        try {
+            String jbossHostName = System.getProperty("jboss.host.name");
+            if (jbossHostName != null) {
+                return InetAddress.getByName(jbossHostName).getHostAddress();
+            } else {
+                return java.net.InetAddress.getLocalHost().getHostAddress();
+            }
+        } catch (UnknownHostException uhe) {
+            throw new IllegalStateException(uhe);
+        }
+    }
+}
diff --git a/core/src/main/java/org/keycloak/util/UriUtils.java b/core/src/main/java/org/keycloak/util/UriUtils.java
index 60418ea..873283f 100644
--- a/core/src/main/java/org/keycloak/util/UriUtils.java
+++ b/core/src/main/java/org/keycloak/util/UriUtils.java
@@ -1,8 +1,6 @@
 package org.keycloak.util;
 
-import java.net.InetAddress;
 import java.net.URI;
-import java.net.UnknownHostException;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -18,12 +16,4 @@ public class UriUtils {
         return u.substring(0, u.indexOf('/', 8));
     }
 
-    public static String getHostName() {
-        try {
-            return InetAddress.getLocalHost().getHostName();
-        } catch (UnknownHostException uhe) {
-            throw new IllegalStateException(uhe);
-        }
-    }
-
 }
diff --git a/examples/demo-template/admin-access-app/src/main/java/org/keycloak/example/AdminClient.java b/examples/demo-template/admin-access-app/src/main/java/org/keycloak/example/AdminClient.java
index 2a83775..caa9ed6 100755
--- a/examples/demo-template/admin-access-app/src/main/java/org/keycloak/example/AdminClient.java
+++ b/examples/demo-template/admin-access-app/src/main/java/org/keycloak/example/AdminClient.java
@@ -13,6 +13,7 @@ import org.keycloak.ServiceUrlConstants;
 import org.keycloak.adapters.HttpClientBuilder;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.util.HostUtils;
 import org.keycloak.util.JsonSerialization;
 import org.keycloak.util.KeycloakUriBuilder;
 import org.keycloak.util.UriUtils;
@@ -161,7 +162,7 @@ public class AdminClient {
     public static String getBaseUrl(HttpServletRequest request) {
         String useHostname = request.getServletContext().getInitParameter("useHostname");
         if (useHostname != null && "true".equalsIgnoreCase(useHostname)) {
-            return "http://" + UriUtils.getHostName() + ":8080";
+            return "http://" + HostUtils.getHostName() + ":8080";
         } else {
             return UriUtils.getOrigin(request.getRequestURL().toString());
         }
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
index 970929b..b20697d 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
@@ -11,6 +11,7 @@ import org.keycloak.OAuth2Constants;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.util.BasicAuthHelper;
+import org.keycloak.util.HostUtils;
 import org.keycloak.util.JsonSerialization;
 import org.keycloak.util.KeycloakUriBuilder;
 import org.keycloak.util.StreamUtil;
@@ -101,6 +102,7 @@ public class ServerRequest {
         formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri));
         if (sessionId != null) {
             formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_ID, sessionId));
+            formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_HOST, HostUtils.getIpAddress()));
         }
         HttpResponse response = null;
         HttpPost post = new HttpPost(codeUrl);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java
index db16b48..007925c 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java
@@ -614,9 +614,13 @@ public class OpenIDConnectService {
 
         String httpSessionId = formData.getFirst(AdapterConstants.HTTP_SESSION_ID);
         if (httpSessionId != null) {
-            logger.debugf("Http Session '%s' saved in ClientSession for client '%s'", httpSessionId, client.getClientId());
+            String httpSessionHost = formData.getFirst(AdapterConstants.HTTP_SESSION_HOST);
+            logger.infof("Http Session '%s' saved in ClientSession for client '%s'. Host is '%s'", httpSessionId, client.getClientId(), httpSessionHost);
+
             event.detail(AdapterConstants.HTTP_SESSION_ID, httpSessionId);
             clientSession.setNote(AdapterConstants.HTTP_SESSION_ID, httpSessionId);
+            event.detail(AdapterConstants.HTTP_SESSION_HOST, httpSessionHost);
+            clientSession.setNote(AdapterConstants.HTTP_SESSION_HOST, httpSessionHost);
         }
 
         AccessToken token = tokenManager.createClientAccessToken(accessCode.getRequestedRoles(), realm, client, user, userSession);
diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
index 537951c..2a4576f 100755
--- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
@@ -20,6 +20,7 @@ import org.keycloak.representations.adapters.action.PushNotBeforeAction;
 import org.keycloak.representations.adapters.action.UserStats;
 import org.keycloak.services.util.HttpClientBuilder;
 import org.keycloak.services.util.ResolveRelative;
+import org.keycloak.util.MultivaluedHashMap;
 import org.keycloak.util.StringPropertyReplacer;
 import org.keycloak.util.Time;
 
@@ -31,6 +32,8 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -38,6 +41,7 @@ import java.util.Map;
  */
 public class ResourceAdminManager {
     protected static Logger logger = Logger.getLogger(ResourceAdminManager.class);
+    private static final String KC_SESSION_HOST = "${kc_session_host}";
 
     public static ApacheHttpClient4Executor createExecutor() {
         HttpClient client = new HttpClientBuilder()
@@ -69,7 +73,7 @@ public class ResourceAdminManager {
 
         try {
             // Map from "app" to clientSessions for this app
-            Map<ApplicationModel, List<ClientSessionModel>> clientSessions = new HashMap<ApplicationModel, List<ClientSessionModel>>();
+            MultivaluedHashMap<ApplicationModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ApplicationModel, ClientSessionModel>();
             for (UserSessionModel userSession : userSessions) {
                 putClientSessions(clientSessions, userSession);
             }
@@ -85,16 +89,11 @@ public class ResourceAdminManager {
         }
     }
 
-    private void putClientSessions(Map<ApplicationModel, List<ClientSessionModel>> clientSessions, UserSessionModel userSession) {
+    private void putClientSessions(MultivaluedHashMap<ApplicationModel, ClientSessionModel> clientSessions, UserSessionModel userSession) {
         for (ClientSessionModel clientSession : userSession.getClientSessions()) {
             ClientModel client = clientSession.getClient();
             if (client instanceof ApplicationModel) {
-                List<ClientSessionModel> curClientSessions = clientSessions.get(client);
-                if (curClientSessions == null) {
-                    curClientSessions = new ArrayList<ClientSessionModel>();
-                    clientSessions.put((ApplicationModel)client, curClientSessions);
-                }
-                curClientSessions.add(clientSession);
+                clientSessions.add((ApplicationModel)client, clientSession);
             }
         }
     }
@@ -103,7 +102,8 @@ public class ResourceAdminManager {
         ApacheHttpClient4Executor executor = createExecutor();
 
         try {
-            Map<ApplicationModel, List<ClientSessionModel>> clientSessions = new HashMap<ApplicationModel, List<ClientSessionModel>>();
+            // Map from "app" to clientSessions for this app
+            MultivaluedHashMap<ApplicationModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ApplicationModel, ClientSessionModel>();
             putClientSessions(clientSessions, session);
 
             logger.debugv("logging out {0} resources ", clientSessions.size());
@@ -138,7 +138,7 @@ public class ResourceAdminManager {
 
             List<ClientSessionModel> ourAppClientSessions = null;
             if (userSessions != null) {
-                Map<ApplicationModel, List<ClientSessionModel>> clientSessions = new HashMap<ApplicationModel, List<ClientSessionModel>>();
+                MultivaluedHashMap<ApplicationModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ApplicationModel, ClientSessionModel>();
                 for (UserSessionModel userSession : userSessions) {
                     putClientSessions(clientSessions, userSession);
                 }
@@ -160,34 +160,40 @@ public class ResourceAdminManager {
         String managementUrl = getManagementUrl(requestUri, resource);
         if (managementUrl != null) {
 
-            List<String> adapterSessionIds = null;
+            // Key is host, value is list of http sessions for this host
+            MultivaluedHashMap<String, String> adapterSessionIds = null;
             if (clientSessions != null && clientSessions.size() > 0) {
-                adapterSessionIds = new ArrayList<String>();
+                adapterSessionIds = new MultivaluedHashMap<String, String>();
                 for (ClientSessionModel clientSession : clientSessions) {
                     String adapterSessionId = clientSession.getNote(AdapterConstants.HTTP_SESSION_ID);
                     if (adapterSessionId != null) {
-                        adapterSessionIds.add(adapterSessionId);
+                        String host = clientSession.getNote(AdapterConstants.HTTP_SESSION_HOST);
+                        adapterSessionIds.add(host, adapterSessionId);
                     }
                 }
             }
 
-            LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), adapterSessionIds, notBefore);
-            String token = new TokenManager().encodeToken(realm, adminAction);
-            logger.debugv("logout resource {0} url: {1} sessionIds: " + adapterSessionIds, resource.getName(), managementUrl);
-            ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_LOGOUT).build().toString());
-            ClientResponse response;
-            try {
-                response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(UserStats.class);
-            } catch (Exception e) {
-                logger.warn("Logout for application '" + resource.getName() + "' failed", e);
-                return false;
-            }
-            try {
-                boolean success = response.getStatus() == 204;
-                logger.debug("logout success.");
-                return success;
-            } finally {
-                response.releaseConnection();
+            if (managementUrl.contains(KC_SESSION_HOST) && adapterSessionIds != null) {
+                boolean allPassed = true;
+                // Send logout separately to each host (needed for single-sign-out in cluster for non-distributable apps - KEYCLOAK-748)
+                for (Map.Entry<String, List<String>> entry : adapterSessionIds.entrySet()) {
+                    String host = entry.getKey();
+                    List<String> sessionIds = entry.getValue();
+                    String currentHostMgmtUrl = managementUrl.replace(KC_SESSION_HOST, host);
+                    allPassed = logoutApplicationOnHost(realm, resource, sessionIds, client, notBefore, currentHostMgmtUrl) && allPassed;
+                }
+
+                return allPassed;
+            } else {
+                // Send single logout request
+                List<String> allSessionIds = null;
+                if (adapterSessionIds != null) {
+                    allSessionIds = new ArrayList<String>();
+                    for (List<String> currentIds : adapterSessionIds.values()) {
+                        allSessionIds.addAll(currentIds);
+                    }
+                }
+                return logoutApplicationOnHost(realm, resource, allSessionIds, client, notBefore, managementUrl);
             }
         } else {
             logger.debugv("Can't logout {0}: no management url", resource.getName());
@@ -195,6 +201,27 @@ public class ResourceAdminManager {
         }
     }
 
+    protected boolean logoutApplicationOnHost(RealmModel realm, ApplicationModel resource, List<String> adapterSessionIds, ApacheHttpClient4Executor client, int notBefore, String managementUrl) {
+        LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), adapterSessionIds, notBefore);
+        String token = new TokenManager().encodeToken(realm, adminAction);
+        logger.infov("logout resource {0} url: {1} sessionIds: " + adapterSessionIds, resource.getName(), managementUrl);
+        ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_LOGOUT).build().toString());
+        ClientResponse response;
+        try {
+            response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(UserStats.class);
+        } catch (Exception e) {
+            logger.warn("Logout for application '" + resource.getName() + "' failed", e);
+            return false;
+        }
+        try {
+            boolean success = response.getStatus() == 204;
+            logger.debug("logout success.");
+            return success;
+        } finally {
+            response.releaseConnection();
+        }
+    }
+
     public void pushRealmRevocationPolicy(URI requestUri, RealmModel realm) {
         ApacheHttpClient4Executor executor = createExecutor();
 
@@ -224,7 +251,7 @@ public class ResourceAdminManager {
         if (managementUrl != null) {
             PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), notBefore);
             String token = new TokenManager().encodeToken(realm, adminAction);
-            logger.debugv("pushRevocation resource: {0} url: {1}", resource.getName(), managementUrl);
+            logger.infov("pushRevocation resource: {0} url: {1}", resource.getName(), managementUrl);
             ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).build().toString());
             ClientResponse response;
             try {
diff --git a/testsuite/docker-cluster/shared-files/deploy-examples.sh b/testsuite/docker-cluster/shared-files/deploy-examples.sh
index 0778a3d..f71f697 100644
--- a/testsuite/docker-cluster/shared-files/deploy-examples.sh
+++ b/testsuite/docker-cluster/shared-files/deploy-examples.sh
@@ -25,17 +25,18 @@ sed -i -e 's/false/true/' admin-access.war/WEB-INF/web.xml
 
 # Enforce refreshing token for product-portal and customer-portal war
 # sed -i -e 's/\"\/auth\",/&\n    \"always-refresh-token\": true,/' customer-portal.war/WEB-INF/keycloak.json;
-sed -i -e 's/\"\/auth\",/&\n    \"always-refresh-token\": true,/' product-portal.war/WEB-INF/keycloak.json;
+# sed -i -e 's/\"\/auth\",/&\n    \"always-refresh-token\": true,/' product-portal.war/WEB-INF/keycloak.json;
 
 # Configure other examples
 for I in *.war/WEB-INF/keycloak.json; do
-  sed -i -e 's/\"\/auth\",/&\n    \"auth-server-url-for-backend-requests\": \"http:\/\/\$\{jboss.host.name\}:8080\/auth\",/' $I;
+  sed -i -e 's/\"auth-server-url\".*: \"\/auth\",/&\n    \"auth-server-url-for-backend-requests\": \"http:\/\/\$\{jboss.host.name\}:8080\/auth\",/' $I;
 done;
 
 # Enable distributable for customer-portal
 sed -i -e 's/<\/module-name>/&\n    <distributable \/>/' customer-portal.war/WEB-INF/web.xml
 
 # Configure testrealm.json - Enable adminUrl to access adapters on local machine
-sed -i -e 's/\"adminUrl\": \"/&http:\/\/\$\{jboss.host.name\}:8080/' /keycloak-docker-cluster/examples/testrealm.json
+sed -i -e 's/\"adminUrl\": \"\/customer-portal/\"adminUrl\": \"http:\/\/\$\{jboss.host.name\}:8080\/customer-portal/' /keycloak-docker-cluster/examples/testrealm.json
+sed -i -e 's/\"adminUrl\": \"\/product-portal/\"adminUrl\": \"http:\/\/\$\{kc_session_host\}:8080\/product-portal/' /keycloak-docker-cluster/examples/testrealm.json
 
 
diff --git a/testsuite/docker-cluster/shared-files/keycloak-run-node.sh b/testsuite/docker-cluster/shared-files/keycloak-run-node.sh
index 55edc82..7d350aa 100644
--- a/testsuite/docker-cluster/shared-files/keycloak-run-node.sh
+++ b/testsuite/docker-cluster/shared-files/keycloak-run-node.sh
@@ -24,6 +24,8 @@ function prepareHost
 
   # Enable Infinispan provider
   sed -i "s|keycloak.userSessions.provider:mem|keycloak.userSessions.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json
+  sed -i "s|keycloak.realm.cache.provider:mem|keycloak.realm.cache.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json
+  sed -i "s|keycloak.user.cache.provider:mem|keycloak.user.cache.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json
 
   # Deploy and configure examples
   /keycloak-docker-cluster/shared-files/deploy-examples.sh