Merge pull request #754 from mposolda/master

KEYCLOAK748 cluster-aware logout for non-distributable applications
This commit is contained in:
Marek Posolda 2014-10-09 21:36:13 +02:00
commit 3d5d7777f6
9 changed files with 113 additions and 47 deletions

View file

@ -19,6 +19,9 @@ public interface AdapterConstants {
// two places to avoid dependency between Keycloak Subsystem and Keyclaok Undertow Integration. // two places to avoid dependency between Keycloak Subsystem and Keyclaok Undertow Integration.
String AUTH_DATA_PARAM_NAME = "org.keycloak.json.adapterConfig"; 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"; 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";
} }

View file

@ -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);
}
}
}

View file

@ -1,8 +1,6 @@
package org.keycloak.util; package org.keycloak.util;
import java.net.InetAddress;
import java.net.URI; import java.net.URI;
import java.net.UnknownHostException;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -18,12 +16,4 @@ public class UriUtils {
return u.substring(0, u.indexOf('/', 8)); return u.substring(0, u.indexOf('/', 8));
} }
public static String getHostName() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException uhe) {
throw new IllegalStateException(uhe);
}
}
} }

View file

@ -13,6 +13,7 @@ import org.keycloak.ServiceUrlConstants;
import org.keycloak.adapters.HttpClientBuilder; import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.util.HostUtils;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.KeycloakUriBuilder;
import org.keycloak.util.UriUtils; import org.keycloak.util.UriUtils;
@ -161,7 +162,7 @@ public class AdminClient {
public static String getBaseUrl(HttpServletRequest request) { public static String getBaseUrl(HttpServletRequest request) {
String useHostname = request.getServletContext().getInitParameter("useHostname"); String useHostname = request.getServletContext().getInitParameter("useHostname");
if (useHostname != null && "true".equalsIgnoreCase(useHostname)) { if (useHostname != null && "true".equalsIgnoreCase(useHostname)) {
return "http://" + UriUtils.getHostName() + ":8080"; return "http://" + HostUtils.getHostName() + ":8080";
} else { } else {
return UriUtils.getOrigin(request.getRequestURL().toString()); return UriUtils.getOrigin(request.getRequestURL().toString());
} }

View file

@ -11,6 +11,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.HostUtils;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.KeycloakUriBuilder;
import org.keycloak.util.StreamUtil; import org.keycloak.util.StreamUtil;
@ -101,6 +102,7 @@ public class ServerRequest {
formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri)); formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri));
if (sessionId != null) { if (sessionId != null) {
formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_ID, sessionId)); formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_ID, sessionId));
formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_HOST, HostUtils.getIpAddress()));
} }
HttpResponse response = null; HttpResponse response = null;
HttpPost post = new HttpPost(codeUrl); HttpPost post = new HttpPost(codeUrl);

View file

@ -614,9 +614,13 @@ public class OpenIDConnectService {
String httpSessionId = formData.getFirst(AdapterConstants.HTTP_SESSION_ID); String httpSessionId = formData.getFirst(AdapterConstants.HTTP_SESSION_ID);
if (httpSessionId != null) { 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); event.detail(AdapterConstants.HTTP_SESSION_ID, httpSessionId);
clientSession.setNote(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); AccessToken token = tokenManager.createClientAccessToken(accessCode.getRequestedRoles(), realm, client, user, userSession);

View file

@ -20,6 +20,7 @@ import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.adapters.action.UserStats;
import org.keycloak.services.util.HttpClientBuilder; import org.keycloak.services.util.HttpClientBuilder;
import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.util.ResolveRelative;
import org.keycloak.util.MultivaluedHashMap;
import org.keycloak.util.StringPropertyReplacer; import org.keycloak.util.StringPropertyReplacer;
import org.keycloak.util.Time; import org.keycloak.util.Time;
@ -31,6 +32,8 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -38,6 +41,7 @@ import java.util.Map;
*/ */
public class ResourceAdminManager { public class ResourceAdminManager {
protected static Logger logger = Logger.getLogger(ResourceAdminManager.class); protected static Logger logger = Logger.getLogger(ResourceAdminManager.class);
private static final String KC_SESSION_HOST = "${kc_session_host}";
public static ApacheHttpClient4Executor createExecutor() { public static ApacheHttpClient4Executor createExecutor() {
HttpClient client = new HttpClientBuilder() HttpClient client = new HttpClientBuilder()
@ -69,7 +73,7 @@ public class ResourceAdminManager {
try { try {
// Map from "app" to clientSessions for this app // 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) { for (UserSessionModel userSession : userSessions) {
putClientSessions(clientSessions, userSession); 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()) { for (ClientSessionModel clientSession : userSession.getClientSessions()) {
ClientModel client = clientSession.getClient(); ClientModel client = clientSession.getClient();
if (client instanceof ApplicationModel) { if (client instanceof ApplicationModel) {
List<ClientSessionModel> curClientSessions = clientSessions.get(client); clientSessions.add((ApplicationModel)client, clientSession);
if (curClientSessions == null) {
curClientSessions = new ArrayList<ClientSessionModel>();
clientSessions.put((ApplicationModel)client, curClientSessions);
}
curClientSessions.add(clientSession);
} }
} }
} }
@ -103,7 +102,8 @@ public class ResourceAdminManager {
ApacheHttpClient4Executor executor = createExecutor(); ApacheHttpClient4Executor executor = createExecutor();
try { 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); putClientSessions(clientSessions, session);
logger.debugv("logging out {0} resources ", clientSessions.size()); logger.debugv("logging out {0} resources ", clientSessions.size());
@ -138,7 +138,7 @@ public class ResourceAdminManager {
List<ClientSessionModel> ourAppClientSessions = null; List<ClientSessionModel> ourAppClientSessions = null;
if (userSessions != 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) { for (UserSessionModel userSession : userSessions) {
putClientSessions(clientSessions, userSession); putClientSessions(clientSessions, userSession);
} }
@ -160,34 +160,40 @@ public class ResourceAdminManager {
String managementUrl = getManagementUrl(requestUri, resource); String managementUrl = getManagementUrl(requestUri, resource);
if (managementUrl != null) { 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) { if (clientSessions != null && clientSessions.size() > 0) {
adapterSessionIds = new ArrayList<String>(); adapterSessionIds = new MultivaluedHashMap<String, String>();
for (ClientSessionModel clientSession : clientSessions) { for (ClientSessionModel clientSession : clientSessions) {
String adapterSessionId = clientSession.getNote(AdapterConstants.HTTP_SESSION_ID); String adapterSessionId = clientSession.getNote(AdapterConstants.HTTP_SESSION_ID);
if (adapterSessionId != null) { 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); if (managementUrl.contains(KC_SESSION_HOST) && adapterSessionIds != null) {
String token = new TokenManager().encodeToken(realm, adminAction); boolean allPassed = true;
logger.debugv("logout resource {0} url: {1} sessionIds: " + adapterSessionIds, resource.getName(), managementUrl); // Send logout separately to each host (needed for single-sign-out in cluster for non-distributable apps - KEYCLOAK-748)
ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_LOGOUT).build().toString()); for (Map.Entry<String, List<String>> entry : adapterSessionIds.entrySet()) {
ClientResponse response; String host = entry.getKey();
try { List<String> sessionIds = entry.getValue();
response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(UserStats.class); String currentHostMgmtUrl = managementUrl.replace(KC_SESSION_HOST, host);
} catch (Exception e) { allPassed = logoutApplicationOnHost(realm, resource, sessionIds, client, notBefore, currentHostMgmtUrl) && allPassed;
logger.warn("Logout for application '" + resource.getName() + "' failed", e); }
return false;
} return allPassed;
try { } else {
boolean success = response.getStatus() == 204; // Send single logout request
logger.debug("logout success."); List<String> allSessionIds = null;
return success; if (adapterSessionIds != null) {
} finally { allSessionIds = new ArrayList<String>();
response.releaseConnection(); for (List<String> currentIds : adapterSessionIds.values()) {
allSessionIds.addAll(currentIds);
}
}
return logoutApplicationOnHost(realm, resource, allSessionIds, client, notBefore, managementUrl);
} }
} else { } else {
logger.debugv("Can't logout {0}: no management url", resource.getName()); 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) { public void pushRealmRevocationPolicy(URI requestUri, RealmModel realm) {
ApacheHttpClient4Executor executor = createExecutor(); ApacheHttpClient4Executor executor = createExecutor();
@ -224,7 +251,7 @@ public class ResourceAdminManager {
if (managementUrl != null) { if (managementUrl != null) {
PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), notBefore); PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), notBefore);
String token = new TokenManager().encodeToken(realm, adminAction); 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()); ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).build().toString());
ClientResponse response; ClientResponse response;
try { try {

View file

@ -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 # 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,/' 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 # Configure other examples
for I in *.war/WEB-INF/keycloak.json; do 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; done;
# Enable distributable for customer-portal # Enable distributable for customer-portal
sed -i -e 's/<\/module-name>/&\n <distributable \/>/' customer-portal.war/WEB-INF/web.xml 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 # 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

View file

@ -24,6 +24,8 @@ function prepareHost
# Enable Infinispan provider # 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.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 # Deploy and configure examples
/keycloak-docker-cluster/shared-files/deploy-examples.sh /keycloak-docker-cluster/shared-files/deploy-examples.sh