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