Merge pull request #754 from mposolda/master
KEYCLOAK748 cluster-aware logout for non-distributable applications
This commit is contained in:
commit
3d5d7777f6
9 changed files with 113 additions and 47 deletions
|
@ -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";
|
||||
}
|
||||
|
|
36
core/src/main/java/org/keycloak/util/HostUtils.java
Normal file
36
core/src/main/java/org/keycloak/util/HostUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue