diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml
index 98eecbd853..5b2c21dd28 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml
@@ -23,6 +23,15 @@
+
+
+
+
+
+
+
+
+
@@ -35,9 +44,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectToMapMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectToMapMapper.java
index cfcf9bb1c2..1de0867b4a 100644
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectToMapMapper.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectToMapMapper.java
@@ -8,7 +8,7 @@ import java.util.HashMap;
import java.util.Map;
/**
- * For now, there is support just for convert to Map
+ * For now, there is support just for convert to Map
*
* @author Marek Posolda
*/
@@ -18,10 +18,10 @@ public class BasicDBObjectToMapMapper implements Mapper {
public Map convertObject(MapperContext context) {
BasicDBObject dbObjectToConvert = context.getObjectToConvert();
- HashMap result = new HashMap();
+ HashMap result = new HashMap();
for (Map.Entry entry : dbObjectToConvert.entrySet()) {
String key = entry.getKey();
- String value = (String)entry.getValue();
+ Object value = entry.getValue();
if (key.contains(MapMapper.DOT_PLACEHOLDER)) {
key = key.replaceAll(MapMapper.DOT_PLACEHOLDER, ".");
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/MapMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/MapMapper.java
index 80cf9d7081..988ce34185 100644
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/MapMapper.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/MapMapper.java
@@ -8,7 +8,7 @@ import java.util.Map;
import java.util.Set;
/**
- * For now, we support just convert from Map
+ * For now, we support just convert from Map
*
* @author Marek Posolda
*/
@@ -31,7 +31,7 @@ public class MapMapper implements Mapper {
Set entries = objectToConvert.entrySet();
for (Map.Entry entry : entries) {
String key = (String)entry.getKey();
- String value = (String)entry.getValue();
+ Object value = entry.getValue();
if (key.contains(".")) {
key = key.replaceAll("\\.", DOT_PLACEHOLDER);
diff --git a/core/src/main/java/org/keycloak/ServiceUrlConstants.java b/core/src/main/java/org/keycloak/ServiceUrlConstants.java
index 7ff32542cf..90e71702cc 100755
--- a/core/src/main/java/org/keycloak/ServiceUrlConstants.java
+++ b/core/src/main/java/org/keycloak/ServiceUrlConstants.java
@@ -13,5 +13,7 @@ public interface ServiceUrlConstants {
public static final String TOKEN_SERVICE_DIRECT_GRANT_PATH = "/realms/{realm-name}/protocol/openid-connect/grants/access";
public static final String ACCOUNT_SERVICE_PATH = "/realms/{realm-name}/account";
public static final String REALM_INFO_PATH = "/realms/{realm-name}";
+ public static final String CLIENTS_MANAGEMENT_REGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/register-node";
+ public static final String CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/unregister-node";
}
diff --git a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java
index a4f7f51bc4..ddc68aa653 100755
--- a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java
+++ b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java
@@ -20,8 +20,11 @@ public interface AdapterConstants {
String AUTH_DATA_PARAM_NAME = "org.keycloak.json.adapterConfig";
// 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 APPLICATION_SESSION_STATE = "application_session_state";
// 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";
+ public static final String APPLICATION_SESSION_HOST = "application_session_host";
+
+ // Attribute passed in registerNode request for register new application cluster node once he joined cluster
+ public static final String APPLICATION_CLUSTER_HOST = "application_cluster_host";
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
index 7f62fd18bb..778f9f5b34 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
@@ -17,7 +17,8 @@ import org.codehaus.jackson.annotate.JsonPropertyOrder;
"connection-pool-size",
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
"client-keystore", "client-keystore-password", "client-key-password",
- "auth-server-url-for-backend-requests", "always-refresh-token"
+ "auth-server-url-for-backend-requests", "always-refresh-token",
+ "register-node-at-startup", "register-node-period"
})
public class AdapterConfig extends BaseAdapterConfig {
@@ -41,6 +42,10 @@ public class AdapterConfig extends BaseAdapterConfig {
protected String authServerUrlForBackendRequests;
@JsonProperty("always-refresh-token")
protected boolean alwaysRefreshToken = false;
+ @JsonProperty("register-node-at-startup")
+ protected boolean registerNodeAtStartup = false;
+ @JsonProperty("register-node-period")
+ protected int registerNodePeriod = -1;
public boolean isAllowAnyHostname() {
return allowAnyHostname;
@@ -121,4 +126,20 @@ public class AdapterConfig extends BaseAdapterConfig {
public void setAlwaysRefreshToken(boolean alwaysRefreshToken) {
this.alwaysRefreshToken = alwaysRefreshToken;
}
+
+ public boolean isRegisterNodeAtStartup() {
+ return registerNodeAtStartup;
+ }
+
+ public void setRegisterNodeAtStartup(boolean registerNodeAtStartup) {
+ this.registerNodeAtStartup = registerNodeAtStartup;
+ }
+
+ public int getRegisterNodePeriod() {
+ return registerNodePeriod;
+ }
+
+ public void setRegisterNodePeriod(int registerNodePeriod) {
+ this.registerNodePeriod = registerNodePeriod;
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java
index 5a3a8fd7ae..faee3c8f8d 100755
--- a/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java
@@ -25,7 +25,8 @@ public class ApplicationRepresentation {
protected String protocol;
protected Map attributes;
protected Boolean fullScopeAllowed;
-
+ protected Integer nodeReRegistrationTimeout;
+ protected Map registeredNodes;
public String getId() {
return id;
@@ -162,4 +163,20 @@ public class ApplicationRepresentation {
public void setAttributes(Map attributes) {
this.attributes = attributes;
}
+
+ public Integer getNodeReRegistrationTimeout() {
+ return nodeReRegistrationTimeout;
+ }
+
+ public void setNodeReRegistrationTimeout(Integer nodeReRegistrationTimeout) {
+ this.nodeReRegistrationTimeout = nodeReRegistrationTimeout;
+ }
+
+ public Map getRegisteredNodes() {
+ return registeredNodes;
+ }
+
+ public void setRegisteredNodes(Map registeredNodes) {
+ this.registeredNodes = registeredNodes;
+ }
}
diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java
index 917b772aba..859a6f7189 100755
--- a/events/api/src/main/java/org/keycloak/events/EventType.java
+++ b/events/api/src/main/java/org/keycloak/events/EventType.java
@@ -41,6 +41,8 @@ public enum EventType {
SEND_RESET_PASSWORD,
SEND_RESET_PASSWORD_ERROR,
SOCIAL_LOGIN,
- SOCIAL_LOGIN_ERROR
+ SOCIAL_LOGIN_ERROR,
+ REGISTER_NODE,
+ UNREGISTER_NODE
}
diff --git a/examples/demo-template/customer-app/src/main/java/org/keycloak/example/AdminClient.java b/examples/demo-template/customer-app/src/main/java/org/keycloak/example/AdminClient.java
index 611292b131..c1ccc38b48 100755
--- a/examples/demo-template/customer-app/src/main/java/org/keycloak/example/AdminClient.java
+++ b/examples/demo-template/customer-app/src/main/java/org/keycloak/example/AdminClient.java
@@ -43,7 +43,7 @@ public class AdminClient {
HttpClient client = new HttpClientBuilder()
.disableTrustManager().build();
try {
- HttpGet get = new HttpGet(AdapterUtils.getBaseUrl(req.getRequestURL().toString(), session) + "/auth/admin/realms/demo/roles");
+ HttpGet get = new HttpGet(AdapterUtils.getOrigin(req.getRequestURL().toString(), session) + "/auth/admin/realms/demo/roles");
get.addHeader("Authorization", "Bearer " + session.getTokenString());
try {
HttpResponse response = client.execute(get);
diff --git a/examples/demo-template/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java b/examples/demo-template/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java
index ec1ed04b46..0cb400f2a4 100755
--- a/examples/demo-template/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java
+++ b/examples/demo-template/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java
@@ -50,7 +50,7 @@ public class CustomerDatabaseClient {
HttpClient client = new HttpClientBuilder()
.disableTrustManager().build();
try {
- HttpGet get = new HttpGet(AdapterUtils.getBaseUrl(req.getRequestURL().toString(), session) + "/database/customers");
+ HttpGet get = new HttpGet(AdapterUtils.getOrigin(req.getRequestURL().toString(), session) + "/database/customers");
get.addHeader("Authorization", "Bearer " + session.getTokenString());
try {
HttpResponse response = client.execute(get);
diff --git a/examples/demo-template/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java b/examples/demo-template/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java
index 9557f28e0a..c8e9cf000c 100755
--- a/examples/demo-template/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java
+++ b/examples/demo-template/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java
@@ -40,7 +40,7 @@ public class ProductDatabaseClient
HttpClient client = new HttpClientBuilder()
.disableTrustManager().build();
try {
- HttpGet get = new HttpGet(AdapterUtils.getBaseUrl(req.getRequestURL().toString(), session) + "/database/products");
+ HttpGet get = new HttpGet(AdapterUtils.getOrigin(req.getRequestURL().toString(), session) + "/database/products");
get.addHeader("Authorization", "Bearer " + session.getTokenString());
try {
HttpResponse response = client.execute(get);
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
index 69e61aaf20..95e31d77ef 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
@@ -157,6 +157,16 @@ public class AdapterDeploymentContext {
return (this.accountUrl != null) ? this.accountUrl : delegate.getAccountUrl();
}
+ @Override
+ public String getRegisterNodeUrl() {
+ return (this.registerNodeUrl != null) ? this.registerNodeUrl : delegate.getRegisterNodeUrl();
+ }
+
+ @Override
+ public String getUnregisterNodeUrl() {
+ return (this.unregisterNodeUrl != null) ? this.unregisterNodeUrl : delegate.getUnregisterNodeUrl();
+ }
+
@Override
public String getResourceName() {
return delegate.getResourceName();
@@ -336,6 +346,26 @@ public class AdapterDeploymentContext {
public void setAlwaysRefreshToken(boolean alwaysRefreshToken) {
delegate.setAlwaysRefreshToken(alwaysRefreshToken);
}
+
+ @Override
+ public int getRegisterNodePeriod() {
+ return delegate.getRegisterNodePeriod();
+ }
+
+ @Override
+ public void setRegisterNodePeriod(int registerNodePeriod) {
+ delegate.setRegisterNodePeriod(registerNodePeriod);
+ }
+
+ @Override
+ public void setRegisterNodeAtStartup(boolean registerNodeAtStartup) {
+ delegate.setRegisterNodeAtStartup(registerNodeAtStartup);
+ }
+
+ @Override
+ public boolean isRegisterNodeAtStartup() {
+ return delegate.isRegisterNodeAtStartup();
+ }
}
protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) {
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java
index 0048d6aa50..e41a8b5fc0 100644
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java
@@ -8,7 +8,7 @@ import org.keycloak.util.UriUtils;
*/
public class AdapterUtils {
- public static String getBaseUrl(String browserRequestURL, KeycloakSecurityContext session) {
+ public static String getOrigin(String browserRequestURL, KeycloakSecurityContext session) {
if (session instanceof RefreshableKeycloakSecurityContext) {
KeycloakDeployment deployment = ((RefreshableKeycloakSecurityContext)session).getDeployment();
switch (deployment.getRelativeUrls()) {
@@ -16,10 +16,9 @@ public class AdapterUtils {
// Resolve baseURI from the request
return UriUtils.getOrigin(browserRequestURL);
case BROWSER_ONLY:
+ case NEVER:
// Resolve baseURI from the codeURL (This is already non-relative and based on our hostname)
return UriUtils.getOrigin(deployment.getCodeUrl());
- case NEVER:
- return "";
default:
return "";
}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
index c3761126f8..1095b47ce7 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
@@ -31,6 +31,8 @@ public class KeycloakDeployment {
protected String refreshUrl;
protected KeycloakUriBuilder logoutUrl;
protected String accountUrl;
+ protected String registerNodeUrl;
+ protected String unregisterNodeUrl;
protected String resourceName;
protected boolean bearerOnly;
@@ -48,6 +50,8 @@ public class KeycloakDeployment {
protected String corsAllowedMethods;
protected boolean exposeToken;
protected boolean alwaysRefreshToken;
+ protected boolean registerNodeAtStartup;
+ protected int registerNodePeriod;
protected volatile int notBefore;
public KeycloakDeployment() {
@@ -136,6 +140,8 @@ public class KeycloakDeployment {
accountUrl = authUrlBuilder.clone().path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH).build(getRealm()).toString();
realmInfoUrl = authUrlBuilder.clone().path(ServiceUrlConstants.REALM_INFO_PATH).build(getRealm()).toString();
codeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.TOKEN_SERVICE_ACCESS_CODE_PATH).build(getRealm()).toString();
+ registerNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_REGISTER_NODE_PATH).build(getRealm()).toString();
+ unregisterNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH).build(getRealm()).toString();
}
public RelativeUrlsUsed getRelativeUrls() {
@@ -166,6 +172,14 @@ public class KeycloakDeployment {
return accountUrl;
}
+ public String getRegisterNodeUrl() {
+ return registerNodeUrl;
+ }
+
+ public String getUnregisterNodeUrl() {
+ return unregisterNodeUrl;
+ }
+
public void setResourceName(String resourceName) {
this.resourceName = resourceName;
}
@@ -289,4 +303,20 @@ public class KeycloakDeployment {
public void setAlwaysRefreshToken(boolean alwaysRefreshToken) {
this.alwaysRefreshToken = alwaysRefreshToken;
}
+
+ public boolean isRegisterNodeAtStartup() {
+ return registerNodeAtStartup;
+ }
+
+ public void setRegisterNodeAtStartup(boolean registerNodeAtStartup) {
+ this.registerNodeAtStartup = registerNodeAtStartup;
+ }
+
+ public int getRegisterNodePeriod() {
+ return registerNodePeriod;
+ }
+
+ public void setRegisterNodePeriod(int registerNodePeriod) {
+ this.registerNodePeriod = registerNodePeriod;
+ }
}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
index 6e5c29fd86..cbb5fef00e 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
@@ -61,11 +61,13 @@ public class KeycloakDeploymentBuilder {
deployment.setBearerOnly(adapterConfig.isBearerOnly());
deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken());
+ deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup());
+ deployment.setRegisterNodePeriod(adapterConfig.getRegisterNodePeriod());
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");
}
- if (realmKeyPem == null || !deployment.isBearerOnly()) {
+ if (realmKeyPem == null || !deployment.isBearerOnly() || deployment.isRegisterNodeAtStartup() || deployment.getRegisterNodePeriod() != -1) {
deployment.setClient(new HttpClientBuilder().build(adapterConfig));
}
if (adapterConfig.getAuthServerUrl() == null && (!deployment.isBearerOnly() || realmKeyPem == null)) {
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/NodesRegistrationLifecycle.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/NodesRegistrationLifecycle.java
new file mode 100644
index 0000000000..b344702812
--- /dev/null
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/NodesRegistrationLifecycle.java
@@ -0,0 +1,120 @@
+package org.keycloak.adapters;
+
+import java.io.IOException;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.jboss.logging.Logger;
+import org.keycloak.enums.RelativeUrlsUsed;
+import org.keycloak.util.HostUtils;
+
+/**
+ * @author Marek Posolda
+ */
+public class NodesRegistrationLifecycle {
+
+ private static final Logger log = Logger.getLogger(NodesRegistrationLifecycle.class);
+
+ private final KeycloakDeployment deployment;
+ private final Timer timer;
+
+ // True if at least one event was successfully sent
+ private volatile boolean registered = false;
+
+ public NodesRegistrationLifecycle(KeycloakDeployment deployment) {
+ this.deployment = deployment;
+ this.timer = new Timer();
+ }
+
+ public void start() {
+ if (!deployment.isRegisterNodeAtStartup() && deployment.getRegisterNodePeriod() <= 0) {
+ log.info("Skip registration of cluster nodes at startup");
+ return;
+ }
+
+ if (deployment.getRelativeUrls() == RelativeUrlsUsed.ALL_REQUESTS) {
+ log.warn("Skip registration of cluster nodes at startup as Keycloak node can't be contacted. Make sure to not use relative URI in adapters configuration!");
+ return;
+ }
+
+ if (deployment.isRegisterNodeAtStartup()) {
+ boolean success = sendRegistrationEvent();
+ if (!success) {
+ throw new IllegalStateException("Failed to register node to keycloak at startup");
+ }
+ }
+
+ if (deployment.getRegisterNodePeriod() > 0) {
+ addPeriodicListener();
+ }
+ }
+
+ public void stop() {
+ removePeriodicListener();
+ if (registered) {
+ sendUnregistrationEvent();
+ }
+ }
+
+ protected void addPeriodicListener() {
+ TimerTask task = new TimerTask() {
+
+ @Override
+ public void run() {
+ sendRegistrationEvent();
+ }
+ };
+
+ long interval = deployment.getRegisterNodePeriod() * 1000;
+ log.info("Setup of periodic re-registration event sending each " + interval + " ms");
+ timer.schedule(task, interval, interval);
+ }
+
+ protected void removePeriodicListener() {
+ timer.cancel();
+ }
+
+ protected boolean sendRegistrationEvent() {
+ log.info("Sending registration event right now");
+
+ String host = HostUtils.getIpAddress();
+ try {
+ ServerRequest.invokeRegisterNode(deployment, host);
+ log.infof("Node '%s' successfully registered in Keycloak", host);
+ registered = true;
+ return true;
+ } catch (ServerRequest.HttpFailure failure) {
+ log.error("failed to register node to keycloak");
+ log.error("status from server: " + failure.getStatus());
+ if (failure.getError() != null) {
+ log.error(" " + failure.getError());
+ }
+ return false;
+ } catch (IOException e) {
+ log.error("failed to register node to keycloak", e);
+ return false;
+ }
+ }
+
+ protected boolean sendUnregistrationEvent() {
+ log.info("Sending UNregistration event right now");
+
+ String host = HostUtils.getIpAddress();
+ try {
+ ServerRequest.invokeUnregisterNode(deployment, host);
+ log.infof("Node '%s' successfully unregistered from Keycloak", host);
+ return true;
+ } catch (ServerRequest.HttpFailure failure) {
+ log.error("failed to unregister node from keycloak");
+ log.error("status from server: " + failure.getStatus());
+ if (failure.getError() != null) {
+ log.error(" " + failure.getError());
+ }
+ return false;
+ } catch (IOException e) {
+ log.error("failed to unregister node from keycloak", e);
+ return false;
+ }
+ }
+
+}
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 b20697d354..438ad63cfe 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
@@ -101,8 +101,8 @@ public class ServerRequest {
formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
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()));
+ formparams.add(new BasicNameValuePair(AdapterConstants.APPLICATION_SESSION_STATE, sessionId));
+ formparams.add(new BasicNameValuePair(AdapterConstants.APPLICATION_SESSION_HOST, HostUtils.getIpAddress()));
}
HttpResponse response = null;
HttpPost post = new HttpPost(codeUrl);
@@ -212,6 +212,46 @@ public class ServerRequest {
}
}
+ public static void invokeRegisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException {
+ String registerNodeUrl = deployment.getRegisterNodeUrl();
+ String client_id = deployment.getResourceName();
+ Map credentials = deployment.getResourceCredentials();
+ HttpClient client = deployment.getClient();
+
+ invokeClientManagementRequest(client, host, registerNodeUrl, client_id, credentials);
+ }
+
+ public static void invokeUnregisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException {
+ String unregisterNodeUrl = deployment.getUnregisterNodeUrl();
+ String client_id = deployment.getResourceName();
+ Map credentials = deployment.getResourceCredentials();
+ HttpClient client = deployment.getClient();
+
+ invokeClientManagementRequest(client, host, unregisterNodeUrl, client_id, credentials);
+ }
+
+ public static void invokeClientManagementRequest(HttpClient client, String host, String endpointUrl, String clientId, Map credentials) throws HttpFailure, IOException {
+ List formparams = new ArrayList();
+ formparams.add(new BasicNameValuePair(AdapterConstants.APPLICATION_CLUSTER_HOST, host));
+
+ HttpPost post = new HttpPost(endpointUrl);
+
+ String clientSecret = credentials.get(CredentialRepresentation.SECRET);
+ if (clientSecret != null) {
+ String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+ post.setHeader("Authorization", authorization);
+ }
+
+ UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
+ post.setEntity(form);
+ HttpResponse response = client.execute(post);
+ int status = response.getStatusLine().getStatusCode();
+ if (status != 204) {
+ HttpEntity entity = response.getEntity();
+ error(status, entity);
+ }
+ }
+
public static void error(int status, HttpEntity entity) throws HttpFailure, IOException {
String body = null;
if (entity != null) {
diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java
index 9bf2c42636..ab4fb3f57a 100755
--- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java
+++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java
@@ -21,6 +21,7 @@ import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.NodesRegistrationLifecycle;
import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
@@ -46,6 +47,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
private static final Logger log = Logger.getLogger(KeycloakAuthenticatorValve.class);
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
protected AdapterDeploymentContext deploymentContext;
+ protected NodesRegistrationLifecycle nodesRegistrationLifecycle;
@Override
@@ -74,7 +76,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
@Override
public void lifecycleEvent(LifecycleEvent event) {
- if (event.getType() == Lifecycle.AFTER_START_EVENT) init();
+ if (event.getType() == Lifecycle.AFTER_START_EVENT) {
+ init();
+ } else if (event.getType() == Lifecycle.BEFORE_STOP_EVENT) {
+ beforeStop();
+ }
}
private static InputStream getJSONFromServletContext(ServletContext servletContext) {
@@ -119,6 +125,13 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getController());
setNext(actions);
+
+ nodesRegistrationLifecycle = new NodesRegistrationLifecycle(kd);
+ nodesRegistrationLifecycle.start();
+ }
+
+ protected void beforeStop() {
+ nodesRegistrationLifecycle.stop();
}
@Override
diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java
index 208882c447..de123895f5 100755
--- a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java
+++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java
@@ -20,6 +20,7 @@ import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.NodesRegistrationLifecycle;
import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.ServerRequest;
@@ -47,6 +48,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
private final static Logger log = Logger.getLogger(""+KeycloakAuthenticatorValve.class);
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
protected AdapterDeploymentContext deploymentContext;
+ protected NodesRegistrationLifecycle nodesRegistrationLifecycle;
@Override
public void lifecycleEvent(LifecycleEvent event) {
@@ -58,6 +60,8 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
}
} else if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) {
initInternal();
+ } else if (event.getType() == Lifecycle.BEFORE_STOP_EVENT) {
+ beforeStop();
}
}
@@ -99,6 +103,13 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getObjectName());
setNext(actions);
+
+ nodesRegistrationLifecycle = new NodesRegistrationLifecycle(kd);
+ nodesRegistrationLifecycle.start();
+ }
+
+ protected void beforeStop() {
+ nodesRegistrationLifecycle.stop();
}
private static InputStream getJSONFromServletContext(ServletContext servletContext) {
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java
index 4c024562e9..9919dfa834 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java
@@ -25,13 +25,18 @@ import io.undertow.server.handlers.form.FormParserFactory;
import io.undertow.servlet.ServletExtension;
import io.undertow.servlet.api.AuthMethodConfig;
import io.undertow.servlet.api.DeploymentInfo;
+import io.undertow.servlet.api.InstanceFactory;
+import io.undertow.servlet.api.InstanceHandle;
+import io.undertow.servlet.api.ListenerInfo;
import io.undertow.servlet.api.LoginConfig;
import io.undertow.servlet.api.ServletSessionConfig;
+import io.undertow.servlet.util.ImmediateInstanceHandle;
import org.jboss.logging.Logger;
import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.NodesRegistrationLifecycle;
import javax.servlet.ServletContext;
import java.io.ByteArrayInputStream;
@@ -96,7 +101,7 @@ public class KeycloakServletExtension implements ServletExtension {
}
log.debug("KeycloakServletException initialization");
InputStream is = getConfigInputStream(servletContext);
- KeycloakDeployment deployment = null;
+ final KeycloakDeployment deployment;
if (is == null) {
log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
deployment = new KeycloakDeployment();
@@ -143,6 +148,17 @@ public class KeycloakServletExtension implements ServletExtension {
ServletSessionConfig cookieConfig = new ServletSessionConfig();
cookieConfig.setPath(deploymentInfo.getContextPath());
deploymentInfo.setServletSessionConfig(cookieConfig);
+
+ deploymentInfo.addListener(new ListenerInfo(UndertowNodesRegistrationLifecycleWrapper.class, new InstanceFactory() {
+
+ @Override
+ public InstanceHandle createInstance() throws InstantiationException {
+ NodesRegistrationLifecycle nodesRegistration = new NodesRegistrationLifecycle(deployment);
+ UndertowNodesRegistrationLifecycleWrapper listener = new UndertowNodesRegistrationLifecycleWrapper(nodesRegistration);
+ return new ImmediateInstanceHandle(listener);
+ }
+
+ }));
}
protected ServletKeycloakAuthMech createAuthenticationMechanism(DeploymentInfo deploymentInfo, AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement) {
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowNodesRegistrationLifecycleWrapper.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowNodesRegistrationLifecycleWrapper.java
new file mode 100644
index 0000000000..5c97f6b7ae
--- /dev/null
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowNodesRegistrationLifecycleWrapper.java
@@ -0,0 +1,28 @@
+package org.keycloak.adapters.undertow;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+
+import org.keycloak.adapters.NodesRegistrationLifecycle;
+
+/**
+ * @author Marek Posolda
+ */
+public class UndertowNodesRegistrationLifecycleWrapper implements ServletContextListener {
+
+ private final NodesRegistrationLifecycle delegate;
+
+ public UndertowNodesRegistrationLifecycleWrapper(NodesRegistrationLifecycle delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce) {
+ delegate.start();
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce) {
+ delegate.stop();
+ }
+}
diff --git a/model/api/src/main/java/org/keycloak/models/ApplicationModel.java b/model/api/src/main/java/org/keycloak/models/ApplicationModel.java
index 2433fadf99..14cd081dd2 100755
--- a/model/api/src/main/java/org/keycloak/models/ApplicationModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ApplicationModel.java
@@ -1,6 +1,7 @@
package org.keycloak.models;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -37,4 +38,20 @@ public interface ApplicationModel extends RoleContainerModel, ClientModel {
boolean isBearerOnly();
void setBearerOnly(boolean only);
+ int getNodeReRegistrationTimeout();
+
+ void setNodeReRegistrationTimeout(int timeout);
+
+ Map getRegisteredNodes();
+
+ /**
+ * Register node or just update the 'lastReRegistration' time if this node is already registered
+ *
+ * @param nodeHost
+ * @param registrationTime
+ */
+ void registerNode(String nodeHost, int registrationTime);
+
+ void unregisterNode(String nodeHost);
+
}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/ApplicationEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ApplicationEntity.java
index 95fefb9a68..c39ede4447 100644
--- a/model/api/src/main/java/org/keycloak/models/entities/ApplicationEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/ApplicationEntity.java
@@ -2,6 +2,7 @@ package org.keycloak.models.entities;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
/**
* @author Marek Posolda
@@ -12,10 +13,13 @@ public class ApplicationEntity extends ClientEntity {
private String managementUrl;
private String baseUrl;
private boolean bearerOnly;
+ private int nodeReRegistrationTimeout;
// We are using names of defaultRoles (not ids)
private List defaultRoles = new ArrayList();
+ private Map registeredNodes;
+
public boolean isSurrogateAuthRequired() {
return surrogateAuthRequired;
}
@@ -55,5 +59,21 @@ public class ApplicationEntity extends ClientEntity {
public void setDefaultRoles(List defaultRoles) {
this.defaultRoles = defaultRoles;
}
+
+ public int getNodeReRegistrationTimeout() {
+ return nodeReRegistrationTimeout;
+ }
+
+ public void setNodeReRegistrationTimeout(int nodeReRegistrationTimeout) {
+ this.nodeReRegistrationTimeout = nodeReRegistrationTimeout;
+ }
+
+ public Map getRegisteredNodes() {
+ return registeredNodes;
+ }
+
+ public void setRegisteredNodes(Map registeredNodes) {
+ this.registeredNodes = registeredNodes;
+ }
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 0baa697cc9..85331aca8e 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -222,6 +222,7 @@ public class ModelToRepresentation {
rep.setSurrogateAuthRequired(applicationModel.isSurrogateAuthRequired());
rep.setBaseUrl(applicationModel.getBaseUrl());
rep.setNotBefore(applicationModel.getNotBefore());
+ rep.setNodeReRegistrationTimeout(applicationModel.getNodeReRegistrationTimeout());
Set redirectUris = applicationModel.getRedirectUris();
if (redirectUris != null) {
@@ -237,6 +238,10 @@ public class ModelToRepresentation {
rep.setDefaultRoles(applicationModel.getDefaultRoles().toArray(new String[0]));
}
+ if (!applicationModel.getRegisteredNodes().isEmpty()) {
+ rep.setRegisteredNodes(new HashMap(applicationModel.getRegisteredNodes()));
+ }
+
return rep;
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 2d45360331..31e26c3609 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -374,8 +374,16 @@ public class RepresentationToModel {
if (resourceRep.isBearerOnly() != null) applicationModel.setBearerOnly(resourceRep.isBearerOnly());
if (resourceRep.isPublicClient() != null) applicationModel.setPublicClient(resourceRep.isPublicClient());
if (resourceRep.getProtocol() != null) applicationModel.setProtocol(resourceRep.getProtocol());
- if (resourceRep.isFullScopeAllowed() != null) applicationModel.setFullScopeAllowed(resourceRep.isFullScopeAllowed());
- else applicationModel.setFullScopeAllowed(true);
+ if (resourceRep.isFullScopeAllowed() != null) {
+ applicationModel.setFullScopeAllowed(resourceRep.isFullScopeAllowed());
+ } else {
+ applicationModel.setFullScopeAllowed(true);
+ }
+ if (resourceRep.getNodeReRegistrationTimeout() != null) {
+ applicationModel.setNodeReRegistrationTimeout(resourceRep.getNodeReRegistrationTimeout());
+ } else {
+ applicationModel.setNodeReRegistrationTimeout(-1);
+ }
applicationModel.updateApplication();
if (resourceRep.getNotBefore() != null) {
@@ -426,6 +434,12 @@ public class RepresentationToModel {
}
}
+ if (resourceRep.getRegisteredNodes() != null) {
+ for (Map.Entry entry : resourceRep.getRegisteredNodes().entrySet()) {
+ applicationModel.registerNode(entry.getKey(), entry.getValue());
+ }
+ }
+
if (addDefaultRoles && resourceRep.getDefaultRoles() != null) {
applicationModel.updateDefaultRoles(resourceRep.getDefaultRoles());
}
@@ -448,6 +462,7 @@ public class RepresentationToModel {
if (rep.getAdminUrl() != null) resource.setManagementUrl(rep.getAdminUrl());
if (rep.getBaseUrl() != null) resource.setBaseUrl(rep.getBaseUrl());
if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired());
+ if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout());
resource.updateApplication();
if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol());
@@ -475,6 +490,12 @@ public class RepresentationToModel {
resource.setWebOrigins(new HashSet(webOrigins));
}
+ if (rep.getRegisteredNodes() != null) {
+ for (Map.Entry entry : rep.getRegisteredNodes().entrySet()) {
+ resource.registerNode(entry.getKey(), entry.getValue());
+ }
+ }
+
if (rep.getClaims() != null) {
setClaims(resource, rep.getClaims());
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ApplicationAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ApplicationAdapter.java
index 50e3ccc97e..fd238811de 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ApplicationAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ApplicationAdapter.java
@@ -9,6 +9,7 @@ import org.keycloak.models.cache.entities.CachedApplication;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -185,6 +186,36 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode
return roles;
}
+ @Override
+ public int getNodeReRegistrationTimeout() {
+ if (updated != null) return updated.getNodeReRegistrationTimeout();
+ return cached.getNodeReRegistrationTimeout();
+ }
+
+ @Override
+ public void setNodeReRegistrationTimeout(int timeout) {
+ getDelegateForUpdate();
+ updated.setNodeReRegistrationTimeout(timeout);
+ }
+
+ @Override
+ public Map getRegisteredNodes() {
+ if (updated != null) return updated.getRegisteredNodes();
+ return cached.getRegisteredNodes();
+ }
+
+ @Override
+ public void registerNode(String nodeHost, int registrationTime) {
+ getDelegateForUpdate();
+ updated.registerNode(nodeHost, registrationTime);
+ }
+
+ @Override
+ public void unregisterNode(String nodeHost) {
+ getDelegateForUpdate();
+ updated.unregisterNode(nodeHost);
+ }
+
@Override
public boolean hasScope(RoleModel role) {
if (super.hasScope(role)) {
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedApplication.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedApplication.java
index 004c7ff100..e537ea7483 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedApplication.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedApplication.java
@@ -10,6 +10,7 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.TreeMap;
/**
* @author Bill Burke
@@ -22,6 +23,8 @@ public class CachedApplication extends CachedClient {
private List defaultRoles = new LinkedList();
private boolean bearerOnly;
private Map roles = new HashMap();
+ private int nodeReRegistrationTimeout;
+ private Map registeredNodes;
public CachedApplication(RealmCache cache, RealmProvider delegate, RealmModel realm, ApplicationModel model) {
super(cache, delegate, realm, model);
@@ -35,7 +38,8 @@ public class CachedApplication extends CachedClient {
cache.addCachedRole(new CachedApplicationRole(id, role, realm));
}
-
+ nodeReRegistrationTimeout = model.getNodeReRegistrationTimeout();
+ registeredNodes = new TreeMap(model.getRegisteredNodes());
}
public boolean isSurrogateAuthRequired() {
@@ -62,4 +66,11 @@ public class CachedApplication extends CachedClient {
return roles;
}
+ public int getNodeReRegistrationTimeout() {
+ return nodeReRegistrationTimeout;
+ }
+
+ public Map getRegisteredNodes() {
+ return registeredNodes;
+ }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java
index 05c4d5c85a..9eab924577 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java
@@ -9,6 +9,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.jpa.entities.ApplicationEntity;
import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.util.Time;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
@@ -16,6 +17,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -260,6 +262,35 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode
em.flush();
}
+ @Override
+ public int getNodeReRegistrationTimeout() {
+ return applicationEntity.getNodeReRegistrationTimeout();
+ }
+
+ @Override
+ public void setNodeReRegistrationTimeout(int timeout) {
+ applicationEntity.setNodeReRegistrationTimeout(timeout);
+ }
+
+ @Override
+ public Map getRegisteredNodes() {
+ return applicationEntity.getRegisteredNodes();
+ }
+
+ @Override
+ public void registerNode(String nodeHost, int registrationTime) {
+ Map currentNodes = getRegisteredNodes();
+ currentNodes.put(nodeHost, registrationTime);
+ em.flush();
+ }
+
+ @Override
+ public void unregisterNode(String nodeHost) {
+ Map currentNodes = getRegisteredNodes();
+ currentNodes.remove(nodeHost);
+ em.flush();
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ApplicationEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ApplicationEntity.java
index a20ca3d3a5..48a9ff0a84 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ApplicationEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ApplicationEntity.java
@@ -1,14 +1,19 @@
package org.keycloak.models.jpa.entities;
import javax.persistence.CascadeType;
+import javax.persistence.CollectionTable;
import javax.persistence.Column;
+import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
+import javax.persistence.MapKeyColumn;
import javax.persistence.OneToMany;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
/**
* @author Bill Burke
@@ -29,6 +34,9 @@ public class ApplicationEntity extends ClientEntity {
@Column(name="BEARER_ONLY")
private boolean bearerOnly;
+ @Column(name="NODE_REREG_TIMEOUT")
+ private int nodeReRegistrationTimeout;
+
@OneToMany(fetch = FetchType.EAGER, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "application")
Collection roles = new ArrayList();
@@ -36,6 +44,12 @@ public class ApplicationEntity extends ClientEntity {
@JoinTable(name="APPLICATION_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="APPLICATION_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")})
Collection defaultRoles = new ArrayList();
+ @ElementCollection
+ @MapKeyColumn(name="NAME")
+ @Column(name="VALUE")
+ @CollectionTable(name="APP_NODE_REGISTRATIONS", joinColumns={ @JoinColumn(name="APPLICATION_ID") })
+ Map registeredNodes = new HashMap();
+
public boolean isSurrogateAuthRequired() {
return surrogateAuthRequired;
}
@@ -83,4 +97,20 @@ public class ApplicationEntity extends ClientEntity {
public void setBearerOnly(boolean bearerOnly) {
this.bearerOnly = bearerOnly;
}
+
+ public int getNodeReRegistrationTimeout() {
+ return nodeReRegistrationTimeout;
+ }
+
+ public void setNodeReRegistrationTimeout(int nodeReRegistrationTimeout) {
+ this.nodeReRegistrationTimeout = nodeReRegistrationTimeout;
+ }
+
+ public Map getRegisteredNodes() {
+ return registeredNodes;
+ }
+
+ public void setRegisteredNodes(Map registeredNodes) {
+ this.registeredNodes = registeredNodes;
+ }
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java
index 2b6ef4cb0b..3f84c2b3d9 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java
@@ -11,10 +11,14 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.mongo.keycloak.entities.MongoApplicationEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
import org.keycloak.models.mongo.utils.MongoModelUtils;
+import org.keycloak.util.Time;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -218,6 +222,41 @@ public class ApplicationAdapter extends ClientAdapter im
updateMongoEntity();
}
+ @Override
+ public int getNodeReRegistrationTimeout() {
+ return getMongoEntity().getNodeReRegistrationTimeout();
+ }
+
+ @Override
+ public void setNodeReRegistrationTimeout(int timeout) {
+ getMongoEntity().setNodeReRegistrationTimeout(timeout);
+ updateMongoEntity();
+ }
+
+ @Override
+ public Map getRegisteredNodes() {
+ return getMongoEntity().getRegisteredNodes() == null ? Collections.emptyMap() : Collections.unmodifiableMap(getMongoEntity().getRegisteredNodes());
+ }
+
+ @Override
+ public void registerNode(String nodeHost, int registrationTime) {
+ MongoApplicationEntity entity = getMongoEntity();
+ if (entity.getRegisteredNodes() == null) {
+ entity.setRegisteredNodes(new HashMap());
+ }
+
+ entity.getRegisteredNodes().put(nodeHost, registrationTime);
+ updateMongoEntity();
+ }
+
+ @Override
+ public void unregisterNode(String nodeHost) {
+ MongoApplicationEntity entity = getMongoEntity();
+ if (entity.getRegisteredNodes() == null) return;
+
+ entity.getRegisteredNodes().remove(nodeHost);
+ updateMongoEntity();
+ }
@Override
public boolean equals(Object o) {
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index 7c6ba7b3d8..da26379f14 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -137,7 +137,7 @@ public class UserAdapter extends AbstractMongoAdapter implement
@Override
public Map getAttributes() {
- return user.getAttributes()==null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(user.getAttributes());
+ return user.getAttributes()==null ? Collections.emptyMap() : Collections.unmodifiableMap(user.getAttributes());
}
public MongoUserEntity getUser() {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java
index e520d8d004..73ac4b51b1 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java
@@ -23,8 +23,6 @@ package org.keycloak.protocol.oidc;
import org.jboss.logging.Logger;
import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;
-import org.jboss.resteasy.spi.HttpRequest;
-import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.ClientSessionModel;
@@ -142,7 +140,7 @@ public class OpenIDConnect implements LoginProtocol {
ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor();
try {
- new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, app, clientSession, executor, 0);
+ new ResourceAdminManager().logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession, executor);
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
}
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 44b3934666..d713450346 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java
@@ -37,7 +37,6 @@ import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
import org.keycloak.services.managers.ClientSessionCode;
-import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.flows.Flows;
@@ -52,12 +51,10 @@ import javax.ws.rs.HeaderParam;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
@@ -612,15 +609,15 @@ public class OpenIDConnectService {
.build();
}
- String httpSessionId = formData.getFirst(AdapterConstants.HTTP_SESSION_ID);
- if (httpSessionId != null) {
- 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);
+ String adapterSessionId = formData.getFirst(AdapterConstants.APPLICATION_SESSION_STATE);
+ if (adapterSessionId != null) {
+ String adapterSessionHost = formData.getFirst(AdapterConstants.APPLICATION_SESSION_HOST);
+ logger.infof("Adapter Session '%s' saved in ClientSession for client '%s'. Host is '%s'", adapterSessionId, client.getClientId(), adapterSessionHost);
- 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);
+ event.detail(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId);
+ clientSession.setNote(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId);
+ event.detail(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost);
+ clientSession.setNote(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost);
}
AccessToken token = tokenManager.createClientAccessToken(accessCode.getRequestedRoles(), realm, client, user, userSession);
@@ -646,6 +643,21 @@ public class OpenIDConnectService {
}
protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap formData, EventBuilder event) {
+ ClientModel client = authorizeClientBase(authorizationHeader, formData, event, realm);
+
+ if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) {
+ Map error = new HashMap();
+ error.put(OAuth2Constants.ERROR, "invalid_client");
+ error.put(OAuth2Constants.ERROR_DESCRIPTION, "Bearer-only not allowed");
+ event.error(Errors.INVALID_CLIENT);
+ throw new BadRequestException("Bearer-only not allowed", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
+ }
+
+ return client;
+ }
+
+ // Just authorize client without further checking about client type
+ public static ClientModel authorizeClientBase(String authorizationHeader, MultivaluedMap formData, EventBuilder event, RealmModel realm) {
String client_id;
String clientSecret;
if (authorizationHeader != null) {
@@ -686,14 +698,6 @@ public class OpenIDConnectService {
throw new BadRequestException("Client is not enabled", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
}
- if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) {
- Map error = new HashMap();
- error.put(OAuth2Constants.ERROR, "invalid_client");
- error.put(OAuth2Constants.ERROR_DESCRIPTION, "Bearer-only not allowed");
- event.error(Errors.INVALID_CLIENT);
- throw new BadRequestException("Bearer-only not allowed", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
- }
-
if (!client.isPublicClient()) {
if (clientSecret == null || !client.validateSecret(clientSecret)) {
Map error = new HashMap();
@@ -702,6 +706,7 @@ public class OpenIDConnectService {
throw new BadRequestException("Unauthorized Client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
}
}
+
return client;
}
diff --git a/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java b/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java
index 3f5de4d3e5..5463f39d16 100755
--- a/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java
@@ -9,10 +9,16 @@ import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.adapters.config.BaseRealmConfig;
import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.util.Time;
import java.net.URI;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
/**
* @author Bill Burke
@@ -46,6 +52,38 @@ public class ApplicationManager {
}
}
+ public Set validateRegisteredNodes(ApplicationModel application) {
+ Map registeredNodes = application.getRegisteredNodes();
+ if (registeredNodes == null || registeredNodes.isEmpty()) {
+ return Collections.emptySet();
+ }
+
+ int currentTime = Time.currentTime();
+
+ Set validatedNodes = new TreeSet();
+ if (application.getNodeReRegistrationTimeout() > 0) {
+ List toRemove = new LinkedList();
+ for (Map.Entry entry : registeredNodes.entrySet()) {
+ Integer lastReRegistration = entry.getValue();
+ if (lastReRegistration + application.getNodeReRegistrationTimeout() < currentTime) {
+ toRemove.add(entry.getKey());
+ } else {
+ validatedNodes.add(entry.getKey());
+ }
+ }
+
+ // Remove time-outed nodes
+ for (String node : toRemove) {
+ application.unregisterNode(node);
+ }
+ } else {
+ // Periodic node reRegistration is disabled, so allow all nodes
+ validatedNodes.addAll(registeredNodes.keySet());
+ }
+
+ return validatedNodes;
+ }
+
@JsonPropertyOrder({"realm", "realm-public-key", "bearer-only", "auth-server-url", "ssl-required",
"resource", "public-client", "credentials",
"use-resource-role-mappings"})
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 2a4576f99e..d959883bb5 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.KeycloakUriBuilder;
import org.keycloak.util.MultivaluedHashMap;
import org.keycloak.util.StringPropertyReplacer;
import org.keycloak.util.Time;
@@ -29,11 +30,11 @@ import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.HashMap;
+import java.util.Collections;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.TreeMap;
/**
* @author Bill Burke
@@ -41,7 +42,7 @@ import java.util.TreeMap;
*/
public class ResourceAdminManager {
protected static Logger logger = Logger.getLogger(ResourceAdminManager.class);
- private static final String KC_SESSION_HOST = "${kc_session_host}";
+ private static final String APPLICATION_SESSION_HOST_PROPERTY = "${application.session.host}";
public static ApacheHttpClient4Executor createExecutor() {
HttpClient client = new HttpClientBuilder()
@@ -63,6 +64,29 @@ public class ResourceAdminManager {
return StringPropertyReplacer.replaceProperties(absoluteURI);
}
+ private List getAllManagementUrls(URI requestUri, ApplicationModel application) {
+ String baseMgmtUrl = getManagementUrl(requestUri, application);
+ if (baseMgmtUrl == null) {
+ return Collections.emptyList();
+ }
+
+ Set registeredNodesHosts = new ApplicationManager().validateRegisteredNodes(application);
+
+ // No-cluster setup
+ if (registeredNodesHosts.isEmpty()) {
+ return Arrays.asList(baseMgmtUrl);
+ }
+
+ List result = new LinkedList();
+ KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(baseMgmtUrl);
+ for (String nodeHost : registeredNodesHosts) {
+ String currentNodeUri = uriBuilder.clone().host(nodeHost).build().toString();
+ result.add(currentNodeUri);
+ }
+
+ return result;
+ }
+
public void logoutUser(URI requestUri, RealmModel realm, UserModel user, KeycloakSession keycloakSession) {
List userSessions = keycloakSession.sessions().getUserSessions(realm, user);
logoutUserSessions(requestUri, realm, userSessions);
@@ -82,7 +106,7 @@ public class ResourceAdminManager {
logger.infov("logging out resources: " + clientSessions);
for (Map.Entry> entry : clientSessions.entrySet()) {
- logoutApplication(requestUri, realm, entry.getKey(), entry.getValue(), executor, 0);
+ logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue(), executor);
}
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
@@ -108,34 +132,18 @@ public class ResourceAdminManager {
logger.debugv("logging out {0} resources ", clientSessions.size());
for (Map.Entry> entry : clientSessions.entrySet()) {
- logoutApplication(requestUri, realm, entry.getKey(), entry.getValue(), executor, 0);
+ logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue(), executor);
}
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
}
}
- public void logoutAll(URI requestUri, RealmModel realm) {
+ public void logoutUserFromApplication(URI requestUri, RealmModel realm, ApplicationModel resource, UserModel user, KeycloakSession session) {
ApacheHttpClient4Executor executor = createExecutor();
try {
- realm.setNotBefore(Time.currentTime());
- List resources = realm.getApplications();
- logger.debugv("logging out {0} resources ", resources.size());
- for (ApplicationModel resource : resources) {
- logoutApplication(requestUri, realm, resource, (List)null, executor, realm.getNotBefore());
- }
- } finally {
- executor.getHttpClient().getConnectionManager().shutdown();
- }
- }
-
- public void logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, List userSessions) {
- ApacheHttpClient4Executor executor = createExecutor();
-
- try {
- resource.setNotBefore(Time.currentTime());
-
+ List userSessions = session.sessions().getUserSessions(realm, user);
List ourAppClientSessions = null;
if (userSessions != null) {
MultivaluedHashMap clientSessions = new MultivaluedHashMap();
@@ -145,18 +153,18 @@ public class ResourceAdminManager {
ourAppClientSessions = clientSessions.get(resource);
}
- logoutApplication(requestUri, realm, resource, ourAppClientSessions, executor, resource.getNotBefore());
+ logoutClientSessions(requestUri, realm, resource, ourAppClientSessions, executor);
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
}
}
- public boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, ClientSessionModel clientSession, ApacheHttpClient4Executor client, int notBefore) {
- return logoutApplication(requestUri, realm, resource, Arrays.asList(clientSession), client, notBefore);
+ public boolean logoutClientSession(URI requestUri, RealmModel realm, ApplicationModel resource, ClientSessionModel clientSession, ApacheHttpClient4Executor client) {
+ return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession), client);
}
- protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, List clientSessions, ApacheHttpClient4Executor client, int notBefore) {
+ protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ApplicationModel resource, List clientSessions, ApacheHttpClient4Executor client) {
String managementUrl = getManagementUrl(requestUri, resource);
if (managementUrl != null) {
@@ -165,22 +173,22 @@ public class ResourceAdminManager {
if (clientSessions != null && clientSessions.size() > 0) {
adapterSessionIds = new MultivaluedHashMap();
for (ClientSessionModel clientSession : clientSessions) {
- String adapterSessionId = clientSession.getNote(AdapterConstants.HTTP_SESSION_ID);
+ String adapterSessionId = clientSession.getNote(AdapterConstants.APPLICATION_SESSION_STATE);
if (adapterSessionId != null) {
- String host = clientSession.getNote(AdapterConstants.HTTP_SESSION_HOST);
+ String host = clientSession.getNote(AdapterConstants.APPLICATION_SESSION_HOST);
adapterSessionIds.add(host, adapterSessionId);
}
}
}
- if (managementUrl.contains(KC_SESSION_HOST) && adapterSessionIds != null) {
+ if (managementUrl.contains(APPLICATION_SESSION_HOST_PROPERTY) && 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;
+ String currentHostMgmtUrl = managementUrl.replace(APPLICATION_SESSION_HOST_PROPERTY, host);
+ allPassed = sendLogoutRequest(realm, resource, sessionIds, client, 0, currentHostMgmtUrl) && allPassed;
}
return allPassed;
@@ -193,7 +201,7 @@ public class ResourceAdminManager {
allSessionIds.addAll(currentIds);
}
}
- return logoutApplicationOnHost(realm, resource, allSessionIds, client, notBefore, managementUrl);
+ return sendLogoutRequest(realm, resource, allSessionIds, client, 0, managementUrl);
}
} else {
logger.debugv("Can't logout {0}: no management url", resource.getName());
@@ -201,7 +209,54 @@ public class ResourceAdminManager {
}
}
- protected boolean logoutApplicationOnHost(RealmModel realm, ApplicationModel resource, List adapterSessionIds, ApacheHttpClient4Executor client, int notBefore, String managementUrl) {
+ // Methods for logout all
+
+ public void logoutAll(URI requestUri, RealmModel realm) {
+ ApacheHttpClient4Executor executor = createExecutor();
+
+ try {
+ realm.setNotBefore(Time.currentTime());
+ List resources = realm.getApplications();
+ logger.debugv("logging out {0} resources ", resources.size());
+ for (ApplicationModel resource : resources) {
+ logoutApplication(requestUri, realm, resource, executor, realm.getNotBefore());
+ }
+ } finally {
+ executor.getHttpClient().getConnectionManager().shutdown();
+ }
+ }
+
+ public void logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource) {
+ ApacheHttpClient4Executor executor = createExecutor();
+ try {
+ resource.setNotBefore(Time.currentTime());
+ logoutApplication(requestUri, realm, resource, executor, resource.getNotBefore());
+ } finally {
+ executor.getHttpClient().getConnectionManager().shutdown();
+ }
+ }
+
+
+ protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, ApacheHttpClient4Executor executor, int notBefore) {
+ List mgmtUrls = getAllManagementUrls(requestUri, resource);
+ if (mgmtUrls.isEmpty()) {
+ logger.debug("No management URL or no registered cluster nodes for the application " + resource.getName());
+ return false;
+ }
+
+ logger.info("Send logoutApplication for URLs: " + mgmtUrls);
+
+ // Propagate this to all hosts
+ boolean anyFailed = false;
+ for (String mgmtUrl : mgmtUrls) {
+ if (!sendLogoutRequest(realm, resource, null, executor, notBefore, mgmtUrl)) {
+ anyFailed = true;
+ }
+ }
+ return !anyFailed;
+ }
+
+ protected boolean sendLogoutRequest(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);
@@ -245,33 +300,43 @@ public class ResourceAdminManager {
}
- protected boolean pushRevocationPolicy(URI requestUri, RealmModel realm, ApplicationModel resource, int notBefore, ApacheHttpClient4Executor client) {
- if (notBefore <= 0) return false;
- String managementUrl = getManagementUrl(requestUri, resource);
- if (managementUrl != null) {
- PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), notBefore);
- String token = new TokenManager().encodeToken(realm, adminAction);
- 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 {
- response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
-
- try {
- boolean success = response.getStatus() == 204;
- logger.debug("pushRevocation success.");
- return success;
- } finally {
- response.releaseConnection();
- }
- } else {
- logger.debug("no management URL for application: " + resource.getName());
+ protected boolean pushRevocationPolicy(URI requestUri, RealmModel realm, ApplicationModel resource, int notBefore, ApacheHttpClient4Executor executor) {
+ List mgmtUrls = getAllManagementUrls(requestUri, resource);
+ if (mgmtUrls.isEmpty()) {
+ logger.debug("No management URL or no registered cluster nodes for the application " + resource.getName());
return false;
}
+ logger.info("Sending push revocation to URLS: " + mgmtUrls);
+ // Propagate this to all hosts
+ boolean anyFailed= false;
+ for (String mgmtUrl : mgmtUrls) {
+ if (!sendPushRevocationPolicyRequest(realm, resource, notBefore, executor, mgmtUrl)) {
+ anyFailed = true;
+ }
+ }
+ return !anyFailed;
+ }
+
+ protected boolean sendPushRevocationPolicyRequest(RealmModel realm, ApplicationModel resource, int notBefore, ApacheHttpClient4Executor client, String managementUrl) {
+ PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), notBefore);
+ String token = new TokenManager().encodeToken(realm, adminAction);
+ 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 {
+ response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post();
+ } catch (Exception e) {
+ logger.warn("Failed to send revocation request", e);
+ return false;
+ }
+ try {
+ boolean success = response.getStatus() == 204;
+ logger.debug("pushRevocation success.");
+ return success;
+ } finally {
+ response.releaseConnection();
+ }
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
new file mode 100644
index 0000000000..689d892d95
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
@@ -0,0 +1,193 @@
+package org.keycloak.services.resources;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.Providers;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.spi.BadRequestException;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.jboss.resteasy.spi.UnauthorizedException;
+import org.keycloak.ClientConnection;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.AdapterConstants;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.OpenIDConnectService;
+import org.keycloak.services.ForbiddenException;
+import org.keycloak.util.Time;
+
+/**
+ * @author Marek Posolda
+ */
+public class ClientsManagementService {
+
+ protected static final Logger logger = Logger.getLogger(ClientsManagementService.class);
+
+ private RealmModel realm;
+
+ private EventBuilder event;
+
+ @Context
+ private HttpRequest request;
+
+ @Context
+ protected HttpHeaders headers;
+
+ @Context
+ private UriInfo uriInfo;
+
+ @Context
+ private ClientConnection clientConnection;
+
+ @Context
+ protected Providers providers;
+
+ @Context
+ protected KeycloakSession session;
+
+ public ClientsManagementService(RealmModel realm, EventBuilder event) {
+ this.realm = realm;
+ this.event = event;
+ }
+
+ public static UriBuilder clientsManagementBaseUrl(UriBuilder baseUriBuilder) {
+ return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getClientsManagementService");
+ }
+
+ public static UriBuilder registerNodeUrl(UriBuilder baseUriBuilder) {
+ UriBuilder uriBuilder = clientsManagementBaseUrl(baseUriBuilder);
+ return uriBuilder.path(ClientsManagementService.class, "registerNode");
+ }
+
+ public static UriBuilder unregisterNodeUrl(UriBuilder baseUriBuilder) {
+ UriBuilder uriBuilder = clientsManagementBaseUrl(baseUriBuilder);
+ return uriBuilder.path(ClientsManagementService.class, "unregisterNode");
+ }
+
+ /**
+ * URL invoked by adapter to register new application cluster node. Each application cluster node will invoke this URL once it joins cluster
+ *
+ * @param authorizationHeader
+ * @param formData
+ * @return
+ */
+ @Path("register-node")
+ @POST
+ @Produces("application/json")
+ public Response registerNode(@HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, final MultivaluedMap formData) {
+ if (!checkSsl()) {
+ throw new ForbiddenException("HTTPS required");
+ }
+
+ event.event(EventType.REGISTER_NODE);
+
+ if (!realm.isEnabled()) {
+ event.error(Errors.REALM_DISABLED);
+ throw new UnauthorizedException("Realm not enabled");
+ }
+
+ ApplicationModel application = authorizeApplication(authorizationHeader, formData);
+ String nodeHost = getApplicationClusterHost(formData);
+
+ logger.infof("Registering cluster host '%s' for client '%s'", nodeHost, application.getName());
+
+ application.registerNode(nodeHost, Time.currentTime());
+
+ return Response.noContent().build();
+ }
+
+
+ /**
+ * URL invoked by adapter to register new application cluster node. Each application cluster node will invoke this URL once it joins cluster
+ *
+ * @param authorizationHeader
+ * @param formData
+ * @return
+ */
+ @Path("unregister-node")
+ @POST
+ @Produces("application/json")
+ public Response unregisterNode(@HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, final MultivaluedMap formData) {
+ if (!checkSsl()) {
+ throw new ForbiddenException("HTTPS required");
+ }
+
+ event.event(EventType.UNREGISTER_NODE);
+
+ if (!realm.isEnabled()) {
+ event.error(Errors.REALM_DISABLED);
+ throw new UnauthorizedException("Realm not enabled");
+ }
+
+ ApplicationModel application = authorizeApplication(authorizationHeader, formData);
+ String nodeHost = getApplicationClusterHost(formData);
+
+ logger.infof("Unregistering cluster host '%s' for client '%s'", nodeHost, application.getName());
+
+ application.unregisterNode(nodeHost);
+
+ return Response.noContent().build();
+ }
+
+ protected ApplicationModel authorizeApplication(String authorizationHeader, MultivaluedMap formData) {
+ ClientModel client = OpenIDConnectService.authorizeClientBase(authorizationHeader, formData, event, realm);
+
+ if (client.isPublicClient()) {
+ Map error = new HashMap();
+ error.put(OAuth2Constants.ERROR, "invalid_client");
+ error.put(OAuth2Constants.ERROR_DESCRIPTION, "Public clients not allowed");
+ event.error(Errors.INVALID_CLIENT);
+ throw new BadRequestException("Public clients not allowed", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
+ }
+
+ if (!(client instanceof ApplicationModel)) {
+ Map error = new HashMap();
+ error.put(OAuth2Constants.ERROR, "invalid_client");
+ error.put(OAuth2Constants.ERROR_DESCRIPTION, "Just applications are allowed");
+ event.error(Errors.INVALID_CLIENT);
+ throw new BadRequestException("ust applications are allowed", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
+ }
+
+ return (ApplicationModel)client;
+ }
+
+ protected String getApplicationClusterHost(MultivaluedMap formData) {
+ String applicationClusterHost = formData.getFirst(AdapterConstants.APPLICATION_CLUSTER_HOST);
+ if (applicationClusterHost == null || applicationClusterHost.length() == 0) {
+ Map error = new HashMap();
+ error.put(OAuth2Constants.ERROR, "invalid_request");
+ error.put(OAuth2Constants.ERROR_DESCRIPTION, "application cluster host not specified");
+ event.error(Errors.INVALID_CODE);
+ throw new BadRequestException("Cluster host not specified", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
+ }
+
+ return applicationClusterHost;
+ }
+
+
+
+ private boolean checkSsl() {
+ if (uriInfo.getBaseUri().getScheme().equals("https")) {
+ return true;
+ } else {
+ return !realm.getSslRequired().isRequired(clientConnection);
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index 785b1014ac..2feeaaaebf 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -134,6 +134,16 @@ public class RealmsResource {
return service;
}
+ @Path("{realm}/clients-managements")
+ public ClientsManagementService getClientsManagementService(final @PathParam("realm") String name) {
+ RealmManager realmManager = new RealmManager(session);
+ RealmModel realm = locateRealm(name, realmManager);
+ EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder();
+ ClientsManagementService service = new ClientsManagementService(realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(service);
+ return service;
+ }
+
protected RealmModel locateRealm(String name, RealmManager realmManager) {
RealmModel realm = realmManager.getRealmByName(name);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
index 15a2773084..0c84d11a7f 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
@@ -335,7 +335,7 @@ public class ApplicationResource {
@POST
public void logoutAll() {
auth.requireManage();
- new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, null);
+ new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application);
}
/**
@@ -351,8 +351,7 @@ public class ApplicationResource {
throw new NotFoundException("User not found");
}
- List userSessions = session.sessions().getUserSessions(realm, user);
- new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, userSessions);
+ new ResourceAdminManager().logoutUserFromApplication(uriInfo.getRequestUri(), realm, application, user, session);
}
diff --git a/testsuite/docker-cluster/shared-files/deploy-examples.sh b/testsuite/docker-cluster/shared-files/deploy-examples.sh
index f71f697a9a..d2e3287965 100644
--- a/testsuite/docker-cluster/shared-files/deploy-examples.sh
+++ b/testsuite/docker-cluster/shared-files/deploy-examples.sh
@@ -29,7 +29,8 @@ sed -i -e 's/false/true/' admin-access.war/WEB-INF/web.xml
# Configure other examples
for I in *.war/WEB-INF/keycloak.json; do
- sed -i -e 's/\"auth-server-url\".*: \"\/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\",\
+ \n \"register-node-at-startup\": false,\n \"register-node-period\": 30,/' $I;
done;
# Enable distributable for customer-portal
@@ -37,6 +38,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:\/\/\$\{kc_session_host\}:8080\/product-portal/' /keycloak-docker-cluster/examples/testrealm.json
+sed -i -e 's/\"adminUrl\": \"\/product-portal/\"adminUrl\": \"http:\/\/\$\{application.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 7d350aa762..bc0d1e65ca 100644
--- a/testsuite/docker-cluster/shared-files/keycloak-run-node.sh
+++ b/testsuite/docker-cluster/shared-files/keycloak-run-node.sh
@@ -23,9 +23,8 @@ function prepareHost
cp -r /keycloak-docker-cluster/deployments/* $JBOSS_HOME/standalone/deployments/
# 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
+ sed -i "s|\"provider\".*: \"mem\"|\"provider\": \"infinispan\"|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json
+ sed -i -e "s/\"connectionsJpa\"/\n \"connectionsInfinispan\": \{\n \"default\" : \{\n \"cacheContainer\" : \"java:jboss\/infinispan\/Keycloak\"\n \}\n \},\n &/" $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
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ApplicationModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ApplicationModelTest.java
index 84bd57ccda..bb8c651043 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ApplicationModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ApplicationModelTest.java
@@ -45,6 +45,9 @@ public class ApplicationModelTest extends AbstractModelTest {
application.addWebOrigin("origin-1");
application.addWebOrigin("origin-2");
+ application.registerNode("node1", 10);
+ application.registerNode("10.20.30.40", 50);
+
application.updateApplication();
}
@@ -84,6 +87,7 @@ public class ApplicationModelTest extends AbstractModelTest {
Assert.assertTrue(expected.getRedirectUris().containsAll(actual.getRedirectUris()));
Assert.assertTrue(expected.getWebOrigins().containsAll(actual.getWebOrigins()));
+ Assert.assertTrue(expected.getRegisteredNodes().equals(actual.getRegisteredNodes()));
}
public static void assertEquals(List expected, List actual) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
index ac0eb6a156..97c433e2a1 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
@@ -97,6 +97,12 @@ public class ImportTest extends AbstractModelTest {
Assert.assertTrue(apps.values().contains(accountApp));
realm.getApplications().containsAll(apps.values());
+ Assert.assertEquals(50, application.getNodeReRegistrationTimeout());
+ Map appRegisteredNodes = application.getRegisteredNodes();
+ Assert.assertEquals(2, appRegisteredNodes.size());
+ Assert.assertTrue(10 == appRegisteredNodes.get("node1"));
+ Assert.assertTrue(20 == appRegisteredNodes.get("172.10.15.20"));
+
// Test finding applications by ID
Assert.assertNull(realm.getApplicationById("982734"));
Assert.assertEquals(application, realm.getApplicationById(application.getId()));
diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json
index a5bf38012e..fdb8f82a2e 100755
--- a/testsuite/integration/src/test/resources/model/testrealm.json
+++ b/testsuite/integration/src/test/resources/model/testrealm.json
@@ -94,7 +94,12 @@
"applications": [
{
"name": "Application",
- "enabled": true
+ "enabled": true,
+ "nodeReRegistrationTimeout": 50,
+ "registeredNodes": {
+ "node1": 10,
+ "172.10.15.20": 20
+ }
},
{
"name": "OtherApp",