From 00716b7bfcd09c626ac6450494f0f9e894062ff8 Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 9 Oct 2014 18:38:59 +0200 Subject: [PATCH 1/2] Docker config update --- testsuite/docker-cluster/shared-files/deploy-examples.sh | 7 ++++--- testsuite/docker-cluster/shared-files/keycloak-run-node.sh | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/testsuite/docker-cluster/shared-files/deploy-examples.sh b/testsuite/docker-cluster/shared-files/deploy-examples.sh index 0778a3d14a..8f980d6c9e 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 /' 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:\/\/\$\{jboss.host.name\}: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 55edc8267f..7d350aa762 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 From 9954d68a1f5d27448fcb6cf08c09d01e7df9f326 Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 9 Oct 2014 20:11:44 +0200 Subject: [PATCH 2/2] KEYCLOAK-748 cluster-aware logout for non-distributable apps --- .../keycloak/adapters/AdapterConstants.java | 5 +- .../java/org/keycloak/util/HostUtils.java | 36 ++++++++ .../main/java/org/keycloak/util/UriUtils.java | 10 --- .../org/keycloak/example/AdminClient.java | 3 +- .../org/keycloak/adapters/ServerRequest.java | 2 + .../protocol/oidc/OpenIDConnectService.java | 6 +- .../managers/ResourceAdminManager.java | 89 ++++++++++++------- .../shared-files/deploy-examples.sh | 2 +- 8 files changed, 108 insertions(+), 45 deletions(-) create mode 100644 core/src/main/java/org/keycloak/util/HostUtils.java diff --git a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java index 988bfba40e..a4f7f51bc4 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 0000000000..fb1b29fce1 --- /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 Marek Posolda + */ +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 60418ea225..873283f61e 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 Stian Thorgersen @@ -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 2a83775252..caa9ed6eea 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 970929b9f0..b20697d354 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 db16b482d2..007925c028 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 537951c9d5..2a4576f99e 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 Bill Burke @@ -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> clientSessions = new HashMap>(); + MultivaluedHashMap clientSessions = new MultivaluedHashMap(); for (UserSessionModel userSession : userSessions) { putClientSessions(clientSessions, userSession); } @@ -85,16 +89,11 @@ public class ResourceAdminManager { } } - private void putClientSessions(Map> clientSessions, UserSessionModel userSession) { + private void putClientSessions(MultivaluedHashMap clientSessions, UserSessionModel userSession) { for (ClientSessionModel clientSession : userSession.getClientSessions()) { ClientModel client = clientSession.getClient(); if (client instanceof ApplicationModel) { - List curClientSessions = clientSessions.get(client); - if (curClientSessions == null) { - curClientSessions = new ArrayList(); - 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> clientSessions = new HashMap>(); + // Map from "app" to clientSessions for this app + MultivaluedHashMap clientSessions = new MultivaluedHashMap(); putClientSessions(clientSessions, session); logger.debugv("logging out {0} resources ", clientSessions.size()); @@ -138,7 +138,7 @@ public class ResourceAdminManager { List ourAppClientSessions = null; if (userSessions != null) { - Map> clientSessions = new HashMap>(); + MultivaluedHashMap clientSessions = new MultivaluedHashMap(); for (UserSessionModel userSession : userSessions) { putClientSessions(clientSessions, userSession); } @@ -160,34 +160,40 @@ public class ResourceAdminManager { String managementUrl = getManagementUrl(requestUri, resource); if (managementUrl != null) { - List adapterSessionIds = null; + // Key is host, value is list of http sessions for this host + MultivaluedHashMap adapterSessionIds = null; if (clientSessions != null && clientSessions.size() > 0) { - adapterSessionIds = new ArrayList(); + adapterSessionIds = new MultivaluedHashMap(); 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> entry : adapterSessionIds.entrySet()) { + String host = entry.getKey(); + List 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 allSessionIds = null; + if (adapterSessionIds != null) { + allSessionIds = new ArrayList(); + for (List 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 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 8f980d6c9e..f71f697a9a 100644 --- a/testsuite/docker-cluster/shared-files/deploy-examples.sh +++ b/testsuite/docker-cluster/shared-files/deploy-examples.sh @@ -37,6 +37,6 @@ sed -i -e 's/<\/module-name>/&\n /' customer-portal.war/WEB # Configure testrealm.json - Enable adminUrl to access adapters on local machine 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:\/\/\$\{jboss.host.name\}:8080\/product-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